Skip to content

Commit 1a70556

Browse files
committed
hacky way for dialogs and drawers
1 parent f54ca33 commit 1a70556

10 files changed

Lines changed: 313 additions & 88 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script lang="ts" generics="T extends { value: string; label: string }">
2+
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
3+
import { type Snippet } from 'svelte';
4+
import * as Command from '$lib/components/ui/command/index.js';
5+
import * as Popover from '$lib/components/ui/popover/index.js';
6+
import { Button } from '$lib/components/ui/button/index.js';
7+
import { Label } from '$lib/components/ui/label/index.js';
8+
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
9+
import { LoaderCircle } from '@lucide/svelte';
10+
11+
const id = $props.id();
12+
13+
let {
14+
selected = $bindable([] as string[]),
15+
options,
16+
selectText,
17+
placeholder,
18+
noMatchText,
19+
disabled,
20+
label,
21+
loading,
22+
item
23+
}: {
24+
selected: string[];
25+
options: T[];
26+
selectText?: string;
27+
placeholder: string;
28+
noMatchText: string;
29+
disabled?: boolean;
30+
label?: string;
31+
loading?: boolean;
32+
item?: Snippet<[T]>;
33+
} = $props();
34+
let open = $state(false);
35+
let triggerRef = $state<HTMLButtonElement>(null!);
36+
</script>
37+
38+
<div class="flex w-full flex-col ">
39+
<Label for="{id}-combo" class="mb-2 px-1">{label}</Label>
40+
<Popover.Root bind:open>
41+
<Popover.Trigger id="{id}-combo" bind:ref={triggerRef}>
42+
{#snippet child({ props })}
43+
<Button
44+
variant="outline"
45+
{...props}
46+
class="justify-between"
47+
role="combobox"
48+
aria-expanded={open}
49+
{disabled}
50+
>
51+
<span class="overflow-hidden">
52+
{options
53+
.filter((o) => selected.includes(o.value))
54+
.map((o) => o.label)
55+
.join(', ') ||
56+
selectText ||
57+
placeholder}
58+
</span>
59+
{#if loading}
60+
<LoaderCircle class="size-4 shrink-0 opacity-50 animate-spin" />
61+
{:else}
62+
<ChevronsUpDownIcon class="size-4 shrink-0 opacity-50" />
63+
{/if}
64+
</Button>
65+
{/snippet}
66+
</Popover.Trigger>
67+
<Popover.Content class="p-0">
68+
<Command.Root>
69+
<Command.Input {placeholder} />
70+
<Command.List>
71+
<Command.Empty>{noMatchText}</Command.Empty>
72+
<Command.Group>
73+
{#each options as option}
74+
<Command.Item
75+
value={option.value}
76+
keywords={[option.value, option.label]}
77+
onSelect={() => {
78+
if (selected.includes(option.value)) {
79+
selected = selected.filter((v) => v !== option.value);
80+
} else {
81+
selected.push(option.value);
82+
}
83+
}}
84+
>
85+
<Checkbox checked={selected.includes(option.value)} class="mr-2" />
86+
{#if item}
87+
{@render item(option)}
88+
{:else}
89+
{option.label}
90+
{/if}
91+
</Command.Item>
92+
{/each}
93+
</Command.Group>
94+
</Command.List>
95+
</Command.Root>
96+
</Popover.Content>
97+
</Popover.Root>
98+
</div>

src/routes/(authenticated)/settings/api-tokens/dialog-token-create.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@
113113
<Dialog.Root bind:open={() => open, onOpenChange}>
114114
<Dialog.Content>
115115
<Dialog.Header>
116-
<Dialog.Title>Generate a new API Token</Dialog.Title>
117-
<Dialog.Description>Example text</Dialog.Description>
116+
<Dialog.Title>New API Token</Dialog.Title>
117+
<Dialog.Description>Please make sure to select the appropriate permissions for the token.</Dialog.Description>
118118
</Dialog.Header>
119119
<form class="modal-form border-surface-500 rounded-container-token space-y-4">
120120
<TextInput
Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
<script lang="ts">
22
import { shockerSharesV2Api } from '$lib/api';
3-
import type { V2UserShares } from '$lib/api/internal/v2';
3+
import type { V2UserShares, V2UserSharesListItem } from '$lib/api/internal/v2';
44
import * as Table from '$lib/components/ui/table';
55
import { onMount } from 'svelte';
66
import UserShareItem from './UserShareItem.svelte';
7+
import { Button } from '$lib/components/ui/button';
8+
import { Plus, RectangleEllipsis, User } from '@lucide/svelte';
9+
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
10+
import DialogShareCodeCreate from './dialog-share-code-create.svelte';
11+
import { OwnHubsStore, refreshOwnHubs } from '$lib/stores/HubsStore';
12+
import EditShare from './edit-share.svelte';
713
14+
let createDialogOpen = $state(false);
815
let userShares = $state<V2UserShares>({ outgoing: [], incoming: [] });
16+
let editShareDrawerOpen = $state(false);
17+
let editShareDrawerOpenCount = $state(0);
18+
let editIndex = $state(0);
919
1020
function refreshUserShares() {
1121
shockerSharesV2Api
@@ -18,15 +28,52 @@
1828
});
1929
}
2030
21-
onMount(refreshUserShares);
31+
onMount(() => {
32+
refreshUserShares()
33+
refreshOwnHubs()
34+
});
35+
36+
function openEditDrawer(userShareIndex: number) {
37+
editIndex = userShareIndex;
38+
editShareDrawerOpenCount += 1;
39+
editShareDrawerOpen = true;
40+
}
2241
</script>
2342

24-
<div class="overflow-y-auto rounded-md border p-0">
25-
<Table.Root>
26-
<Table.Body>
27-
{#each userShares.outgoing as userShare, i (userShare.id)}
28-
<UserShareItem bind:userShare={userShares.outgoing[i]} onUpdated={refreshUserShares} />
29-
{/each}
30-
</Table.Body>
31-
</Table.Root>
43+
<DialogShareCodeCreate bind:open={createDialogOpen} onCreated={refreshUserShares} />
44+
45+
{#key editShareDrawerOpenCount}
46+
{#if userShares.outgoing[editIndex] !== undefined}
47+
<EditShare bind:userShare={userShares.outgoing[editIndex]} bind:editDrawer={editShareDrawerOpen} />
48+
{/if}
49+
{/key}
50+
51+
<div class="h-full m-8 mt-4 flex flex-col gap-4">
52+
<div class="flex-none flex justify-end">
53+
<DropdownMenu.Root>
54+
<DropdownMenu.Trigger>
55+
{#snippet child({ props })}
56+
<Button {...props}>
57+
<Plus />
58+
New Share
59+
</Button>
60+
{/snippet}
61+
</DropdownMenu.Trigger>
62+
<DropdownMenu.Content>
63+
<DropdownMenu.Group>
64+
<DropdownMenu.Item><User /> To User</DropdownMenu.Item>
65+
<DropdownMenu.Item onclick={() => createDialogOpen = true}><RectangleEllipsis /> Create Code</DropdownMenu.Item>
66+
</DropdownMenu.Group>
67+
</DropdownMenu.Content>
68+
</DropdownMenu.Root>
69+
</div>
70+
<div class="rounded-md border overflow-y-auto">
71+
<Table.Root>
72+
<Table.Body>
73+
{#each userShares.outgoing as userShare, i (userShare.id)}
74+
<UserShareItem bind:userShare={userShares.outgoing[i]} onOpenEdit={() => openEditDrawer(i)} />
75+
{/each}
76+
</Table.Body>
77+
</Table.Root>
78+
</div>
3279
</div>

src/routes/(authenticated)/shares/user/UserShareItem.svelte

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,18 @@
88
import * as Tooltip from '$lib/components/ui/tooltip';
99
import MultiPauseToggle from '$lib/components/utils/MultiPauseToggle.svelte';
1010
import { Zap } from '@lucide/svelte';
11-
import EditShare from './EditShare.svelte';
11+
import EditShare from './edit-share.svelte';
1212
1313
interface Props {
1414
userShare: V2UserSharesListItem;
15-
onUpdated: () => void;
15+
onOpenEdit: () => void;
1616
}
1717
18-
let editDrawer = $state(false);
19-
let { userShare = $bindable(), onUpdated }: Props = $props();
18+
let { userShare = $bindable(), onOpenEdit }: Props = $props();
2019
2120
</script>
2221

23-
<EditShare bind:userShare={userShare} onUpdated={onUpdated} bind:editDrawer={editDrawer} />
24-
25-
<Table.Row onclick={() => (editDrawer = true)}>
22+
<Table.Row onclick={onOpenEdit}>
2623
<Table.Cell class="flex items-center font-medium">
2724
<Avatar.Root class="h-15 w-15">
2825
<Avatar.Image src={userShare.image} alt="User Avatar" />
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script lang="ts">
2+
import { shockerSharesV2Api } from '$lib/api';
3+
import { type ShockerPermLimitPair } from '$lib/api/internal/v1';
4+
import Button from '$lib/components/ui/button/button.svelte';
5+
import * as Dialog from '$lib/components/ui/dialog';
6+
import MultiSelectCombobox from '$lib/components/ui/multi-select-combobox/multi-select-combobox.svelte';
7+
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';
8+
import { OwnHubsStore } from '$lib/stores/HubsStore';
9+
10+
let availableShockers = $derived(
11+
Array.from($OwnHubsStore)
12+
.flatMap(([, hub]) => hub.shockers)
13+
.map((shocker) => ({
14+
value: shocker.id,
15+
label: shocker.name,
16+
}))
17+
);
18+
19+
interface Props {
20+
open: boolean;
21+
onCreated: (code: string) => void;
22+
}
23+
24+
let { open = $bindable(), onCreated }: Props = $props();
25+
26+
let shockerIds = $state<string[]>([]);
27+
let restrictions = $state<ShockerPermLimitPair>(getDefaultRestrictions());
28+
29+
function getDefaultRestrictions(): ShockerPermLimitPair {
30+
return {
31+
limits: {
32+
duration: 30_000,
33+
intensity: 100,
34+
},
35+
permissions: {
36+
shock: false,
37+
vibrate: false,
38+
sound: false,
39+
live: false,
40+
},
41+
};
42+
}
43+
44+
function onOpenChange(o: boolean) {
45+
if (!o) {
46+
shockerIds = [];
47+
restrictions = getDefaultRestrictions();
48+
}
49+
open = o;
50+
}
51+
52+
async function onFormSubmit() {
53+
try {
54+
const createdCode = await shockerSharesV2Api.sharesCreateShareInvite({
55+
shockers: shockerIds.map((id) => ({
56+
shockerId: id,
57+
permissions: restrictions.permissions,
58+
limits: restrictions.limits,
59+
})),
60+
});
61+
onCreated(createdCode);
62+
open = false;
63+
} catch (error) {
64+
await handleApiError(error);
65+
}
66+
}
67+
</script>
68+
69+
<Dialog.Root bind:open={() => open, onOpenChange}>
70+
<Dialog.Content>
71+
<Dialog.Header>
72+
<Dialog.Title>New Share Code</Dialog.Title>
73+
<Dialog.Description>Example text</Dialog.Description>
74+
</Dialog.Header>
75+
76+
<form class="modal-form border-surface-500 rounded-container-token space-y-4 min-w-0">
77+
<MultiSelectCombobox
78+
bind:selected={shockerIds}
79+
options={availableShockers}
80+
label="Shockers"
81+
placeholder="Select shockers to share..."
82+
noMatchText="Not matching shockers"
83+
></MultiSelectCombobox>
84+
</form>
85+
<Button onclick={onFormSubmit}>Create</Button>
86+
</Dialog.Content>
87+
</Dialog.Root>

0 commit comments

Comments
 (0)