feat: simplify admin workspace
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { BackendClient } from "./client";
|
||||
export const dynamic = "force-dynamic";
|
||||
const platforms = ["youtube", "roblox", "discord", "tiktok"];
|
||||
@@ -23,7 +22,7 @@ export default async function BackendPage() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<AdminShellServer>
|
||||
<>
|
||||
<div className="p-6 space-y-6 max-w-6xl mx-auto w-full">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Backend</h1>
|
||||
@@ -33,6 +32,6 @@ export default async function BackendPage() {
|
||||
</div>
|
||||
<BackendClient platforms={platformData} />
|
||||
</div>
|
||||
</AdminShellServer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const tips = [
|
||||
{
|
||||
title: "Responses update while you work",
|
||||
description:
|
||||
"Open form results can refresh automatically when a new response arrives.",
|
||||
},
|
||||
{
|
||||
title: "Closed forms keep their history",
|
||||
description:
|
||||
"Closing a form stops new responses without removing its existing results.",
|
||||
},
|
||||
{
|
||||
title: "Platform counts can be refreshed",
|
||||
description:
|
||||
"Use Backend when a YouTube, TikTok, Discord, or Roblox count looks stale.",
|
||||
},
|
||||
{
|
||||
title: "Preview before sharing",
|
||||
description:
|
||||
"Every form editor includes a preview of what respondents will see.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function AdminWelcome() {
|
||||
const [tipIndex, setTipIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
setTipIndex(Math.floor(Math.random() * tips.length));
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, []);
|
||||
|
||||
const tip = tipIndex === null ? null : tips[tipIndex];
|
||||
|
||||
return (
|
||||
<main className="flex min-h-[calc(100svh-3rem)] w-full items-center justify-center p-6 md:min-h-svh">
|
||||
<div className="flex max-w-2xl flex-col items-center gap-1 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Welcome back!</h1>
|
||||
{tip ? (
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<p className="text-sm font-medium">{tip.title}</p>
|
||||
<p className="text-xs text-muted-foreground">{tip.description}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-md flex-col items-center gap-1.5 pt-1">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus } from "lucide-react";
|
||||
import { DeleteFormButton } from "@/components/delete-buttons";
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { createDefaultForm } from "./actions";
|
||||
import { RealtimeRefresh } from "@/components/realtime-refresh";
|
||||
import { getFormSubmissionStats } from "@/lib/form/form-submission-stats";
|
||||
@@ -31,7 +30,7 @@ export default async function AdminFormsPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminShellServer>
|
||||
<>
|
||||
<RealtimeRefresh topic="forms" />
|
||||
<div className="flex w-full flex-col gap-6 p-4 pb-12 md:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -154,6 +153,6 @@ export default async function AdminFormsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdminShellServer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+130
-2
@@ -1,7 +1,125 @@
|
||||
import { Suspense } from "react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/auth-options";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
Activity,
|
||||
Database,
|
||||
MessageCircle,
|
||||
Music2,
|
||||
RadioTower,
|
||||
} from "lucide-react";
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { getAdminDiscordIds, getSessionDiscordId } from "@/lib/auth/auth";
|
||||
import { authOptions } from "@/lib/auth/auth-options";
|
||||
import { getAdminServiceHealth } from "@/lib/admin/service-health";
|
||||
import { RobloxIcon, YoutubeIcon } from "@/lib/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const iconByService = {
|
||||
database: Database,
|
||||
redis: RadioTower,
|
||||
youtube: YoutubeIcon,
|
||||
tiktok: Music2,
|
||||
discord: MessageCircle,
|
||||
roblox: RobloxIcon,
|
||||
} as const;
|
||||
|
||||
async function AdminHealthStatus() {
|
||||
const snapshot = await getAdminServiceHealth();
|
||||
const unavailable = snapshot.services.filter(
|
||||
(service) => service.status === "unavailable",
|
||||
).length;
|
||||
const summary = unavailable
|
||||
? `${unavailable} ${unavailable === 1 ? "service" : "services"} unavailable`
|
||||
: "All systems operational";
|
||||
|
||||
return (
|
||||
<SidebarGroup className="pb-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" tooltip={summary} aria-label={summary}>
|
||||
<Activity
|
||||
className={cn(
|
||||
"hidden group-data-[collapsible=icon]:block",
|
||||
unavailable ? "text-destructive" : "text-emerald-500",
|
||||
)}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 group-data-[collapsible=icon]:hidden">
|
||||
<div className="flex flex-col gap-1">
|
||||
{(["core", "integration"] as const).map((category) => (
|
||||
<div key={category} className="flex items-center gap-1">
|
||||
{snapshot.services
|
||||
.filter((service) => service.category === category)
|
||||
.map((service) => {
|
||||
const Icon =
|
||||
iconByService[
|
||||
service.id as keyof typeof iconByService
|
||||
];
|
||||
return (
|
||||
<Tooltip key={service.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"flex [&>svg]:shrink-0",
|
||||
category === "core"
|
||||
? "[&>svg]:size-5"
|
||||
: "[&>svg]:size-4",
|
||||
service.status === "operational"
|
||||
? "text-emerald-500"
|
||||
: "text-destructive",
|
||||
)}
|
||||
aria-label={`${service.label}: ${service.status}`}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{service.label}: {service.status}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="truncate text-xs">{summary}</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminHealthStatusSkeleton() {
|
||||
return (
|
||||
<SidebarGroup className="pb-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex h-12 items-center gap-2 px-2">
|
||||
<Skeleton className="size-4" />
|
||||
<Skeleton className="h-3 flex-1" />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
@@ -15,5 +133,15 @@ export default async function AdminLayout({
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
return (
|
||||
<AdminShellServer
|
||||
healthStatus={
|
||||
<Suspense fallback={<AdminHealthStatusSkeleton />}>
|
||||
<AdminHealthStatus />
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AdminShellServer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -105,7 +104,7 @@ export default function AdminLoginPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminShellServer>
|
||||
<>
|
||||
<main className="mx-auto flex w-full max-w-4xl flex-col gap-6 p-4 pb-12 md:p-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Login</h1>
|
||||
@@ -156,6 +155,6 @@ export default function AdminLoginPage() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</AdminShellServer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-370
@@ -1,372 +1,5 @@
|
||||
import { createDefaultForm } from "@/app/admin/form/actions";
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { RealtimeRefresh } from "@/components/realtime-refresh";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getAdminDashboardData } from "@/lib/admin/admin-dashboard";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
ClipboardList,
|
||||
FilePlus2,
|
||||
Server,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { AdminWelcome } from "./client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function formatNumber(value: number) {
|
||||
return new Intl.NumberFormat("en-US").format(value);
|
||||
}
|
||||
|
||||
function formatDate(date: Date | null) {
|
||||
if (!date) return "No responses";
|
||||
return date.toLocaleString("en-GB", {
|
||||
timeZone: "Asia/Bangkok",
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
function respondentName(submission: {
|
||||
userName: string | null;
|
||||
userDiscordId: string | null;
|
||||
}) {
|
||||
return submission.userName || submission.userDiscordId || "Anonymous";
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
label,
|
||||
value,
|
||||
detail,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
detail: string;
|
||||
}) {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardDescription>{label}</CardDescription>
|
||||
<CardTitle className="text-2xl tabular-nums">
|
||||
{formatNumber(value)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-xs text-muted-foreground">
|
||||
{detail}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAction({
|
||||
href,
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
disabled = false,
|
||||
}: {
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: typeof ClipboardList;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Icon className="size-5 text-primary" />
|
||||
{title}
|
||||
</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
<CardAction>
|
||||
{disabled ? (
|
||||
<Button variant="ghost" size="icon-sm" disabled aria-label={`${title} disabled`}>
|
||||
<ArrowUpRight />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="icon-sm" asChild>
|
||||
<Link href={href} aria-label={`Open ${title}`}>
|
||||
<ArrowUpRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminPage() {
|
||||
const { summary, recentSubmissions, activeForms } =
|
||||
await getAdminDashboardData();
|
||||
const busiestFormResponses = Math.max(
|
||||
...activeForms.map((form) => form.responsesLast7Days),
|
||||
1,
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminShellServer>
|
||||
<RealtimeRefresh topic="forms" />
|
||||
<RealtimeRefresh topic="submissions" />
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-6 p-4 pb-12 md:p-6">
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">Command center</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Live form operations · Bangkok time
|
||||
</p>
|
||||
</div>
|
||||
<form action={createDefaultForm}>
|
||||
<Button type="submit">
|
||||
<FilePlus2 data-icon="inline-start" />
|
||||
Create form
|
||||
</Button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-[1.25fr_1fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Responses today</CardDescription>
|
||||
<CardTitle className="text-5xl tabular-nums md:text-6xl">
|
||||
{formatNumber(summary.responsesToday)}
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge>Live</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<p className="max-w-xl text-sm text-muted-foreground">
|
||||
Submissions received since midnight. Realtime updates keep this
|
||||
workspace current while you are here.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
{formatNumber(summary.responsesLast7Days)} this week
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{formatNumber(summary.openForms)} forms accepting responses
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<MetricCard
|
||||
label="Last 7 days"
|
||||
value={summary.responsesLast7Days}
|
||||
detail="Including today"
|
||||
/>
|
||||
<MetricCard
|
||||
label="All responses"
|
||||
value={summary.totalResponses}
|
||||
detail="Lifetime submissions"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Open forms"
|
||||
value={summary.openForms}
|
||||
detail={`${formatNumber(summary.closedForms)} closed`}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total forms"
|
||||
value={summary.totalForms}
|
||||
detail="Across this workspace"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[minmax(0,1.6fr)_minmax(20rem,0.9fr)]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent responses</CardTitle>
|
||||
<CardDescription>
|
||||
The latest submissions across every form.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/admin/form">View forms</Link>
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{recentSubmissions.length === 0 ? (
|
||||
<p className="border p-6 text-sm text-muted-foreground">
|
||||
Responses will appear here after someone submits a form.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="hidden border md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Respondent</TableHead>
|
||||
<TableHead>Form</TableHead>
|
||||
<TableHead>Submitted</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{recentSubmissions.map((submission) => (
|
||||
<TableRow key={submission.id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/admin/form/${submission.formId}/result/${submission.id}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{respondentName(submission)}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/admin/form/${submission.formId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{submission.formTitle}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(submission.submittedAt)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
submission.edited ? "secondary" : "outline"
|
||||
}
|
||||
>
|
||||
{submission.edited ? "Edited" : "Original"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 md:hidden">
|
||||
{recentSubmissions.map((submission) => (
|
||||
<Card key={submission.id} size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="truncate">
|
||||
<Link
|
||||
href={`/admin/form/${submission.formId}/result/${submission.id}`}
|
||||
>
|
||||
{respondentName(submission)}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<CardDescription className="truncate">
|
||||
{submission.formTitle} · {formatDate(submission.submittedAt)}
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Badge
|
||||
variant={
|
||||
submission.edited ? "secondary" : "outline"
|
||||
}
|
||||
>
|
||||
{submission.edited ? "Edited" : "Original"}
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Form momentum</CardTitle>
|
||||
<CardDescription>
|
||||
Response activity during the last 7 days.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-5">
|
||||
{activeForms.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a form to start tracking activity.
|
||||
</p>
|
||||
) : (
|
||||
activeForms.map((form) => (
|
||||
<div key={form.id} className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Link
|
||||
href={`/admin/form/${form.id}/result`}
|
||||
className="truncate font-medium hover:underline"
|
||||
>
|
||||
{form.title}
|
||||
</Link>
|
||||
<span className="shrink-0 text-sm tabular-nums text-muted-foreground">
|
||||
{formatNumber(form.responsesLast7Days)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
(form.responsesLast7Days / busiestFormResponses) * 100
|
||||
}
|
||||
aria-label={`${form.title}: ${form.responsesLast7Days} responses in the last 7 days`}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<Badge variant={form.isOpen ? "default" : "secondary"}>
|
||||
{form.isOpen ? "Open" : "Closed"}
|
||||
</Badge>
|
||||
<span>{formatNumber(form.totalResponses)} total</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-3">
|
||||
<h2 className="font-heading text-lg font-medium">Workspace</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Go directly to the tools you operate most often.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<QuickAction
|
||||
href="/admin/form"
|
||||
title="Forms"
|
||||
description="Build forms and review submissions."
|
||||
icon={ClipboardList}
|
||||
/>
|
||||
<QuickAction
|
||||
href="/admin/upload"
|
||||
title="Video uploads"
|
||||
description="Publish and monitor platform jobs."
|
||||
icon={Upload}
|
||||
disabled
|
||||
/>
|
||||
<QuickAction
|
||||
href="/admin/backend"
|
||||
title="Backend"
|
||||
description="Manage follower APIs and cache."
|
||||
icon={Server}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</AdminShellServer>
|
||||
);
|
||||
export default function AdminPage() {
|
||||
return <AdminWelcome />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AdminShellServer } from "@/components/admin-shell-server";
|
||||
import { UploadNavigation } from "./upload-navigation";
|
||||
|
||||
export default function AdminUploadLayout({
|
||||
@@ -7,7 +6,7 @@ export default function AdminUploadLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AdminShellServer>
|
||||
<>
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-6 p-4 pb-12 md:p-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">Creator studio</p>
|
||||
@@ -21,6 +20,6 @@ export default function AdminUploadLayout({
|
||||
<UploadNavigation />
|
||||
{children}
|
||||
</main>
|
||||
</AdminShellServer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
|
||||
title: "Erika!",
|
||||
description:
|
||||
"แหล่งรวมลิ้งค์ทั้งหมดของช่อง Erika!",
|
||||
keywords: ["Erika", "erika!", "erika jjs", "สุดหล่อ", "Roblox", "Discord", "TikTok", "YouTube", "Linktree", "ลิ้งค์"],
|
||||
keywords: ["Erika", "เอริกะ", "เอริก้า", "สุดหล่อ", "Roblox", "Discord", "TikTok", "YouTube", "Linktree", "ลิ้งค์"],
|
||||
metadataBase: new URL(siteUrl),
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
|
||||
@@ -36,18 +36,6 @@ export default async function ShareLayout({ children }: { children: React.ReactN
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/share/all">
|
||||
<List data-icon="inline-start" />
|
||||
ทั้งหมด
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/share">
|
||||
<Plus data-icon="inline-start" />
|
||||
สร้างลิงก์
|
||||
</Link>
|
||||
</Button>
|
||||
{session?.user && (
|
||||
<DropdownMenuAvatar
|
||||
userName={session.user.name ?? ""}
|
||||
|
||||
Reference in New Issue
Block a user