feat(admin): show character portrait in editor headers
This commit is contained in:
@@ -6,6 +6,7 @@ import { connection } from "next/server";
|
|||||||
import { GuideSectionEditor } from "@/components/admin/guide-section-editor";
|
import { GuideSectionEditor } from "@/components/admin/guide-section-editor";
|
||||||
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
||||||
import { AdminHeader } from "@/components/admin/admin-header";
|
import { AdminHeader } from "@/components/admin/admin-header";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getAdminSession } from "@/lib/auth/server";
|
import { getAdminSession } from "@/lib/auth/server";
|
||||||
import { getActiveCatalog, getGuideByCharacterKey, getPublicGuide } from "@/lib/guides/queries";
|
import { getActiveCatalog, getGuideByCharacterKey, getPublicGuide } from "@/lib/guides/queries";
|
||||||
@@ -21,5 +22,5 @@ export default async function StructuredEditorPage({ params }: { params: Promise
|
|||||||
if (!guide) notFound();
|
if (!guide) notFound();
|
||||||
const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
|
const [catalog, content] = await Promise.all([getActiveCatalog(), getPublicGuide(guide.slug)]);
|
||||||
if (!catalog || !content) notFound();
|
if (!catalog || !content) notFound();
|
||||||
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div><p className="text-sm text-muted-foreground">{guide.name}</p><h1 className="font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={catalog.characters.find((item) => item.key === guide.characterKey)?.constellations ?? []} initial={{ weapons: content.weapons, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent })) })), artifactProfile: content.artifactProfile, constellations: content.constellations, teams: content.teams.map((team) => ({ title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} /></main></div>;
|
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href={`/admin/${character}`} />} nativeButton={false} aria-label="กลับหน้า Guide"><ArrowLeftIcon /></Button><div className="min-w-0 flex-1"><p className="truncate text-sm text-muted-foreground">{guide.name}</p><h1 className="truncate font-heading text-3xl font-semibold">{guide.sections.find((section) => section.slug === page)?.title ?? page}</h1></div><Avatar size="lg" className="size-16 sm:size-20"><AvatarImage src={content.character.imageUrl} alt={content.character.name} /><AvatarFallback>{content.character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></header><GuideSectionNav characterKey={guide.characterKey} current={page as (typeof sections)[number]} /><GuideSectionEditor section={page as (typeof sections)[number]} guide={guide} weapons={catalog.weapons} artifacts={catalog.artifacts} characters={catalog.characters} catalogConstellations={catalog.characters.find((item) => item.key === guide.characterKey)?.constellations ?? []} initial={{ weapons: content.weapons, artifactGroups: content.artifactGroups.map((group) => ({ id: group.id, name: group.name, rows: group.rows.map((row) => ({ firstArtifactKey: row.firstArtifactKey, secondArtifactKey: row.secondArtifactKey, percent: row.percent })) })), artifactProfile: content.artifactProfile, constellations: content.constellations, teams: content.teams.map((team) => ({ title: team.title, rotationSeconds: team.rotationSeconds, combo: team.combo, members: team.members.map((member) => ({ characterKey: member.characterKey, weaponKey: member.weaponKey, artifactKey: member.artifactKey, constellation: member.constellation, refinement: member.refinement, damage: member.damage })) })), extras: content.extras.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ mediaId: row.mediaId, heading: row.heading, body: row.body, percent: row.percent, imageUrl: row.imageUrl })) })) }} /></main></div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeftIcon, BoxesIcon, GemIcon, SparklesIcon, SwordsIcon, UsersIcon } from "lucide-react";
|
import { ArrowLeftIcon, BoxesIcon, GemIcon, SparklesIcon, SwordsIcon, UsersIcon } from "lucide-react";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
@@ -6,6 +5,7 @@ import { connection } from "next/server";
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { AdminHeader } from "@/components/admin/admin-header";
|
import { AdminHeader } from "@/components/admin/admin-header";
|
||||||
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
|
import { GuideOverviewForm } from "@/components/admin/guide-overview-form";
|
||||||
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
import { GuideSectionNav } from "@/components/admin/guide-section-nav";
|
||||||
@@ -25,5 +25,5 @@ export default async function GuideOverviewPage({ params }: { params: Promise<{
|
|||||||
const { character } = await params;
|
const { character } = await params;
|
||||||
const guide = await getGuideByCharacterKey(character);
|
const guide = await getGuideByCharacterKey(character);
|
||||||
if (!guide) notFound();
|
if (!guide) notFound();
|
||||||
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6 lg:p-8"><header className="flex flex-wrap items-center gap-4"><Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button><div className="relative size-16 overflow-hidden rounded-lg border"><Image src={guide.coverUrl} alt={guide.name} fill className="object-cover" /></div><div><p className="text-sm text-muted-foreground">เลือกส่วนที่ต้องการแก้ไข</p><h1 className="font-heading text-3xl font-semibold">{guide.name}</h1></div></header><GuideSectionNav characterKey={guide.characterKey} current="overview" /><GuideOverviewForm guide={guide} /><div className="grid gap-4 md:grid-cols-2">{sections.map(({ slug, title, description, icon: Icon }) => <Link key={slug} href={`/admin/${guide.characterKey}/${slug}`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader><Icon className="size-6 text-primary" /><CardTitle>{title}</CardTitle><CardDescription>{description}</CardDescription></CardHeader></Card></Link>)}<Link href={`/admin/${guide.characterKey}/extra`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader><BoxesIcon className="size-6 text-primary" /><CardTitle>Extra sections</CardTitle><CardDescription>ส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์</CardDescription></CardHeader></Card></Link></div></main></div>;
|
return <div className="min-h-svh"><AdminHeader /><main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6 lg:p-8"><header className="flex items-center gap-4"><Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button><div className="min-w-0 flex-1"><p className="text-sm text-muted-foreground">เลือกส่วนที่ต้องการแก้ไข</p><h1 className="truncate font-heading text-3xl font-semibold">{guide.name}</h1></div><Avatar size="lg" className="size-16 sm:size-20"><AvatarImage src={guide.character.imageUrl} alt={guide.character.name} /><AvatarFallback>{guide.character.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar></header><GuideSectionNav characterKey={guide.characterKey} current="overview" /><GuideOverviewForm guide={guide} /><div className="grid gap-4 md:grid-cols-2">{sections.map(({ slug, title, description, icon: Icon }) => <Link key={slug} href={`/admin/${guide.characterKey}/${slug}`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader><Icon className="size-6 text-primary" /><CardTitle>{title}</CardTitle><CardDescription>{description}</CardDescription></CardHeader></Card></Link>)}<Link href={`/admin/${guide.characterKey}/extra`} className="rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring"><Card className="h-full transition-colors hover:bg-muted/50"><CardHeader><BoxesIcon className="size-6 text-primary" /><CardTitle>Extra sections</CardTitle><CardDescription>ส่วนเสริมแบบภาพ หัวข้อ ข้อความสั้น และเปอร์เซ็นต์</CardDescription></CardHeader></Card></Link></div></main></div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,15 @@ export async function listGuideCards(includeTrash = false) {
|
|||||||
|
|
||||||
export async function getGuideByCharacterKey(characterKey: string) {
|
export async function getGuideByCharacterKey(characterKey: string) {
|
||||||
const [row] = await getDb()
|
const [row] = await getDb()
|
||||||
.select({ guide: guides, cover: media })
|
.select({ guide: guides, cover: media, character: catalogCharacters })
|
||||||
.from(guides)
|
.from(guides)
|
||||||
.innerJoin(media, eq(media.id, guides.coverMediaId))
|
.innerJoin(media, eq(media.id, guides.coverMediaId))
|
||||||
|
.innerJoin(catalogCharacters, and(eq(catalogCharacters.version, guides.catalogVersion), eq(catalogCharacters.key, guides.characterKey)))
|
||||||
.where(and(eq(guides.characterKey, characterKey), isNull(guides.trashedAt)))
|
.where(and(eq(guides.characterKey, characterKey), isNull(guides.trashedAt)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
const sections = await getDb().select().from(guideSections).where(eq(guideSections.guideId, row.guide.id)).orderBy(asc(guideSections.sortOrder));
|
const sections = await getDb().select().from(guideSections).where(eq(guideSections.guideId, row.guide.id)).orderBy(asc(guideSections.sortOrder));
|
||||||
return { ...row.guide, coverUrl: publicMediaUrl(row.cover.objectKey), sections };
|
return { ...row.guide, coverUrl: publicMediaUrl(row.cover.objectKey), character: catalogCharacterWithUrls(row.character), sections };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPublicGuide(slug: string) {
|
export async function getPublicGuide(slug: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user