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

@@ -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>