style: refine admin & me UI — card forms, back arrows, avatar in admin sidebar, Empty component

- Replace ← text with icon-[ri--arrow-left-line] in admin and me layouts
- Add avatar + admin shield badge to admin sidebar header
- Wrap all admin edit forms in Card (bg-card/50 border-primary/20) with styled inputs
- Fix sm:pl-6 → lg:pl-6 so extra left padding only applies when sidebar is visible
- Update security form submit button to gradient style matching profile
- Remove "View Public Profile" button from me/profile
- Use shadcn-svelte Empty component for recordings empty state
- Install empty component via shadcn-svelte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 18:16:39 +01:00
parent ba648c796a
commit d9a60f0572
26 changed files with 873 additions and 643 deletions

View File

@@ -1,8 +1,17 @@
<script lang="ts">
import { page } from "$app/state";
import { _ } from "svelte-i18n";
import { Avatar, AvatarImage, AvatarFallback } from "$lib/components/ui/avatar";
import { getUserInitials } from "$lib/utils";
import { getAssetUrl } from "$lib/api";
const { children } = $props();
const { children, data } = $props();
const user = $derived(data.authStatus.user!);
const avatarUrl = $derived(
user.avatar ? (getAssetUrl(user.avatar, "thumbnail") ?? undefined) : undefined,
);
const displayName = $derived(user.artist_name ?? user.email);
const navLinks = $derived([
{ name: $_("admin.nav.users"), href: "/admin/users", icon: "icon-[ri--team-line]" },
@@ -33,9 +42,10 @@
<div class="flex items-center gap-1 overflow-x-auto py-2 scrollbar-none">
<a
href="/"
class="shrink-0 text-xs text-muted-foreground hover:text-foreground transition-colors px-2"
class="shrink-0 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors px-2"
>
{$_("admin.nav.back_mobile")}
<span class="icon-[ri--arrow-left-line] h-4 w-4"></span>
<span class="hidden sm:inline">{$_("admin.nav.back_mobile")}</span>
</a>
{#each navLinks as link (link.href)}
<a
@@ -58,10 +68,27 @@
<!-- Sidebar (desktop only) -->
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
<div class="px-4 py-5 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors">
<a href="/" class="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
<span class="icon-[ri--arrow-left-line] h-3.5 w-3.5"></span>
{$_("admin.nav.back_to_site")}
</a>
<h1 class="mt-2 text-base font-bold text-foreground">{$_("admin.nav.title")}</h1>
<div class="mt-3 flex items-center gap-3">
<div class="relative shrink-0">
<Avatar class="h-9 w-9">
<AvatarImage src={avatarUrl} alt={displayName} />
<AvatarFallback class="text-xs">
{getUserInitials(displayName)}
</AvatarFallback>
</Avatar>
<span class="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary ring-2 ring-background">
<span class="icon-[ri--shield-keyhole-fill] h-2.5 w-2.5 text-primary-foreground"></span>
</span>
</div>
<div class="min-w-0">
<p class="text-sm font-semibold text-foreground truncate">{displayName}</p>
<p class="text-xs text-primary font-medium">{$_("admin.nav.title")}</p>
</div>
</div>
</div>
<nav class="flex-1 p-3 space-y-1">

View File

@@ -67,7 +67,7 @@
<Meta title={$_("admin.articles.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.articles.title")}</h1>
<div class="flex items-center gap-3">

View File

@@ -11,6 +11,7 @@
import { Textarea } from "$lib/components/ui/textarea";
import { TagsInput } from "$lib/components/ui/tags-input";
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
import { Card, CardContent } from "$lib/components/ui/card";
import { getAssetUrl } from "$lib/api";
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
import { DatePicker } from "$lib/components/ui/date-picker";
@@ -96,147 +97,165 @@
<Meta title={$_("admin.article_form.edit_title")} description={null} />
<div class="p-3 sm:p-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/articles" size="sm">
<Button variant="ghost" href="/admin/articles" size="sm" class="shrink-0">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">{$_("admin.article_form.edit_title")}</h1>
</div>
<div class="space-y-5 max-w-4xl">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input id="title" bind:value={title} />
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} />
</div>
</div>
<div class="space-y-1.5">
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea id="excerpt" bind:value={excerpt} rows={2} />
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</Button
>
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</Button
>
<Card class="bg-card/50 border-primary/20 max-w-4xl">
<CardContent class="space-y-5 pt-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input
id="slug"
bind:value={slug}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
<div class="space-y-1.5">
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea
bind:value={content}
class={`h-full min-h-96 font-mono text-sm resize-none ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
id="excerpt"
bind:value={excerpt}
rows={2}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
<div
class={`rounded-lg border border-border/40 bg-muted/20 p-4 overflow-auto prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground min-h-96 ${editorTab === "write" ? "hidden sm:block" : ""}`}
>
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if}
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</Button
>
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</Button
>
</div>
</div>
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
<Textarea
bind:value={content}
class={`h-full min-h-96 font-mono text-sm resize-none bg-background/50 border-primary/20 focus:border-primary ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
/>
<div
class={`rounded-lg border border-border/40 bg-muted/20 p-4 overflow-auto prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground min-h-96 ${editorTab === "write" ? "hidden sm:block" : ""}`}
>
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if}
</div>
</div>
</div>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
<!-- Author -->
<div class="space-y-1.5">
<Label>{$_("admin.article_form.author")}</Label>
<Select type="single" bind:value={authorId}>
<SelectTrigger class="w-full">
{#if selectedAuthor}
{#if selectedAuthor.avatar}
<img
src={getAssetUrl(selectedAuthor.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if}
{selectedAuthor.artist_name}
{:else}
<span class="text-muted-foreground">{$_("admin.article_form.no_author")}</span>
{/if}
</SelectTrigger>
<SelectContent>
<SelectItem value="">{$_("admin.article_form.no_author")}</SelectItem>
{#each data.authors as author (author.id)}
<SelectItem value={author.id}>
{#if author.avatar}
<div class="space-y-1.5">
<Label>{$_("admin.article_form.author")}</Label>
<Select type="single" bind:value={authorId}>
<SelectTrigger class="w-full bg-background/50 border-primary/20">
{#if selectedAuthor}
{#if selectedAuthor.avatar}
<img
src={getAssetUrl(author.avatar, "mini")}
src={getAssetUrl(selectedAuthor.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if}
{author.artist_name}
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input id="category" bind:value={category} />
{selectedAuthor.artist_name}
{:else}
<span class="text-muted-foreground">{$_("admin.article_form.no_author")}</span>
{/if}
</SelectTrigger>
<SelectContent>
<SelectItem value="">{$_("admin.article_form.no_author")}</SelectItem>
{#each data.authors as author (author.id)}
<SelectItem value={author.id}>
{#if author.avatar}
<img
src={getAssetUrl(author.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if}
{author.artist_name}
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input
id="category"
bind:value={category}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
</div>
</div>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
</div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</div>
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</CardContent>
</Card>
</div>

View File

@@ -11,6 +11,7 @@
import { TagsInput } from "$lib/components/ui/tags-input";
import { DatePicker } from "$lib/components/ui/date-picker";
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
import { Card, CardContent } from "$lib/components/ui/card";
import Meta from "$lib/components/meta/meta.svelte";
let title = $state("");
@@ -78,129 +79,135 @@
<Meta title={$_("admin.article_form.new_title")} description={null} />
<div class="p-3 sm:p-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/articles" size="sm">
<Button variant="ghost" href="/admin/articles" size="sm" class="shrink-0">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">{$_("admin.article_form.new_title")}</h1>
</div>
<div class="space-y-5 max-w-4xl">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder={$_("admin.article_form.title_placeholder")}
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input
id="slug"
bind:value={slug}
placeholder={$_("admin.article_form.slug_placeholder")}
/>
</div>
</div>
<div class="space-y-1.5">
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea
id="excerpt"
bind:value={excerpt}
placeholder={$_("admin.article_form.excerpt_placeholder")}
rows={2}
/>
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</Button
>
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</Button
>
<Card class="bg-card/50 border-primary/20 max-w-4xl">
<CardContent class="space-y-5 pt-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder={$_("admin.article_form.title_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input
id="slug"
bind:value={slug}
placeholder={$_("admin.article_form.slug_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
<!-- Mobile: single pane toggled; Desktop: side by side -->
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
<div class="space-y-1.5">
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea
bind:value={content}
placeholder={$_("admin.article_form.content_placeholder")}
class={`h-full min-h-96 font-mono text-sm resize-none ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
id="excerpt"
bind:value={excerpt}
placeholder={$_("admin.article_form.excerpt_placeholder")}
rows={2}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
<div
class={`rounded-lg border border-border/40 bg-muted/20 p-4 overflow-auto prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground min-h-96 ${editorTab === "write" ? "hidden sm:block" : ""}`}
>
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if}
</div>
<!-- Markdown editor with live preview -->
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<Label>{$_("admin.article_form.content")}</Label>
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</Button
>
<Button
variant="ghost"
size="sm"
class={`px-3 py-1 h-auto rounded-none transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</Button
>
</div>
</div>
<!-- Mobile: single pane toggled; Desktop: side by side -->
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
<Textarea
bind:value={content}
placeholder={$_("admin.article_form.content_placeholder")}
class={`h-full min-h-96 font-mono text-sm resize-none bg-background/50 border-primary/20 focus:border-primary ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
/>
<div
class={`rounded-lg border border-border/40 bg-muted/20 p-4 overflow-auto prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground min-h-96 ${editorTab === "write" ? "hidden sm:block" : ""}`}
>
{#if preview}
{@html preview}
{:else}
<p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if}
</div>
</div>
</div>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">
{$_("admin.common.image_uploaded")}
</p>{/if}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input
id="category"
bind:value={category}
placeholder={$_("admin.article_form.category_placeholder")}
/>
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}
<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>
{/if}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="category">{$_("admin.article_form.category")}</Label>
<Input
id="category"
bind:value={category}
placeholder={$_("admin.article_form.category_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
</div>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
</div>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</div>
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
</Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
</div>
</CardContent>
</Card>
</div>

View File

@@ -56,7 +56,7 @@
<Meta title={$_("admin.comments.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.comments.title")}</h1>
<span class="text-sm text-muted-foreground"

View File

@@ -127,7 +127,7 @@
<Meta title={$_("admin.queues.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.queues.title")}</h1>
</div>

View File

@@ -66,7 +66,7 @@
<Meta title={$_("admin.recordings.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.recordings.title")}</h1>
<span class="text-sm text-muted-foreground"

View File

@@ -87,7 +87,7 @@
<Meta title={$_("admin.users.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1>
<span class="text-sm text-muted-foreground"

View File

@@ -13,6 +13,7 @@
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Card, CardContent } from "$lib/components/ui/card";
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
import Meta from "$lib/components/meta/meta.svelte";
@@ -128,9 +129,9 @@
<Meta title={data.user.artist_name || data.user.email} description={null} />
<div class="p-3 sm:p-6 max-w-2xl">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/users" size="sm">
<Button variant="ghost" href="/admin/users" size="sm" class="shrink-0">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<div>
@@ -143,118 +144,130 @@
</div>
</div>
<div class="space-y-6">
<!-- Basic info -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="firstName">{$_("admin.user_edit.first_name")}</Label>
<Input id="firstName" bind:value={firstName} />
</div>
<div class="space-y-1.5">
<Label for="lastName">{$_("admin.user_edit.last_name")}</Label>
<Input id="lastName" bind:value={lastName} />
</div>
</div>
<div class="space-y-1.5">
<Label for="artistName">{$_("admin.user_edit.artist_name")}</Label>
<Input id="artistName" bind:value={artistName} />
</div>
<!-- Avatar -->
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.avatar")}</Label>
{#if avatarId}
<img
src={getAssetUrl(avatarId, "thumbnail")}
alt=""
class="h-20 w-20 rounded-full object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleAvatarUpload} />
</div>
<!-- Banner -->
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.banner")}</Label>
{#if bannerId}
<img
src={getAssetUrl(bannerId, "preview")}
alt=""
class="w-full h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleBannerUpload} />
</div>
<!-- Model photo (used in cards & model page, not for avatar/comments) -->
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.model_photo")}</Label>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.model_photo_hint")}</p>
{#if photoId}
<img
src={getAssetUrl(photoId, "preview")}
alt=""
class="w-full h-48 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handlePhotoUpload2} />
</div>
<!-- Admin flag -->
<label
class="flex items-center gap-3 rounded-lg border border-border/40 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors"
>
<input
type="checkbox"
bind:checked={isAdmin}
class="h-4 w-4 rounded accent-primary shrink-0"
/>
<div>
<span class="text-sm font-medium">{$_("admin.user_edit.is_admin")}</span>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.is_admin_hint")}</p>
</div>
</label>
<div class="flex gap-3">
<Button
onclick={handleSave}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
</div>
<!-- Photo gallery -->
<div class="space-y-3 pt-4 border-t border-border/40">
<Label>{$_("admin.user_edit.photos")}</Label>
{#if data.user.photos && data.user.photos.length > 0}
<div class="grid grid-cols-3 gap-2">
{#each data.user.photos as photo (photo.id)}
<div class="relative group">
<img
src={getAssetUrl(photo.id, "thumbnail")}
alt=""
class="w-full aspect-square object-cover rounded"
/>
<Button
variant="ghost"
class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded h-auto p-0"
onclick={() => removePhoto(photo.id)}
aria-label="Remove photo"
>
<span class="icon-[ri--delete-bin-line] h-5 w-5 text-white"></span>
</Button>
</div>
{/each}
<div class="space-y-6 max-w-2xl">
<!-- Profile & files card -->
<Card class="bg-card/50 border-primary/20">
<CardContent class="space-y-5 pt-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="firstName">{$_("admin.user_edit.first_name")}</Label>
<Input
id="firstName"
bind:value={firstName}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="lastName">{$_("admin.user_edit.last_name")}</Label>
<Input
id="lastName"
bind:value={lastName}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
{:else}
<p class="text-sm text-muted-foreground">{$_("admin.user_edit.no_photos")}</p>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handlePhotoUpload} />
</div>
<div class="space-y-1.5">
<Label for="artistName">{$_("admin.user_edit.artist_name")}</Label>
<Input
id="artistName"
bind:value={artistName}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.avatar")}</Label>
{#if avatarId}
<img
src={getAssetUrl(avatarId, "thumbnail")}
alt=""
class="h-20 w-20 rounded-full object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleAvatarUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.banner")}</Label>
{#if bannerId}
<img
src={getAssetUrl(bannerId, "preview")}
alt=""
class="w-full h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleBannerUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.user_edit.model_photo")}</Label>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.model_photo_hint")}</p>
{#if photoId}
<img
src={getAssetUrl(photoId, "preview")}
alt=""
class="w-full h-48 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handlePhotoUpload2} />
</div>
<label
class="flex items-center gap-3 rounded-lg border border-border/40 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors"
>
<input
type="checkbox"
bind:checked={isAdmin}
class="h-4 w-4 rounded accent-primary shrink-0"
/>
<div>
<span class="text-sm font-medium">{$_("admin.user_edit.is_admin")}</span>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.is_admin_hint")}</p>
</div>
</label>
<Button
onclick={handleSave}
disabled={saving}
class="cursor-pointer w-full bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
</CardContent>
</Card>
<!-- Photo gallery card -->
<Card class="bg-card/50 border-primary/20">
<CardContent class="space-y-4 pt-6">
<Label>{$_("admin.user_edit.photos")}</Label>
{#if data.user.photos && data.user.photos.length > 0}
<div class="grid grid-cols-3 gap-2">
{#each data.user.photos as photo (photo.id)}
<div class="relative group">
<img
src={getAssetUrl(photo.id, "thumbnail")}
alt=""
class="w-full aspect-square object-cover rounded"
/>
<Button
variant="ghost"
class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded h-auto p-0"
onclick={() => removePhoto(photo.id)}
aria-label="Remove photo"
>
<span class="icon-[ri--delete-bin-line] h-5 w-5 text-white"></span>
</Button>
</div>
{/each}
</div>
{:else}
<p class="text-sm text-muted-foreground">{$_("admin.user_edit.no_photos")}</p>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handlePhotoUpload} />
</CardContent>
</Card>
</div>
</div>

View File

@@ -64,7 +64,7 @@
<Meta title={$_("admin.videos.title")} description={null} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.videos.title")}</h1>
<div class="flex items-center gap-3">

View File

@@ -10,6 +10,7 @@
import { Textarea } from "$lib/components/ui/textarea";
import { TagsInput } from "$lib/components/ui/tags-input";
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
import { Card, CardContent } from "$lib/components/ui/card";
import { getAssetUrl } from "$lib/api";
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
import { DatePicker } from "$lib/components/ui/date-picker";
@@ -105,132 +106,141 @@
<Meta title={$_("admin.video_form.edit_title")} description={null} />
<div class="p-3 sm:p-6 max-w-2xl">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/videos" size="sm">
<Button variant="ghost" href="/admin/videos" size="sm" class="shrink-0">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">{$_("admin.video_form.edit_title")}</h1>
</div>
<div class="space-y-5">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<CardContent class="space-y-5 pt-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
placeholder={$_("admin.video_form.title_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input
id="slug"
bind:value={slug}
placeholder={$_("admin.video_form.slug_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
placeholder={$_("admin.video_form.title_placeholder")}
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea
id="description"
bind:value={description}
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
</div>
<div class="space-y-1.5">
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea
id="description"
bind:value={description}
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.video_form.video_file")}</Label>
{#if movieId}
<video
src={getAssetUrl(movieId)}
poster={imageId ? (getAssetUrl(imageId, "preview") ?? undefined) : undefined}
controls
class="w-full rounded-lg bg-black max-h-72 mb-2"
>
<track kind="captions" />
</video>
{/if}
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
{#if imageId}
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
{#if data.models.length > 0}
<div class="space-y-1.5">
<Label>{$_("admin.video_form.models")}</Label>
<Select type="multiple" bind:value={selectedModelIds}>
<SelectTrigger class="w-full bg-background/50 border-primary/20">
{#if selectedModelIds.length}
{$_("admin.video_form.models_selected", {
values: { count: selectedModelIds.length },
})}
{:else}
<span class="text-muted-foreground">{$_("admin.video_form.no_models")}</span>
{/if}
</SelectTrigger>
<SelectContent>
{#each data.models as model (model.id)}
<SelectItem value={model.id}>
{#if model.avatar}
<img
src={getAssetUrl(model.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if}
{model.artist_name}
</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
{/if}
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.video_form.video_file")}</Label>
{#if movieId}
<video
src={getAssetUrl(movieId)}
poster={imageId ? (getAssetUrl(imageId, "preview") ?? undefined) : undefined}
controls
class="w-full rounded-lg bg-black max-h-72 mb-2"
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
<track kind="captions" />
</video>
{/if}
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
{#if data.models.length > 0}
<div class="space-y-1.5">
<Label>{$_("admin.video_form.models")}</Label>
<Select type="multiple" bind:value={selectedModelIds}>
<SelectTrigger class="w-full">
{#if selectedModelIds.length}
{$_("admin.video_form.models_selected", {
values: { count: selectedModelIds.length },
})}
{:else}
<span class="text-muted-foreground">{$_("admin.video_form.no_models")}</span>
{/if}
</SelectTrigger>
<SelectContent>
{#each data.models as model (model.id)}
<SelectItem value={model.id}>
{#if model.avatar}
<img
src={getAssetUrl(model.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if}
{model.artist_name}
</SelectItem>
{/each}
</SelectContent>
</Select>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
{/if}
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
</div>
</CardContent>
</Card>
</div>

View File

@@ -10,6 +10,7 @@
import { TagsInput } from "$lib/components/ui/tags-input";
import { DatePicker } from "$lib/components/ui/date-picker";
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
import { Card, CardContent } from "$lib/components/ui/card";
import Meta from "$lib/components/meta/meta.svelte";
const { data } = $props();
@@ -100,115 +101,124 @@
<Meta title={$_("admin.video_form.new_title")} description={null} />
<div class="p-3 sm:p-6 max-w-2xl">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center gap-4 mb-6">
<Button variant="ghost" href="/admin/videos" size="sm">
<Button variant="ghost" href="/admin/videos" size="sm" class="shrink-0">
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
</Button>
<h1 class="text-2xl font-bold">{$_("admin.video_form.new_title")}</h1>
</div>
<div class="space-y-5">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder={$_("admin.video_form.title_placeholder")}
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
</div>
</div>
<div class="space-y-1.5">
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea
id="description"
bind:value={description}
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">
{$_("admin.common.image_uploaded")}
</p>{/if}
</div>
<div class="space-y-1.5">
<Label>{$_("admin.video_form.video_file")}</Label>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
{#if movieId}<p class="text-xs text-green-600 mt-1">
{$_("admin.video_form.video_uploaded")}
</p>{/if}
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
{#if data.models.length > 0}
<div class="space-y-2">
<Label>Models</Label>
<div class="flex flex-wrap gap-2">
{#each data.models as model (model.id)}
<Button
variant="ghost"
size="sm"
class={`px-3 py-1.5 h-auto rounded-full text-sm border transition-colors ${
selectedModelIds.includes(model.id)
? "border-primary bg-primary/10 text-primary"
: "border-border/40 text-muted-foreground hover:border-primary/40"
}`}
onclick={() => toggleModel(model.id)}
>
{model.artist_name || model.id}
</Button>
{/each}
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<CardContent class="space-y-5 pt-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label>
<Input
id="title"
bind:value={title}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder={$_("admin.video_form.title_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input
id="slug"
bind:value={slug}
placeholder={$_("admin.video_form.slug_placeholder")}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
</div>
{/if}
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.video_form.create")}
</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
</div>
<div class="space-y-1.5">
<Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea
id="description"
bind:value={description}
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
class="bg-background/50 border-primary/20 focus:border-primary"
/>
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}
<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>
{/if}
</div>
<div class="space-y-1.5">
<Label>{$_("admin.video_form.video_file")}</Label>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
{#if movieId}
<p class="text-xs text-green-600 mt-1">{$_("admin.video_form.video_uploaded")}</p>
{/if}
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.tags")}</Label>
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
</div>
<div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label>
<DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div>
<div class="flex gap-6">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={premium} class="rounded" />
<span class="text-sm">{$_("admin.common.premium")}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={featured} class="rounded" />
<span class="text-sm">{$_("admin.common.featured")}</span>
</label>
</div>
{#if data.models.length > 0}
<div class="space-y-2">
<Label>{$_("admin.video_form.models")}</Label>
<div class="flex flex-wrap gap-2">
{#each data.models as model (model.id)}
<Button
variant="ghost"
size="sm"
class={`px-3 py-1.5 h-auto rounded-full text-sm border transition-colors ${
selectedModelIds.includes(model.id)
? "border-primary bg-primary/10 text-primary"
: "border-border/40 text-muted-foreground hover:border-primary/40"
}`}
onclick={() => toggleModel(model.id)}
>
{model.artist_name || model.id}
</Button>
{/each}
</div>
</div>
{/if}
<div class="flex gap-3 pt-2">
<Button
onclick={handleSubmit}
disabled={saving}
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.video_form.create")}
</Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
</div>
</CardContent>
</Card>
</div>

View File

@@ -44,9 +44,10 @@
<div class="flex items-center gap-1 overflow-x-auto py-2 scrollbar-none">
<a
href="/"
class="shrink-0 text-xs text-muted-foreground hover:text-foreground transition-colors px-2"
class="shrink-0 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors px-2"
>
{$_("me.nav.back_mobile")}
<span class="icon-[ri--arrow-left-line] h-4 w-4"></span>
<span class="hidden sm:inline">{$_("me.nav.back_mobile")}</span>
</a>
{#each navLinks as link (link.href)}
<a
@@ -69,7 +70,8 @@
<!-- Sidebar (desktop only) -->
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
<div class="px-4 py-5 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors">
<a href="/" class="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
<span class="icon-[ri--arrow-left-line] h-3.5 w-3.5"></span>
{$_("me.nav.back_to_site")}
</a>
<div class="mt-3 flex items-center gap-3">

View File

@@ -14,15 +14,15 @@
<Meta title={$_("me.analytics.title")} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold">{$_("me.analytics.title")}</h1>
<p class="text-sm text-muted-foreground mt-0.5">{$_("me.analytics.description")}</p>
</div>
</div>
<div class="px-3 sm:px-0 space-y-6">
<div class="space-y-6">
{#if data.analytics}
<!-- Overview Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">

View File

@@ -2,7 +2,7 @@
import { _ } from "svelte-i18n";
import { invalidateAll } from "$app/navigation";
import { untrack } from "svelte";
import { getAssetUrl, isModel } from "$lib/api";
import { getAssetUrl } from "$lib/api";
import { toast } from "svelte-sonner";
import { updateProfile, uploadFile, removeFile } from "$lib/services";
import { Button } from "$lib/components/ui/button";
@@ -129,18 +129,12 @@
<Meta title={$_("me.settings.profile_title")} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="mb-6">
<h1 class="text-2xl font-bold">{$_("me.settings.profile_title")}</h1>
{#if isModel(data.authStatus.user!)}
<Button href={`/models/${data.authStatus.user!.slug}`} variant="outline">
{$_("me.view_profile")}
</Button>
{/if}
</div>
<div class="px-3 sm:px-0">
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<CardHeader>
<CardTitle>{$_("me.settings.profile_title")}</CardTitle>
<CardDescription>{$_("me.settings.profile_subtitle")}</CardDescription>
@@ -287,5 +281,4 @@
</form>
</CardContent>
</Card>
</div>
</div>

View File

@@ -5,7 +5,7 @@
import { toast } from "svelte-sonner";
import { deleteRecording } from "$lib/services";
import { Button } from "$lib/components/ui/button";
import { Card, CardContent } from "$lib/components/ui/card";
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";
@@ -45,8 +45,8 @@
<Meta title={$_("me.recordings.title")} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">{$_("me.recordings.title")}</h1>
<Button
href="/play"
@@ -57,42 +57,36 @@
</Button>
</div>
<div class="px-3 sm:px-0">
{#if recordings.length === 0}
<Card class="bg-card/50 border-primary/20">
<CardContent class="py-12">
<div class="flex flex-col items-center justify-center text-center">
<div class="mb-4 p-4 rounded-full bg-muted/30 border border-border/30">
<span class="icon-[ri--play-list-2-line] w-12 h-12 text-muted-foreground"></span>
</div>
<h3 class="text-xl font-semibold mb-2">
{$_("me.recordings.no_recordings")}
</h3>
<p class="text-muted-foreground mb-6 max-w-md">
{$_("me.recordings.no_recordings_description")}
</p>
<Button
href="/play"
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>
</div>
</CardContent>
</Card>
{: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}
onDelete={handleDeleteRecording}
/>
{/each}
</div>
{/if}
</div>
{#if recordings.length === 0}
<Empty.Root>
<Empty.Header>
<Empty.Media variant="icon">
<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"
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}
onDelete={handleDeleteRecording}
/>
{/each}
</div>
{/if}
</div>
<Dialog.Root bind:open={deleteOpen}>

View File

@@ -57,13 +57,12 @@
<Meta title={$_("me.settings.privacy_title")} />
<div class="py-3 sm:py-6 sm:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<div class="py-3 sm:py-6 lg:pl-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">{$_("me.settings.privacy_title")}</h1>
</div>
<div class="px-3 sm:px-0">
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<Card class="bg-card/50 border-primary/20 max-w-2xl">
<CardHeader>
<CardTitle>{$_("me.settings.privacy_title")}</CardTitle>
<CardDescription>{$_("me.settings.privacy_subtitle")}</CardDescription>
@@ -145,9 +144,8 @@
{/if}
<Button
variant="outline"
type="submit"
class="cursor-pointer w-full border-primary/20 hover:bg-primary/10"
class="cursor-pointer w-full bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
disabled={isSecurityLoading}
>
{#if isSecurityLoading}
@@ -162,5 +160,4 @@
</form>
</CardContent>
</Card>
</div>
</div>