feat: mobile-optimize admin section
- Layout: sidebar hidden on mobile, replaced with horizontal top nav strip - Tables: overflow-x-auto + hide secondary columns (email/category/dates/ plays/likes) on small screens; show email inline under name on mobile - Forms: grid-cols-2 → grid-cols-1 sm:grid-cols-2 on all admin forms - Markdown editor: Write/Preview tab toggle on mobile, side-by-side on sm+ - Padding: p-3 sm:p-6 on all admin pages for tighter mobile layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,9 +16,31 @@
|
||||
|
||||
<div class="min-h-screen bg-background">
|
||||
<div class="container mx-auto px-4">
|
||||
|
||||
<!-- Mobile top nav -->
|
||||
<div class="lg:hidden flex items-center gap-2 py-3 border-b border-border/40">
|
||||
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors shrink-0 mr-2">
|
||||
← Back
|
||||
</a>
|
||||
{#each navLinks as link (link.href)}
|
||||
<a
|
||||
href={link.href}
|
||||
class={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
isActive(link.href)
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<span class={`${link.icon} h-4 w-4`}></span>
|
||||
{link.name}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Desktop layout -->
|
||||
<div class="flex min-h-screen">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-56 shrink-0 flex flex-col">
|
||||
<!-- Sidebar (desktop only) -->
|
||||
<aside class="hidden lg:flex w-56 shrink-0 flex-col border-r border-border/40">
|
||||
<div class="px-4 py-5 border-b border-border/40">
|
||||
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
||||
← Back to site
|
||||
@@ -44,7 +66,7 @@
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="flex-1 overflow-auto">
|
||||
<main class="flex-1 min-w-0">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-3 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Articles</h1>
|
||||
<Button href="/admin/articles/new">
|
||||
@@ -46,13 +46,13 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-border/40 overflow-hidden">
|
||||
<div class="rounded-lg border border-border/40 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/30">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Article</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Category</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Published</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Category</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Published</th>
|
||||
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -85,8 +85,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground capitalize">{article.category ?? "—"}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
<td class="px-4 py-3 text-muted-foreground capitalize hidden sm:table-cell">{article.category ?? "—"}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground hidden sm:table-cell">
|
||||
{timeAgo.format(new Date(article.publish_date))}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
);
|
||||
let imageId = $state<string | null>(data.article.image ?? null);
|
||||
let saving = $state(false);
|
||||
let editorTab = $state<"write" | "preview">("write");
|
||||
|
||||
let preview = $derived(content ? (marked.parse(content) as string) : "");
|
||||
|
||||
@@ -67,7 +68,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-3 sm:p-6">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" href="/admin/articles" size="sm">
|
||||
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
|
||||
@@ -76,7 +77,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 max-w-4xl">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input id="title" bind:value={title} />
|
||||
@@ -94,11 +95,28 @@
|
||||
|
||||
<!-- Markdown editor with live preview -->
|
||||
<div class="space-y-1.5">
|
||||
<Label>Content (Markdown)</Label>
|
||||
<div class="grid grid-cols-2 gap-4 min-h-96">
|
||||
<Textarea bind:value={content} class="h-full min-h-96 font-mono text-sm resize-none" />
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>Content (Markdown)</Label>
|
||||
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
|
||||
onclick={() => (editorTab = "write")}
|
||||
>Write</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
|
||||
onclick={() => (editorTab = "preview")}
|
||||
>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 ${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"
|
||||
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}
|
||||
@@ -121,7 +139,7 @@
|
||||
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="category">Category</Label>
|
||||
<Input id="category" bind:value={category} />
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
let publishDate = $state("");
|
||||
let imageId = $state<string | null>(null);
|
||||
let saving = $state(false);
|
||||
let editorTab = $state<"write" | "preview">("write");
|
||||
|
||||
let preview = $derived(content ? (marked.parse(content) as string) : "");
|
||||
|
||||
@@ -72,7 +73,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-3 sm:p-6">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" href="/admin/articles" size="sm">
|
||||
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
|
||||
@@ -81,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 max-w-4xl">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input
|
||||
@@ -106,15 +107,30 @@
|
||||
|
||||
<!-- Markdown editor with live preview -->
|
||||
<div class="space-y-1.5">
|
||||
<Label>Content (Markdown)</Label>
|
||||
<div class="grid grid-cols-2 gap-4 min-h-96">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>Content (Markdown)</Label>
|
||||
<div class="flex rounded-lg border border-border/40 overflow-hidden text-xs sm:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
|
||||
onclick={() => (editorTab = "write")}
|
||||
>Write</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
|
||||
onclick={() => (editorTab = "preview")}
|
||||
>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="Write in Markdown…"
|
||||
class="h-full min-h-96 font-mono text-sm resize-none"
|
||||
class={`h-full min-h-96 font-mono text-sm resize-none ${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"
|
||||
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}
|
||||
@@ -131,7 +147,7 @@
|
||||
{#if imageId}<p class="text-xs text-green-600 mt-1">Image uploaded ✓</p>{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="category">Category</Label>
|
||||
<Input id="category" bind:value={category} placeholder="e.g. news, tutorial…" />
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-3 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Users</h1>
|
||||
<span class="text-sm text-muted-foreground">{data.total} total</span>
|
||||
@@ -114,14 +114,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="rounded-lg border border-border/40 overflow-hidden">
|
||||
<div class="rounded-lg border border-border/40 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/30">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">User</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Email</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Email</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Role</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Joined</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Joined</th>
|
||||
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -134,19 +134,22 @@
|
||||
<img
|
||||
src={getAssetUrl(user.avatar, "mini")}
|
||||
alt=""
|
||||
class="h-8 w-8 rounded-full object-cover"
|
||||
class="h-8 w-8 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-xs font-semibold text-primary"
|
||||
class="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-xs font-semibold text-primary shrink-0"
|
||||
>
|
||||
{(user.artist_name || user.email)[0].toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="font-medium">{user.artist_name || user.first_name || "—"}</span>
|
||||
<div class="min-w-0">
|
||||
<span class="font-medium block truncate">{user.artist_name || user.first_name || "—"}</span>
|
||||
<span class="text-xs text-muted-foreground sm:hidden truncate block">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">{user.email}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground hidden sm:table-cell">{user.email}</td>
|
||||
<td class="px-4 py-3">
|
||||
<Select
|
||||
type="single"
|
||||
@@ -164,7 +167,7 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">{formatDate(user.date_created)}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground hidden md:table-cell">{formatDate(user.date_created)}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Button size="sm" variant="ghost" href="/admin/users/{user.id}">
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-2xl">
|
||||
<div class="p-3 sm:p-6 max-w-2xl">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" href="/admin/users" size="sm">
|
||||
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
|
||||
@@ -109,7 +109,7 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Basic info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="firstName">First name</Label>
|
||||
<Input id="firstName" bind:value={firstName} />
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="p-3 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">Videos</h1>
|
||||
<Button href="/admin/videos/new">
|
||||
@@ -43,14 +43,14 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-border/40 overflow-hidden">
|
||||
<div class="rounded-lg border border-border/40 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/30">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Video</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Badges</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Plays</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground">Likes</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">Badges</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Plays</th>
|
||||
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">Likes</th>
|
||||
<th class="px-4 py-3 text-right font-medium text-muted-foreground">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<td class="px-4 py-3 hidden sm:table-cell">
|
||||
<div class="flex gap-1">
|
||||
{#if video.premium}
|
||||
<span
|
||||
@@ -93,8 +93,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">{video.plays_count ?? 0}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">{video.likes_count ?? 0}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground hidden md:table-cell">{video.plays_count ?? 0}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground hidden md:table-cell">{video.likes_count ?? 0}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Button size="sm" variant="ghost" href="/admin/videos/{video.id}">
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-2xl">
|
||||
<div class="p-3 sm:p-6 max-w-2xl">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" href="/admin/videos" size="sm">
|
||||
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
|
||||
@@ -97,7 +97,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input id="title" bind:value={title} placeholder="Video title" />
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-2xl">
|
||||
<div class="p-3 sm:p-6 max-w-2xl">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" href="/admin/videos" size="sm">
|
||||
<span class="icon-[ri--arrow-left-line] h-4 w-4 mr-1"></span>Back
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user