feat: refactor play area into sidebar layout with buttplug, recordings, and leaderboard sub-pages
- Add /play sidebar layout (mobile nav + desktop sidebar) with SexyBackground - Move buttplug device control to /play/buttplug with Empty component and scan button - Move recordings from /me/recordings to /play/recordings - Move leaderboard to /play/leaderboard; redirect /leaderboard → /play/leaderboard - Redirect /me/recordings → /play/recordings and /play → /play/buttplug - Remove recordings entry from /me sidebar nav - Rename "SexyPlay" → "Play", swap bluetooth icon for rocket, remove subtitle - Add play.nav i18n keys (play, recordings, leaderboard, back_to_site, back_mobile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { getRecordings } from "$lib/services";
|
||||
|
||||
export async function load({ fetch }) {
|
||||
return {
|
||||
recordings: await getRecordings(fetch).catch(() => []),
|
||||
};
|
||||
}
|
||||
122
packages/frontend/src/routes/play/recordings/+page.svelte
Normal file
122
packages/frontend/src/routes/play/recordings/+page.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { goto } from "$app/navigation";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { deleteRecording, updateRecording } from "$lib/services";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Empty from "$lib/components/ui/empty";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
|
||||
import Meta from "$lib/components/meta/meta.svelte";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
let recordings = $state(untrack(() => data.recordings));
|
||||
let deleteTarget = $state<string | null>(null);
|
||||
let deleteOpen = $state(false);
|
||||
let deleting = $state(false);
|
||||
|
||||
function handleDeleteRecording(id: string) {
|
||||
deleteTarget = id;
|
||||
deleteOpen = true;
|
||||
}
|
||||
|
||||
async function confirmDeleteRecording() {
|
||||
if (!deleteTarget) return;
|
||||
deleting = true;
|
||||
try {
|
||||
await deleteRecording(deleteTarget);
|
||||
recordings = recordings.filter((r) => r.id !== deleteTarget);
|
||||
toast.success($_("me.recordings.delete_success"));
|
||||
deleteOpen = false;
|
||||
deleteTarget = null;
|
||||
} catch {
|
||||
toast.error($_("me.recordings.delete_error"));
|
||||
} finally {
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublishRecording(id: string) {
|
||||
try {
|
||||
await updateRecording(id, { status: "published" });
|
||||
recordings = recordings.map((r) => (r.id === id ? { ...r, status: "published" } : r));
|
||||
toast.success($_("me.recordings.publish_success"));
|
||||
} catch {
|
||||
toast.error($_("me.recordings.publish_error"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnpublishRecording(id: string) {
|
||||
try {
|
||||
await updateRecording(id, { status: "draft" });
|
||||
recordings = recordings.map((r) => (r.id === id ? { ...r, status: "draft" } : r));
|
||||
toast.success($_("me.recordings.unpublish_success"));
|
||||
} catch {
|
||||
toast.error($_("me.recordings.unpublish_error"));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlayRecording(id: string) {
|
||||
goto(`/play/buttplug?recording=${id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Meta title={$_("me.recordings.title")} />
|
||||
|
||||
<div class="py-3 sm:py-6 lg:pl-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold">{$_("me.recordings.title")}</h1>
|
||||
</div>
|
||||
|
||||
{#if recordings.length === 0}
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media>
|
||||
<span class="icon-[ri--play-list-2-line] w-8 h-8"></span>
|
||||
</Empty.Media>
|
||||
<Empty.Title>{$_("me.recordings.no_recordings")}</Empty.Title>
|
||||
<Empty.Description>{$_("me.recordings.no_recordings_description")}</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button
|
||||
href="/play/buttplug"
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] w-4 h-4 mr-2"></span>
|
||||
{$_("me.recordings.go_to_play")}
|
||||
</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each recordings as recording (recording.id)}
|
||||
<RecordingCard
|
||||
{recording}
|
||||
onPlay={handlePlayRecording}
|
||||
onPublish={handlePublishRecording}
|
||||
onUnpublish={handleUnpublishRecording}
|
||||
onDelete={handleDeleteRecording}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Root bind:open={deleteOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$_("me.recordings.delete_confirm")}</Dialog.Title>
|
||||
<Dialog.Description>This cannot be undone.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (deleteOpen = false)}>
|
||||
{$_("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" disabled={deleting} onclick={confirmDeleteRecording}>
|
||||
{deleting ? "Deleting…" : $_("common.delete")}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user