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:
@@ -38,7 +38,7 @@
|
|||||||
isMobileMenuOpen = false;
|
isMobileMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveLink(link: { name: string; href: string }) {
|
function isActiveLink(link: { name?: string; href: string }) {
|
||||||
return (
|
return (
|
||||||
(page.url.pathname === "/" && link === navLinks[0]) ||
|
(page.url.pathname === "/" && link === navLinks[0]) ||
|
||||||
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
||||||
@@ -80,20 +80,6 @@
|
|||||||
{#if authStatus.authenticated}
|
{#if authStatus.authenticated}
|
||||||
<div class="w-full flex items-center justify-end">
|
<div class="w-full flex items-center justify-end">
|
||||||
<div class="flex items-center gap-2 rounded-full bg-muted/30 p-1">
|
<div class="flex items-center gap-2 rounded-full bg-muted/30 p-1">
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="icon"
|
|
||||||
class={`flex h-9 w-9 rounded-full p-0 relative text-foreground/80 group ${isActiveLink({ href: "/me" }) ? "text-foreground" : "hover:text-foreground"}`}
|
|
||||||
href="/me"
|
|
||||||
title={$_("header.dashboard")}
|
|
||||||
>
|
|
||||||
<span class="icon-[ri--dashboard-2-line] h-4 w-4"></span>
|
|
||||||
<span
|
|
||||||
class={`absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent transition-all duration-300 ${isActiveLink({ href: "/me" }) ? "w-full" : "group-hover:w-full"}`}
|
|
||||||
></span>
|
|
||||||
<span class="sr-only">{$_("header.dashboard")}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty-content"
|
||||||
|
class={cn(
|
||||||
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty-header"
|
||||||
|
class={cn("flex max-w-sm flex-col items-center gap-2 text-center", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const emptyMediaVariants = tv({
|
||||||
|
base: "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EmptyMediaVariant = VariantProps<typeof emptyMediaVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
variant = "default",
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty-icon"
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(emptyMediaVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty-title"
|
||||||
|
class={cn("text-lg font-medium tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
packages/frontend/src/lib/components/ui/empty/empty.svelte
Normal file
23
packages/frontend/src/lib/components/ui/empty/empty.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="empty"
|
||||||
|
class={cn(
|
||||||
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
22
packages/frontend/src/lib/components/ui/empty/index.ts
Normal file
22
packages/frontend/src/lib/components/ui/empty/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Root from "./empty.svelte";
|
||||||
|
import Header from "./empty-header.svelte";
|
||||||
|
import Media from "./empty-media.svelte";
|
||||||
|
import Title from "./empty-title.svelte";
|
||||||
|
import Description from "./empty-description.svelte";
|
||||||
|
import Content from "./empty-content.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Header,
|
||||||
|
Media,
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
Content,
|
||||||
|
//
|
||||||
|
Root as Empty,
|
||||||
|
Header as EmptyHeader,
|
||||||
|
Media as EmptyMedia,
|
||||||
|
Title as EmptyTitle,
|
||||||
|
Description as EmptyDescription,
|
||||||
|
Content as EmptyContent,
|
||||||
|
};
|
||||||
@@ -96,8 +96,8 @@ export default {
|
|||||||
security: "Security",
|
security: "Security",
|
||||||
recordings: "Recordings",
|
recordings: "Recordings",
|
||||||
analytics: "Analytics",
|
analytics: "Analytics",
|
||||||
back_to_site: "← Back to site",
|
back_to_site: "Back to site",
|
||||||
back_mobile: "← Back",
|
back_mobile: "Back",
|
||||||
},
|
},
|
||||||
analytics: {
|
analytics: {
|
||||||
title: "Analytics",
|
title: "Analytics",
|
||||||
@@ -922,8 +922,8 @@ export default {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
nav: {
|
nav: {
|
||||||
back_to_site: "← Back to site",
|
back_to_site: "Back to site",
|
||||||
back_mobile: "← Back",
|
back_mobile: "Back",
|
||||||
title: "Admin",
|
title: "Admin",
|
||||||
users: "Users",
|
users: "Users",
|
||||||
videos: "Videos",
|
videos: "Videos",
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import { _ } from "svelte-i18n";
|
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([
|
const navLinks = $derived([
|
||||||
{ name: $_("admin.nav.users"), href: "/admin/users", icon: "icon-[ri--team-line]" },
|
{ 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">
|
<div class="flex items-center gap-1 overflow-x-auto py-2 scrollbar-none">
|
||||||
<a
|
<a
|
||||||
href="/"
|
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>
|
</a>
|
||||||
{#each navLinks as link (link.href)}
|
{#each navLinks as link (link.href)}
|
||||||
<a
|
<a
|
||||||
@@ -58,10 +68,27 @@
|
|||||||
<!-- Sidebar (desktop only) -->
|
<!-- Sidebar (desktop only) -->
|
||||||
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
|
<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">
|
<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")}
|
{$_("admin.nav.back_to_site")}
|
||||||
</a>
|
</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>
|
</div>
|
||||||
|
|
||||||
<nav class="flex-1 p-3 space-y-1">
|
<nav class="flex-1 p-3 space-y-1">
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.articles.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.articles.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.articles.title")}</h1>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { Textarea } from "$lib/components/ui/textarea";
|
import { Textarea } from "$lib/components/ui/textarea";
|
||||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||||
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
||||||
|
import { Card, CardContent } from "$lib/components/ui/card";
|
||||||
import { getAssetUrl } from "$lib/api";
|
import { getAssetUrl } from "$lib/api";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
|
||||||
import { DatePicker } from "$lib/components/ui/date-picker";
|
import { DatePicker } from "$lib/components/ui/date-picker";
|
||||||
@@ -96,147 +97,165 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.article_form.edit_title")} description={null} />
|
<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">
|
<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")}
|
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.article_form.edit_title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.article_form.edit_title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5 max-w-4xl">
|
<Card class="bg-card/50 border-primary/20 max-w-4xl">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<CardContent class="space-y-5 pt-6">
|
||||||
<div class="space-y-1.5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Label for="title">{$_("admin.common.title_field")}</Label>
|
<div class="space-y-1.5">
|
||||||
<Input id="title" bind:value={title} />
|
<Label for="title">{$_("admin.common.title_field")}</Label>
|
||||||
</div>
|
<Input
|
||||||
<div class="space-y-1.5">
|
id="title"
|
||||||
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
bind:value={title}
|
||||||
<Input id="slug" bind:value={slug} />
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
<div class="space-y-1.5">
|
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
||||||
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
|
<Input
|
||||||
<Textarea id="excerpt" bind:value={excerpt} rows={2} />
|
id="slug"
|
||||||
</div>
|
bind:value={slug}
|
||||||
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
<!-- 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>
|
</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
|
<Textarea
|
||||||
bind:value={content}
|
id="excerpt"
|
||||||
class={`h-full min-h-96 font-mono text-sm resize-none ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
|
bind:value={excerpt}
|
||||||
|
rows={2}
|
||||||
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
/>
|
/>
|
||||||
<div
|
</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" : ""}`}
|
|
||||||
>
|
<!-- Markdown editor with live preview -->
|
||||||
{#if preview}
|
<div class="space-y-1.5">
|
||||||
{@html preview}
|
<div class="flex items-center justify-between">
|
||||||
{:else}
|
<Label>{$_("admin.article_form.content")}</Label>
|
||||||
<p class="text-muted-foreground italic text-sm">
|
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
|
||||||
{$_("admin.article_form.preview_placeholder")}
|
<Button
|
||||||
</p>
|
variant="ghost"
|
||||||
{/if}
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label>{$_("admin.common.cover_image")}</Label>
|
<Label>{$_("admin.common.cover_image")}</Label>
|
||||||
{#if imageId}
|
{#if imageId}
|
||||||
<img
|
<img
|
||||||
src={getAssetUrl(imageId, "thumbnail")}
|
src={getAssetUrl(imageId, "thumbnail")}
|
||||||
alt=""
|
alt=""
|
||||||
class="h-24 rounded object-cover mb-2"
|
class="h-24 rounded object-cover mb-2"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
|
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Author -->
|
<div class="space-y-1.5">
|
||||||
<div class="space-y-1.5">
|
<Label>{$_("admin.article_form.author")}</Label>
|
||||||
<Label>{$_("admin.article_form.author")}</Label>
|
<Select type="single" bind:value={authorId}>
|
||||||
<Select type="single" bind:value={authorId}>
|
<SelectTrigger class="w-full bg-background/50 border-primary/20">
|
||||||
<SelectTrigger class="w-full">
|
{#if selectedAuthor}
|
||||||
{#if selectedAuthor}
|
{#if selectedAuthor.avatar}
|
||||||
{#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}
|
|
||||||
<img
|
<img
|
||||||
src={getAssetUrl(author.avatar, "mini")}
|
src={getAssetUrl(selectedAuthor.avatar, "mini")}
|
||||||
alt=""
|
alt=""
|
||||||
class="h-5 w-5 rounded-full object-cover shrink-0"
|
class="h-5 w-5 rounded-full object-cover shrink-0"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{author.artist_name}
|
{selectedAuthor.artist_name}
|
||||||
</SelectItem>
|
{:else}
|
||||||
{/each}
|
<span class="text-muted-foreground">{$_("admin.article_form.no_author")}</span>
|
||||||
</SelectContent>
|
{/if}
|
||||||
</Select>
|
</SelectTrigger>
|
||||||
</div>
|
<SelectContent>
|
||||||
|
<SelectItem value="">{$_("admin.article_form.no_author")}</SelectItem>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
{#each data.authors as author (author.id)}
|
||||||
<div class="space-y-1.5">
|
<SelectItem value={author.id}>
|
||||||
<Label for="category">{$_("admin.article_form.category")}</Label>
|
{#if author.avatar}
|
||||||
<Input id="category" bind:value={category} />
|
<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>
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label>{$_("admin.common.publish_date")}</Label>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
|
<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>
|
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label>{$_("admin.common.tags")}</Label>
|
<Label>{$_("admin.common.tags")}</Label>
|
||||||
<TagsInput bind:value={tags} />
|
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
<input type="checkbox" bind:checked={featured} class="rounded" />
|
<input type="checkbox" bind:checked={featured} class="rounded" />
|
||||||
<span class="text-sm">{$_("admin.common.featured")}</span>
|
<span class="text-sm">{$_("admin.common.featured")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex gap-3 pt-2">
|
<div class="flex gap-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
onclick={handleSubmit}
|
onclick={handleSubmit}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
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")}
|
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
|
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||||
import { DatePicker } from "$lib/components/ui/date-picker";
|
import { DatePicker } from "$lib/components/ui/date-picker";
|
||||||
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
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";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|
||||||
let title = $state("");
|
let title = $state("");
|
||||||
@@ -78,129 +79,135 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.article_form.new_title")} description={null} />
|
<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">
|
<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")}
|
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.article_form.new_title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.article_form.new_title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5 max-w-4xl">
|
<Card class="bg-card/50 border-primary/20 max-w-4xl">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<CardContent class="space-y-5 pt-6">
|
||||||
<div class="space-y-1.5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Label for="title">{$_("admin.common.title_field")}</Label>
|
<div class="space-y-1.5">
|
||||||
<Input
|
<Label for="title">{$_("admin.common.title_field")}</Label>
|
||||||
id="title"
|
<Input
|
||||||
bind:value={title}
|
id="title"
|
||||||
oninput={() => {
|
bind:value={title}
|
||||||
if (!slug) slug = generateSlug(title);
|
oninput={() => {
|
||||||
}}
|
if (!slug) slug = generateSlug(title);
|
||||||
placeholder={$_("admin.article_form.title_placeholder")}
|
}}
|
||||||
/>
|
placeholder={$_("admin.article_form.title_placeholder")}
|
||||||
</div>
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
<div class="space-y-1.5">
|
/>
|
||||||
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
</div>
|
||||||
<Input
|
<div class="space-y-1.5">
|
||||||
id="slug"
|
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
||||||
bind:value={slug}
|
<Input
|
||||||
placeholder={$_("admin.article_form.slug_placeholder")}
|
id="slug"
|
||||||
/>
|
bind:value={slug}
|
||||||
</div>
|
placeholder={$_("admin.article_form.slug_placeholder")}
|
||||||
</div>
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
|
/>
|
||||||
<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
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</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
|
<Textarea
|
||||||
bind:value={content}
|
id="excerpt"
|
||||||
placeholder={$_("admin.article_form.content_placeholder")}
|
bind:value={excerpt}
|
||||||
class={`h-full min-h-96 font-mono text-sm resize-none ${editorTab === "preview" ? "hidden sm:flex" : ""}`}
|
placeholder={$_("admin.article_form.excerpt_placeholder")}
|
||||||
|
rows={2}
|
||||||
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
/>
|
/>
|
||||||
<div
|
</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" : ""}`}
|
|
||||||
>
|
<!-- Markdown editor with live preview -->
|
||||||
{#if preview}
|
<div class="space-y-1.5">
|
||||||
{@html preview}
|
<div class="flex items-center justify-between">
|
||||||
{:else}
|
<Label>{$_("admin.article_form.content")}</Label>
|
||||||
<p class="text-muted-foreground italic text-sm">
|
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
|
||||||
{$_("admin.article_form.preview_placeholder")}
|
<Button
|
||||||
</p>
|
variant="ghost"
|
||||||
{/if}
|
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>
|
||||||
</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">
|
<div class="space-y-1.5">
|
||||||
<Label for="category">{$_("admin.article_form.category")}</Label>
|
<Label>{$_("admin.common.cover_image")}</Label>
|
||||||
<Input
|
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
|
||||||
id="category"
|
{#if imageId}
|
||||||
bind:value={category}
|
<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")} ✓</p>
|
||||||
placeholder={$_("admin.article_form.category_placeholder")}
|
{/if}
|
||||||
/>
|
|
||||||
</div>
|
</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">
|
<div class="space-y-1.5">
|
||||||
<Label>{$_("admin.common.publish_date")}</Label>
|
<Label>{$_("admin.common.tags")}</Label>
|
||||||
<DatePicker bind:value={publishDate} placeholder={$_("admin.common.publish_date")} />
|
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
<Label>{$_("admin.common.tags")}</Label>
|
<input type="checkbox" bind:checked={featured} class="rounded" />
|
||||||
<TagsInput bind:value={tags} />
|
<span class="text-sm">{$_("admin.common.featured")}</span>
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<div class="flex gap-3 pt-2">
|
||||||
<input type="checkbox" bind:checked={featured} class="rounded" />
|
<Button
|
||||||
<span class="text-sm">{$_("admin.common.featured")}</span>
|
onclick={handleSubmit}
|
||||||
</label>
|
disabled={saving}
|
||||||
|
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||||
<div class="flex gap-3 pt-2">
|
>
|
||||||
<Button
|
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
|
||||||
onclick={handleSubmit}
|
</Button>
|
||||||
disabled={saving}
|
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
|
||||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
</div>
|
||||||
>
|
</CardContent>
|
||||||
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
|
</Card>
|
||||||
</Button>
|
|
||||||
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.comments.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.comments.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.comments.title")}</h1>
|
||||||
<span class="text-sm text-muted-foreground"
|
<span class="text-sm text-muted-foreground"
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.queues.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.queues.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.queues.title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.recordings.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.recordings.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.recordings.title")}</h1>
|
||||||
<span class="text-sm text-muted-foreground"
|
<span class="text-sm text-muted-foreground"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.users.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1>
|
||||||
<span class="text-sm text-muted-foreground"
|
<span class="text-sm text-muted-foreground"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { Label } from "$lib/components/ui/label";
|
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 { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|
||||||
@@ -128,9 +129,9 @@
|
|||||||
|
|
||||||
<Meta title={data.user.artist_name || data.user.email} description={null} />
|
<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">
|
<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")}
|
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
@@ -143,118 +144,130 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6 max-w-2xl">
|
||||||
<!-- Basic info -->
|
<!-- Profile & files card -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<Card class="bg-card/50 border-primary/20">
|
||||||
<div class="space-y-1.5">
|
<CardContent class="space-y-5 pt-6">
|
||||||
<Label for="firstName">{$_("admin.user_edit.first_name")}</Label>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input id="firstName" bind:value={firstName} />
|
<div class="space-y-1.5">
|
||||||
</div>
|
<Label for="firstName">{$_("admin.user_edit.first_name")}</Label>
|
||||||
<div class="space-y-1.5">
|
<Input
|
||||||
<Label for="lastName">{$_("admin.user_edit.last_name")}</Label>
|
id="firstName"
|
||||||
<Input id="lastName" bind:value={lastName} />
|
bind:value={firstName}
|
||||||
</div>
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="artistName">{$_("admin.user_edit.artist_name")}</Label>
|
<Label for="lastName">{$_("admin.user_edit.last_name")}</Label>
|
||||||
<Input id="artistName" bind:value={artistName} />
|
<Input
|
||||||
</div>
|
id="lastName"
|
||||||
|
bind:value={lastName}
|
||||||
<!-- Avatar -->
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
<div class="space-y-1.5">
|
/>
|
||||||
<Label>{$_("admin.user_edit.avatar")}</Label>
|
</div>
|
||||||
{#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>
|
</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 class="space-y-1.5">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.videos.title")} description={null} />
|
<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">
|
<div class="flex items-center justify-between mb-6 px-3 sm:px-0">
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.videos.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.videos.title")}</h1>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { Textarea } from "$lib/components/ui/textarea";
|
import { Textarea } from "$lib/components/ui/textarea";
|
||||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||||
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
||||||
|
import { Card, CardContent } from "$lib/components/ui/card";
|
||||||
import { getAssetUrl } from "$lib/api";
|
import { getAssetUrl } from "$lib/api";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
|
||||||
import { DatePicker } from "$lib/components/ui/date-picker";
|
import { DatePicker } from "$lib/components/ui/date-picker";
|
||||||
@@ -105,132 +106,141 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.video_form.edit_title")} description={null} />
|
<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">
|
<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")}
|
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.video_form.edit_title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.video_form.edit_title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<Card class="bg-card/50 border-primary/20 max-w-2xl">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<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">
|
<div class="space-y-1.5">
|
||||||
<Label for="title">{$_("admin.common.title_field")}</Label>
|
<Label for="description">{$_("admin.video_form.description")}</Label>
|
||||||
<Input
|
<Textarea
|
||||||
id="title"
|
id="description"
|
||||||
bind:value={title}
|
bind:value={description}
|
||||||
placeholder={$_("admin.video_form.title_placeholder")}
|
placeholder={$_("admin.video_form.description_placeholder")}
|
||||||
|
rows={3}
|
||||||
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
<Label>{$_("admin.common.cover_image")}</Label>
|
||||||
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
|
{#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>
|
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="description">{$_("admin.video_form.description")}</Label>
|
<Label>{$_("admin.video_form.video_file")}</Label>
|
||||||
<Textarea
|
{#if movieId}
|
||||||
id="description"
|
<video
|
||||||
bind:value={description}
|
src={getAssetUrl(movieId)}
|
||||||
placeholder={$_("admin.video_form.description_placeholder")}
|
poster={imageId ? (getAssetUrl(imageId, "preview") ?? undefined) : undefined}
|
||||||
rows={3}
|
controls
|
||||||
/>
|
class="w-full rounded-lg bg-black max-h-72 mb-2"
|
||||||
</div>
|
>
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
|
{/if}
|
||||||
|
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label>{$_("admin.common.cover_image")}</Label>
|
<Label>{$_("admin.common.tags")}</Label>
|
||||||
{#if imageId}
|
<TagsInput bind:value={tags} class="bg-background/50 border-primary/20 focus:border-primary" />
|
||||||
<img
|
</div>
|
||||||
src={getAssetUrl(imageId, "thumbnail")}
|
|
||||||
alt=""
|
<div class="space-y-1.5">
|
||||||
class="h-24 rounded object-cover mb-2"
|
<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}
|
{/if}
|
||||||
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="flex gap-3 pt-2">
|
||||||
<Label>{$_("admin.video_form.video_file")}</Label>
|
<Button
|
||||||
{#if movieId}
|
onclick={handleSubmit}
|
||||||
<video
|
disabled={saving}
|
||||||
src={getAssetUrl(movieId)}
|
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||||
poster={imageId ? (getAssetUrl(imageId, "preview") ?? undefined) : undefined}
|
|
||||||
controls
|
|
||||||
class="w-full rounded-lg bg-black max-h-72 mb-2"
|
|
||||||
>
|
>
|
||||||
<track kind="captions" />
|
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
|
||||||
</video>
|
</Button>
|
||||||
{/if}
|
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||||
import { DatePicker } from "$lib/components/ui/date-picker";
|
import { DatePicker } from "$lib/components/ui/date-picker";
|
||||||
import { FileDropZone, MEGABYTE } from "$lib/components/ui/file-drop-zone";
|
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";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
@@ -100,115 +101,124 @@
|
|||||||
|
|
||||||
<Meta title={$_("admin.video_form.new_title")} description={null} />
|
<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">
|
<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")}
|
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>{$_("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-2xl font-bold">{$_("admin.video_form.new_title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("admin.video_form.new_title")}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<Card class="bg-card/50 border-primary/20 max-w-2xl">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<CardContent class="space-y-5 pt-6">
|
||||||
<div class="space-y-1.5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Label for="title">{$_("admin.common.title_field")}</Label>
|
<div class="space-y-1.5">
|
||||||
<Input
|
<Label for="title">{$_("admin.common.title_field")}</Label>
|
||||||
id="title"
|
<Input
|
||||||
bind:value={title}
|
id="title"
|
||||||
oninput={() => {
|
bind:value={title}
|
||||||
if (!slug) slug = generateSlug(title);
|
oninput={() => {
|
||||||
}}
|
if (!slug) slug = generateSlug(title);
|
||||||
placeholder={$_("admin.video_form.title_placeholder")}
|
}}
|
||||||
/>
|
placeholder={$_("admin.video_form.title_placeholder")}
|
||||||
</div>
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
<div class="space-y-1.5">
|
/>
|
||||||
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
</div>
|
||||||
<Input id="slug" bind:value={slug} placeholder={$_("admin.video_form.slug_placeholder")} />
|
<div class="space-y-1.5">
|
||||||
</div>
|
<Label for="slug">{$_("admin.common.slug_field")}</Label>
|
||||||
</div>
|
<Input
|
||||||
|
id="slug"
|
||||||
<div class="space-y-1.5">
|
bind:value={slug}
|
||||||
<Label for="description">{$_("admin.video_form.description")}</Label>
|
placeholder={$_("admin.video_form.slug_placeholder")}
|
||||||
<Textarea
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex gap-3 pt-2">
|
<div class="space-y-1.5">
|
||||||
<Button
|
<Label for="description">{$_("admin.video_form.description")}</Label>
|
||||||
onclick={handleSubmit}
|
<Textarea
|
||||||
disabled={saving}
|
id="description"
|
||||||
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
bind:value={description}
|
||||||
>
|
placeholder={$_("admin.video_form.description_placeholder")}
|
||||||
{saving ? $_("admin.common.creating") : $_("admin.video_form.create")}
|
rows={3}
|
||||||
</Button>
|
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||||
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>
|
/>
|
||||||
</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="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>
|
</div>
|
||||||
|
|||||||
@@ -44,9 +44,10 @@
|
|||||||
<div class="flex items-center gap-1 overflow-x-auto py-2 scrollbar-none">
|
<div class="flex items-center gap-1 overflow-x-auto py-2 scrollbar-none">
|
||||||
<a
|
<a
|
||||||
href="/"
|
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>
|
</a>
|
||||||
{#each navLinks as link (link.href)}
|
{#each navLinks as link (link.href)}
|
||||||
<a
|
<a
|
||||||
@@ -69,7 +70,8 @@
|
|||||||
<!-- Sidebar (desktop only) -->
|
<!-- Sidebar (desktop only) -->
|
||||||
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
|
<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">
|
<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")}
|
{$_("me.nav.back_to_site")}
|
||||||
</a>
|
</a>
|
||||||
<div class="mt-3 flex items-center gap-3">
|
<div class="mt-3 flex items-center gap-3">
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
|
|
||||||
<Meta title={$_("me.analytics.title")} />
|
<Meta title={$_("me.analytics.title")} />
|
||||||
|
|
||||||
<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">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">{$_("me.analytics.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("me.analytics.title")}</h1>
|
||||||
<p class="text-sm text-muted-foreground mt-0.5">{$_("me.analytics.description")}</p>
|
<p class="text-sm text-muted-foreground mt-0.5">{$_("me.analytics.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 sm:px-0 space-y-6">
|
<div class="space-y-6">
|
||||||
{#if data.analytics}
|
{#if data.analytics}
|
||||||
<!-- Overview Stats -->
|
<!-- Overview Stats -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { invalidateAll } from "$app/navigation";
|
import { invalidateAll } from "$app/navigation";
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import { getAssetUrl, isModel } from "$lib/api";
|
import { getAssetUrl } from "$lib/api";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { updateProfile, uploadFile, removeFile } from "$lib/services";
|
import { updateProfile, uploadFile, removeFile } from "$lib/services";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
@@ -129,18 +129,12 @@
|
|||||||
|
|
||||||
<Meta title={$_("me.settings.profile_title")} />
|
<Meta title={$_("me.settings.profile_title")} />
|
||||||
|
|
||||||
<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">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-bold">{$_("me.settings.profile_title")}</h1>
|
<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>
|
||||||
|
|
||||||
<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>
|
<CardHeader>
|
||||||
<CardTitle>{$_("me.settings.profile_title")}</CardTitle>
|
<CardTitle>{$_("me.settings.profile_title")}</CardTitle>
|
||||||
<CardDescription>{$_("me.settings.profile_subtitle")}</CardDescription>
|
<CardDescription>{$_("me.settings.profile_subtitle")}</CardDescription>
|
||||||
@@ -287,5 +281,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { deleteRecording } from "$lib/services";
|
import { deleteRecording } from "$lib/services";
|
||||||
import { Button } from "$lib/components/ui/button";
|
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 * as Dialog from "$lib/components/ui/dialog";
|
||||||
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
|
import RecordingCard from "$lib/components/recording-card/recording-card.svelte";
|
||||||
import Meta from "$lib/components/meta/meta.svelte";
|
import Meta from "$lib/components/meta/meta.svelte";
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
|
|
||||||
<Meta title={$_("me.recordings.title")} />
|
<Meta title={$_("me.recordings.title")} />
|
||||||
|
|
||||||
<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">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold">{$_("me.recordings.title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("me.recordings.title")}</h1>
|
||||||
<Button
|
<Button
|
||||||
href="/play"
|
href="/play"
|
||||||
@@ -57,42 +57,36 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 sm:px-0">
|
{#if recordings.length === 0}
|
||||||
{#if recordings.length === 0}
|
<Empty.Root>
|
||||||
<Card class="bg-card/50 border-primary/20">
|
<Empty.Header>
|
||||||
<CardContent class="py-12">
|
<Empty.Media variant="icon">
|
||||||
<div class="flex flex-col items-center justify-center text-center">
|
<span class="icon-[ri--play-list-2-line] w-8 h-8"></span>
|
||||||
<div class="mb-4 p-4 rounded-full bg-muted/30 border border-border/30">
|
</Empty.Media>
|
||||||
<span class="icon-[ri--play-list-2-line] w-12 h-12 text-muted-foreground"></span>
|
<Empty.Title>{$_("me.recordings.no_recordings")}</Empty.Title>
|
||||||
</div>
|
<Empty.Description>{$_("me.recordings.no_recordings_description")}</Empty.Description>
|
||||||
<h3 class="text-xl font-semibold mb-2">
|
</Empty.Header>
|
||||||
{$_("me.recordings.no_recordings")}
|
<Empty.Content>
|
||||||
</h3>
|
<Button
|
||||||
<p class="text-muted-foreground mb-6 max-w-md">
|
href="/play"
|
||||||
{$_("me.recordings.no_recordings_description")}
|
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||||
</p>
|
>
|
||||||
<Button
|
<span class="icon-[ri--rocket-line] w-4 h-4 mr-2"></span>
|
||||||
href="/play"
|
{$_("me.recordings.go_to_play")}
|
||||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
</Button>
|
||||||
>
|
</Empty.Content>
|
||||||
<span class="icon-[ri--rocket-line] w-4 h-4 mr-2"></span>
|
</Empty.Root>
|
||||||
{$_("me.recordings.go_to_play")}
|
{:else}
|
||||||
</Button>
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
</div>
|
{#each recordings as recording (recording.id)}
|
||||||
</CardContent>
|
<RecordingCard
|
||||||
</Card>
|
{recording}
|
||||||
{:else}
|
onPlay={handlePlayRecording}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
onDelete={handleDeleteRecording}
|
||||||
{#each recordings as recording (recording.id)}
|
/>
|
||||||
<RecordingCard
|
{/each}
|
||||||
{recording}
|
</div>
|
||||||
onPlay={handlePlayRecording}
|
{/if}
|
||||||
onDelete={handleDeleteRecording}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog.Root bind:open={deleteOpen}>
|
<Dialog.Root bind:open={deleteOpen}>
|
||||||
|
|||||||
@@ -57,13 +57,12 @@
|
|||||||
|
|
||||||
<Meta title={$_("me.settings.privacy_title")} />
|
<Meta title={$_("me.settings.privacy_title")} />
|
||||||
|
|
||||||
<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">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold">{$_("me.settings.privacy_title")}</h1>
|
<h1 class="text-2xl font-bold">{$_("me.settings.privacy_title")}</h1>
|
||||||
</div>
|
</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>
|
<CardHeader>
|
||||||
<CardTitle>{$_("me.settings.privacy_title")}</CardTitle>
|
<CardTitle>{$_("me.settings.privacy_title")}</CardTitle>
|
||||||
<CardDescription>{$_("me.settings.privacy_subtitle")}</CardDescription>
|
<CardDescription>{$_("me.settings.privacy_subtitle")}</CardDescription>
|
||||||
@@ -145,9 +144,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
type="submit"
|
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}
|
disabled={isSecurityLoading}
|
||||||
>
|
>
|
||||||
{#if isSecurityLoading}
|
{#if isSecurityLoading}
|
||||||
@@ -162,5 +160,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user