Skip to content

Commit 9eb2236

Browse files
committed
More shares work
1 parent 9ff4364 commit 9eb2236

4 files changed

Lines changed: 330 additions & 315 deletions

File tree

src/routes/(authenticated)/shares/user/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<Table.Root>
2626
<Table.Body>
2727
{#each userShares.outgoing as userShare, i (userShare.id)}
28-
<UserShareItem bind:userShare={userShares.outgoing[i]} />
28+
<UserShareItem bind:userShare={userShares.outgoing[i]} onUpdated={refreshUserShares} />
2929
{/each}
3030
</Table.Body>
3131
</Table.Root>
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<script lang="ts">
2+
import { Volume2, Waves, Zap } from '@lucide/svelte';
3+
import { shockersV1Api } from '$lib/api';
4+
import type { V2UserSharesListItem } from '$lib/api/internal/v2';
5+
import { ComparePermissionsAndLimits } from '$lib/comparers/UserShareComparer';
6+
import LoadingCircle from '$lib/components/svg/LoadingCircle.svelte';
7+
import * as Avatar from '$lib/components/ui/avatar';
8+
import { Badge } from '$lib/components/ui/badge';
9+
import { Button } from '$lib/components/ui/button';
10+
import * as Drawer from '$lib/components/ui/drawer';
11+
import Label from '$lib/components/ui/label/label.svelte';
12+
import Slider from '$lib/components/ui/slider/slider.svelte';
13+
import * as Tabs from '$lib/components/ui/tabs';
14+
import MultiPauseToggle from '$lib/components/utils/MultiPauseToggle.svelte';
15+
import PauseToggle from '$lib/components/utils/PauseToggle.svelte';
16+
import { onMount } from 'svelte';
17+
import { toast } from 'svelte-sonner';
18+
import PermissionSwitch from './PermissionSwitch.svelte';
19+
20+
interface Props {
21+
userShare: V2UserSharesListItem;
22+
onUpdated: () => void;
23+
editDrawer: boolean;
24+
}
25+
26+
let saving = $state(false);
27+
28+
let { userShare = $bindable(), onUpdated, editDrawer = $bindable() }: Props = $props();
29+
30+
let isUniformRestrictions = $state(false);
31+
32+
let uniformPermissions = $state({
33+
live: false,
34+
shock: false,
35+
vibrate: false,
36+
sound: false,
37+
});
38+
39+
let uniformLimits = $state({
40+
intensity: 1,
41+
duration: 300,
42+
});
43+
44+
let shares = $state(
45+
userShare.shares.map((share) => ({
46+
...share,
47+
limits: {
48+
intensity: share.limits.intensity ?? 100,
49+
duration: share.limits.duration ?? 30_000,
50+
},
51+
permissions: {
52+
live: share.permissions.live ?? false,
53+
shock: share.permissions.shock ?? false,
54+
vibrate: share.permissions.vibrate ?? false,
55+
sound: share.permissions.sound ?? false,
56+
},
57+
}))
58+
);
59+
60+
onMount(() => {
61+
if (shares.length === 0) {
62+
console.error('User share has no shares to edit.');
63+
return;
64+
}
65+
66+
isUniformRestrictions = userShare.shares.every((share) =>
67+
ComparePermissionsAndLimits(share, userShare.shares[0])
68+
);
69+
70+
const limit = shares[0].limits;
71+
uniformLimits = {
72+
intensity: limit.intensity === null ? 100 : limit.intensity,
73+
duration: limit.duration === null ? 30_000 : limit.duration,
74+
};
75+
76+
const permission = shares[0].permissions;
77+
uniformPermissions = {
78+
live: permission.live ?? false,
79+
shock: permission.shock ?? false,
80+
vibrate: permission.vibrate ?? false,
81+
sound: permission.sound ?? false,
82+
};
83+
});
84+
85+
function handleSave() {
86+
if (saving) return;
87+
saving = true;
88+
89+
if (isUniformRestrictions) {
90+
// Update the shares once more before saving to make sure we apply the uniform restrictions to all shockers
91+
// This can happen when the user doesnt change any value but switched to uniform mode.
92+
shares.forEach((share) => {
93+
share.permissions = uniformPermissions;
94+
share.limits = uniformLimits;
95+
});
96+
}
97+
98+
let promises: Promise<void>[] = [];
99+
100+
shares.forEach((share) => {
101+
promises.push(
102+
shockersV1Api
103+
.shockerShockerShareCodeUpdate(share.id, userShare.id, {
104+
limits: {
105+
intensity: share.limits.intensity === 100 ? null : share.limits.intensity,
106+
duration: share.limits.duration === 30_000 ? null : share.limits.duration,
107+
},
108+
permissions: share.permissions,
109+
})
110+
.then(() => {
111+
// Update the list copy of the share
112+
const index = userShare.shares.findIndex((s) => s.id === share.id);
113+
if (index !== -1) {
114+
userShare.shares[index] = { ...userShare.shares[index], ...share };
115+
}
116+
})
117+
.catch((error) => {
118+
toast.error(`Failed to update share ${share.id}: ${error.message}`);
119+
})
120+
);
121+
});
122+
123+
Promise.all(promises).finally(() => {
124+
editDrawer = false;
125+
saving = false;
126+
});
127+
}
128+
129+
function onTabChanged(value: string) {
130+
isUniformRestrictions = value === 'uniform';
131+
}
132+
133+
$effect(() => {
134+
// Nice instant state update to indicate what is happening to the share limits
135+
if (isUniformRestrictions) {
136+
shares.forEach((share) => {
137+
share.permissions = uniformPermissions;
138+
share.limits = uniformLimits;
139+
});
140+
}
141+
});
142+
</script>
143+
144+
<Drawer.Root
145+
bind:open={editDrawer}
146+
onOpenChange={(newState) => (editDrawer = newState)}
147+
direction="right"
148+
>
149+
<Drawer.Content>
150+
<div class="mx-auto w-full">
151+
<Drawer.Header>
152+
<Drawer.Description>Edit shares for</Drawer.Description>
153+
<Drawer.Title class="flex items-center gap-2 mt-1">
154+
<Avatar.Root class="size-10">
155+
<Avatar.Image src={userShare.image} alt="User Avatar" />
156+
<Avatar.Fallback>
157+
{userShare.name.charAt(0)}
158+
</Avatar.Fallback>
159+
</Avatar.Root>
160+
<b>{userShare.name}</b></Drawer.Title
161+
>
162+
</Drawer.Header>
163+
<div class="p-4 pb-0 mb-5">
164+
<Tabs.Root
165+
value={isUniformRestrictions ? 'uniform' : 'individual'}
166+
onValueChange={onTabChanged}
167+
>
168+
{isUniformRestrictions}
169+
<div class="flex items-center justify-between">
170+
<p class="text-lg font-bold grow self-end border-b border-b-neutral-800 mr-[-15px]">
171+
Limits and Permissions
172+
</p>
173+
174+
<Tabs.List>
175+
<Tabs.Trigger value="uniform">User</Tabs.Trigger>
176+
<Tabs.Trigger value="individual">Shockers</Tabs.Trigger>
177+
</Tabs.List>
178+
</div>
179+
180+
<Tabs.Content value="uniform">
181+
<p class="mb-6 text-neutral-400 text-[10pt] mt-[-10px] text-right">
182+
Apply same restrictions to all shockers
183+
</p>
184+
<!-- Intensity Slider -->
185+
<div class="flex flex-col gap-2 border-1 border-neutral-800 p-4 rounded-md h-75">
186+
<span class="flex">
187+
<span class="ml-auto">
188+
<MultiPauseToggle
189+
shockers={shares.map((share) => ({
190+
shockerId: share.id,
191+
paused: share.paused,
192+
userShareUserId: userShare.id,
193+
}))}
194+
onPausedChange={(paused) => {
195+
shares.forEach((share) => (share.paused = paused)); // Update the local copy of the shares
196+
userShare.shares.forEach((share) => (share.paused = paused)); // Update the actual lists shares
197+
}}
198+
/>
199+
</span>
200+
</span>
201+
<div>
202+
<Label class="mb-3 text-sm">Intensity: {uniformLimits.intensity}%</Label>
203+
<Slider
204+
type="single"
205+
bind:value={uniformLimits.intensity}
206+
min={0}
207+
max={100}
208+
step={1}
209+
/>
210+
</div>
211+
212+
<div>
213+
<Label class="mb-3 text-sm">Duration: {uniformLimits.duration / 1000}s</Label>
214+
<Slider
215+
type="single"
216+
bind:value={uniformLimits.duration}
217+
min={0}
218+
max={30_000}
219+
step={100}
220+
/>
221+
</div>
222+
223+
<br />
224+
225+
<div class="flex gap-3">
226+
<PermissionSwitch icon={Zap} bind:enabled={uniformPermissions.shock} />
227+
<PermissionSwitch icon={Waves} bind:enabled={uniformPermissions.vibrate} />
228+
<PermissionSwitch icon={Volume2} bind:enabled={uniformPermissions.sound} />
229+
<PermissionSwitch icon={Volume2} bind:enabled={uniformPermissions.live} />
230+
</div>
231+
</div>
232+
</Tabs.Content>
233+
<Tabs.Content value="individual">
234+
<p class="mb-6 text-neutral-400 text-[10pt] mt-[-10px] text-right">
235+
Change restrictions for individual shockers
236+
</p>
237+
<div class="flex flex-col gap-8 overflow-x-auto">
238+
{#each shares as share}
239+
<div class="flex flex-col gap-2 border-1 border-neutral-800 p-4 rounded-md">
240+
<div class="flex justify-between">
241+
<span>
242+
<Badge>{share.name}</Badge>
243+
</span>
244+
<PauseToggle
245+
shockerId={share.id}
246+
bind:paused={share.paused}
247+
userShareUserId={userShare.id}
248+
onPausedChange={(paused) => {
249+
userShare.shares.forEach((s) => {
250+
if (s.id === share.id) {
251+
s.paused = paused; // Update the actual shares list
252+
}
253+
});
254+
}}
255+
/>
256+
</div>
257+
<div>
258+
<Label class="mb-3 text-sm">Intensity: {share.limits.intensity}%</Label>
259+
<Slider
260+
type="single"
261+
bind:value={share.limits.intensity}
262+
min={0}
263+
max={100}
264+
step={1}
265+
/>
266+
</div>
267+
268+
<div>
269+
<Label class="mb-3 text-sm">Duration: {share.limits.duration / 1000}s</Label>
270+
<Slider
271+
type="single"
272+
bind:value={share.limits.duration}
273+
min={0}
274+
max={30_000}
275+
step={100}
276+
/>
277+
</div>
278+
279+
<br />
280+
281+
<div class="flex gap-3">
282+
<PermissionSwitch icon={Zap} bind:enabled={share.permissions.shock} />
283+
<PermissionSwitch icon={Waves} bind:enabled={share.permissions.vibrate} />
284+
<PermissionSwitch icon={Volume2} bind:enabled={share.permissions.sound} />
285+
<PermissionSwitch icon={Volume2} bind:enabled={share.permissions.live} />
286+
</div>
287+
</div>
288+
{/each}
289+
</div>
290+
</Tabs.Content>
291+
</Tabs.Root>
292+
</div>
293+
294+
<Drawer.Footer class="flex flex-row justify-between mx-20">
295+
<Drawer.Close>Cancel</Drawer.Close>
296+
<Button onclick={handleSave}
297+
>Save {#if saving}<LoadingCircle />{/if}</Button
298+
>
299+
</Drawer.Footer>
300+
</div>
301+
</Drawer.Content>
302+
</Drawer.Root>
303+
304+
<style>
305+
:global(.data-\[vaul-drawer-direction\=right\]\:sm\:max-w-sm) {
306+
&[data-vaul-drawer-direction='right'] {
307+
@media (width >= 40rem) {
308+
max-width: 33rem;
309+
}
310+
}
311+
}
312+
</style>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
</script>
1616

17-
<div class="border-1 border-neutral-800 rounded-lg p-4 w-30 h-30 flex flex-col gap-4 items-center justify-center cursor-pointer hover:bg-neutral-800 transition-colors duration-300"
17+
<div class="border-1 border-neutral-800 rounded-lg p-4 w-30 h-30 flex flex-col gap-4 items-center justify-center cursor-pointer hover:bg-neutral-800 transition-colors duration-300" aria-label="Toggle permission"
1818
onclick={() => (enabled = !enabled)}>
1919
<Icon size="100%" />
2020
<Switch checked={enabled} />

0 commit comments

Comments
 (0)