diff --git a/app/admin/[character]/[page]/page.tsx b/app/admin/[character]/[page]/page.tsx
index d9ff124..66860df 100644
--- a/app/admin/[character]/[page]/page.tsx
+++ b/app/admin/[character]/[page]/page.tsx
@@ -13,7 +13,7 @@ import { getAdminSession } from "@/lib/auth/server";
import {
getActiveCatalog,
getAdminGuideContent,
- getGuideByCharacterKey,
+ getGuideEditorHeader,
} from "@/lib/guides/queries";
const sections = [
@@ -33,7 +33,7 @@ export default async function StructuredEditorPage({
await connection();
if (!(await getAdminSession())) redirect("/admin/login");
const { character, page } = await params;
- const guide = await getGuideByCharacterKey(character);
+ const guide = await getGuideEditorHeader(character);
if (!guide) notFound();
const extraSections = guide.sections.filter((section) => section.kind === "extra");
if (page === "extra") {
diff --git a/app/admin/[character]/page.tsx b/app/admin/[character]/page.tsx
index acc6690..9615043 100644
--- a/app/admin/[character]/page.tsx
+++ b/app/admin/[character]/page.tsx
@@ -55,7 +55,7 @@ export default async function GuideOverviewPage({ params }: { params: Promise<{
{sections.map(({ slug, title, description }) => (
-
+
{navigationIconPath(slug) ? : null}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index ab92431..4dc2fb5 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -4,17 +4,20 @@ import { connection } from "next/server";
import { StructuredDashboard } from "@/components/admin/structured-dashboard";
import { AdminHeader } from "@/components/admin/admin-header";
import { getAdminSession } from "@/lib/auth/server";
-import { getLatestLunarisVersion } from "@/lib/catalog/sync";
-import { getActiveCatalog, listGuideCards } from "@/lib/guides/queries";
+import {
+ getCurrentCatalogVersion,
+ getLatestLunarisVersion,
+} from "@/lib/catalog/sync";
+import { listGuideCards } from "@/lib/guides/queries";
export default async function AdminPage() {
await connection();
if (!(await getAdminSession())) redirect("/admin/login");
- const [guides, trash, catalog, latestCatalogVersion] = await Promise.all([
+ const [guides, trash, catalogVersion, latestCatalogVersion] = await Promise.all([
listGuideCards(),
listGuideCards(true),
- getActiveCatalog(),
- getLatestLunarisVersion().catch(() => null),
+ getCurrentCatalogVersion(),
+ getLatestLunarisVersion(AbortSignal.timeout(3_000)).catch(() => null),
]);
return (
@@ -25,7 +28,7 @@ export default async function AdminPage() {
Guide Dashboard
จัดการ Guide แบบภาพ-first และบันทึกขึ้นเว็บไซต์ทันที
-
+
);
diff --git a/components/admin/guide-section-nav.tsx b/components/admin/guide-section-nav.tsx
index 2b59c99..35e40df 100644
--- a/components/admin/guide-section-nav.tsx
+++ b/components/admin/guide-section-nav.tsx
@@ -40,7 +40,7 @@ export function GuideSectionNav({
>
แก้ไข Guide {guide.name}
@@ -205,6 +205,7 @@ export function StructuredDashboard({
src={guide.coverUrl}
alt={`Guide ${guide.name}`}
fill
+ sizes="(min-width: 1280px) 33vw, (min-width: 768px) 50vw, 100vw"
className="object-cover"
/>
diff --git a/components/public/structured-directory.tsx b/components/public/structured-directory.tsx
index 700cf4a..987b1dc 100644
--- a/components/public/structured-directory.tsx
+++ b/components/public/structured-directory.tsx
@@ -70,6 +70,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
src={guide.coverUrl}
alt={`ไกด์ ${guide.name}`}
fill
+ sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
className="object-cover"
/>
diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx
index 0d4a0e8..9d53678 100644
--- a/components/public/structured-guide.tsx
+++ b/components/public/structured-guide.tsx
@@ -146,7 +146,7 @@ export function StructuredGuideShell({
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
- prefetch
+ prefetch={false}
>
{navigationIconPath(item.slug) ? (
diff --git a/lib/cache/public.ts b/lib/cache/public.ts
index a561bdf..0d28f23 100644
--- a/lib/cache/public.ts
+++ b/lib/cache/public.ts
@@ -57,7 +57,6 @@ export async function getCachedPublicGuide(slug: string) {
"use cache: remote";
cacheLife({ stale: 15, revalidate: 60, expire: 60 * 60 });
const result = await getPublicGuide(slug);
- cacheTag(DIRECTORY_CACHE_TAG);
cacheTag(GLOSSARY_CACHE_TAG);
if (result) cacheTag(pageCacheTag(result.id));
return result;
diff --git a/lib/catalog/sync.ts b/lib/catalog/sync.ts
index 5c66c97..2762970 100644
--- a/lib/catalog/sync.ts
+++ b/lib/catalog/sync.ts
@@ -157,8 +157,10 @@ function rarity(value: string | undefined): number | null {
);
}
-export async function getLatestLunarisVersion(): Promise {
- return versionSchema.parse(await getJson(VERSION_URL)).version;
+export async function getLatestLunarisVersion(
+ signal?: AbortSignal,
+): Promise {
+ return versionSchema.parse(await getJson(VERSION_URL, signal)).version;
}
async function mapConcurrent(
diff --git a/lib/guides/mutations.ts b/lib/guides/mutations.ts
index da8ff8f..714d298 100644
--- a/lib/guides/mutations.ts
+++ b/lib/guides/mutations.ts
@@ -55,9 +55,11 @@ export class GuideVersionConflictError extends Error {
}
}
-async function emitGuideEvents(tx: Parameters["transaction"]>[0]>[0], guideId: string, version: number) {
+async function emitGuideEvents(tx: Parameters["transaction"]>[0]>[0], guideId: string, version: number, directoryChanged = false) {
await tx.insert(outboxEvents).values([
- { topic: "directory", aggregateId: guideId, eventType: "directory.updated", payload: { id: guideId, version } },
+ ...(directoryChanged
+ ? [{ topic: "directory", aggregateId: guideId, eventType: "directory.updated", payload: { id: guideId, version } }]
+ : []),
{ topic: `page:${guideId}`, aggregateId: guideId, eventType: "page.updated", payload: { id: guideId, version } },
{ topic: "admin", aggregateId: guideId, eventType: "admin.updated", payload: { id: guideId, version } },
]);
@@ -89,19 +91,19 @@ export async function createGuide(input: unknown) {
]);
await tx.insert(artifactProfiles).values({ guideId: guide.id });
await tx.update(media).set({ currentReferenceCount: sql`${media.currentReferenceCount} + 1` }).where(eq(media.id, parsed.coverMediaId));
- await emitGuideEvents(tx, guide.id, guide.version);
+ await emitGuideEvents(tx, guide.id, guide.version, true);
return guide;
});
}
-async function advanceGuideVersion(tx: Parameters["transaction"]>[0]>[0], guideId: string, expectedVersion: number) {
+async function advanceGuideVersion(tx: Parameters["transaction"]>[0]>[0], guideId: string, expectedVersion: number, directoryChanged = false) {
const [guide] = await tx.update(guides).set({ version: expectedVersion + 1 }).where(and(eq(guides.id, guideId), eq(guides.version, expectedVersion), sql`${guides.trashedAt} is null`)).returning();
if (!guide) {
const [current] = await tx.select({ version: guides.version }).from(guides).where(eq(guides.id, guideId)).limit(1);
if (current) throw new GuideVersionConflictError(current.version);
throw new Error("ไม่พบ Guide");
}
- await emitGuideEvents(tx, guide.id, guide.version);
+ await emitGuideEvents(tx, guide.id, guide.version, directoryChanged);
return guide;
}
@@ -146,7 +148,7 @@ export async function updateGuideBasics(guideId: string, input: unknown) {
: [];
if (bleedingMedia.length !== bleedingMediaIds.length) throw new Error("ไม่พบภาพ Bleeding ที่อัปโหลดแล้ว");
}
- const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion);
+ const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, true);
await tx.update(guides).set({
overview: data.overview,
coverMediaId: data.coverMediaId,
@@ -414,7 +416,7 @@ export async function trashGuide(guideId: string) {
return getDb().transaction(async (tx) => {
const [guide] = await tx.update(guides).set({ trashedAt: new Date(), version: sql`${guides.version} + 1` }).where(eq(guides.id, guideId)).returning();
if (!guide) throw new Error("ไม่พบ Guide");
- await emitGuideEvents(tx, guide.id, guide.version);
+ await emitGuideEvents(tx, guide.id, guide.version, true);
return guide;
});
}
@@ -423,7 +425,7 @@ export async function restoreGuide(guideId: string) {
return getDb().transaction(async (tx) => {
const [guide] = await tx.update(guides).set({ trashedAt: null, version: sql`${guides.version} + 1` }).where(eq(guides.id, guideId)).returning();
if (!guide) throw new Error("ไม่พบ Guide");
- await emitGuideEvents(tx, guide.id, guide.version);
+ await emitGuideEvents(tx, guide.id, guide.version, true);
return guide;
});
}
@@ -436,7 +438,7 @@ export async function setGuideVisibility(guideId: string, isPublic: boolean) {
.where(and(eq(guides.id, guideId), sql`${guides.trashedAt} is null`))
.returning();
if (!guide) throw new Error("ไม่พบ Guide");
- await emitGuideEvents(tx, guide.id, guide.version);
+ await emitGuideEvents(tx, guide.id, guide.version, true);
return guide;
});
}
@@ -454,7 +456,7 @@ export async function permanentlyDeleteGuide(guideId: string) {
for (const layer of bleeding) {
await tx.update(media).set({ currentReferenceCount: sql`greatest(${media.currentReferenceCount} - 1, 0)` }).where(eq(media.id, layer.mediaId));
}
- await emitGuideEvents(tx, guide.id, guide.version + 1);
+ await emitGuideEvents(tx, guide.id, guide.version + 1, true);
return { version: guide.version + 1 };
});
}
diff --git a/lib/guides/queries.ts b/lib/guides/queries.ts
index d10b419..c11d8ed 100644
--- a/lib/guides/queries.ts
+++ b/lib/guides/queries.ts
@@ -113,23 +113,72 @@ export async function getActiveCatalog() {
export async function listGuideCards(includeTrash = false) {
const rows = await getDb()
- .select({ guide: guides, cover: media })
+ .select({
+ id: guides.id,
+ characterKey: guides.characterKey,
+ name: guides.name,
+ overview: guides.overview,
+ version: guides.version,
+ updatedAt: guides.updatedAt,
+ trashedAt: guides.trashedAt,
+ isPublic: guides.isPublic,
+ coverObjectKey: media.objectKey,
+ })
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.where(includeTrash ? isNotNull(guides.trashedAt) : isNull(guides.trashedAt))
.orderBy(asc(guides.sortOrder), asc(guides.name));
- return rows.map(({ guide, cover }) => ({ ...guide, coverUrl: publicMediaUrl(cover.objectKey) }));
+ return rows.map(({ coverObjectKey, ...guide }) => ({
+ ...guide,
+ coverUrl: publicMediaUrl(coverObjectKey),
+ }));
}
export async function listPublicGuideCards() {
const [rows, aliases] = await Promise.all([getDb()
- .select({ guide: guides, cover: media })
+ .select({
+ id: guides.id,
+ characterKey: guides.characterKey,
+ slug: guides.slug,
+ name: guides.name,
+ overview: guides.overview,
+ version: guides.version,
+ coverObjectKey: media.objectKey,
+ })
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.where(and(eq(guides.isPublic, true), isNull(guides.trashedAt)))
.orderBy(asc(guides.sortOrder), asc(guides.name)), getDb().select().from(glossaryAliases)]);
const grouped = aliasesByItem(aliases);
- return rows.map(({ guide, cover }) => ({ ...guide, aliases: grouped.character.get(guide.characterKey) ?? [], coverUrl: publicMediaUrl(cover.objectKey) }));
+ return rows.map(({ coverObjectKey, ...guide }) => ({
+ ...guide,
+ aliases: grouped.character.get(guide.characterKey) ?? [],
+ coverUrl: publicMediaUrl(coverObjectKey),
+ }));
+}
+
+export async function getGuideEditorHeader(characterKey: string) {
+ const [row] = await getDb()
+ .select({
+ id: guides.id,
+ characterKey: guides.characterKey,
+ slug: guides.slug,
+ name: guides.name,
+ version: guides.version,
+ coverObjectKey: media.objectKey,
+ })
+ .from(guides)
+ .innerJoin(media, eq(media.id, guides.coverMediaId))
+ .where(and(eq(guides.characterKey, characterKey), isNull(guides.trashedAt)))
+ .limit(1);
+ if (!row) return null;
+ const sections = await getDb()
+ .select()
+ .from(guideSections)
+ .where(eq(guideSections.guideId, row.id))
+ .orderBy(asc(guideSections.sortOrder));
+ const { coverObjectKey, ...guide } = row;
+ return { ...guide, coverUrl: publicMediaUrl(coverObjectKey), sections };
}
export async function getGuideByCharacterKey(characterKey: string) {