diff --git a/app/api/guides/[guideId]/artifacts/image/route.tsx b/app/api/guides/[guideId]/artifacts/image/route.tsx
deleted file mode 100644
index a63384f..0000000
--- a/app/api/guides/[guideId]/artifacts/image/route.tsx
+++ /dev/null
@@ -1,778 +0,0 @@
-/* eslint-disable @next/next/no-img-element */
-import { readFile } from "node:fs/promises";
-import { join } from "node:path";
-
-import { ImageResponse } from "next/og";
-import sharp from "sharp";
-
-import { rarityGradient } from "@/lib/catalog/rarity";
-import { artifactTierCount } from "@/lib/guides/artifacts";
-import { getCachedPublicArtifactExport } from "@/lib/cache/public";
-import { imageContentDisposition } from "@/lib/guides/export-image";
-import {
- normalizeTargetStat,
- recommendedStatIconName,
- statPriorityColor,
- targetStatIconName,
- targetStatOptions,
-} from "@/lib/guides/stats";
-import {
- normalizeTalentPriorities,
- talentRelationSymbol,
- talentTypeLabel,
-} from "@/lib/guides/talents";
-
-const anuphanFonts = Promise.all(
- [
- { subset: "latin", weight: 400 as const },
- { subset: "thai", weight: 400 as const },
- { subset: "latin", weight: 700 as const },
- { subset: "thai", weight: 700 as const },
- ].map(async ({ subset, weight }) => {
- const response = await fetch(
- `https://unpkg.com/@fontsource/anuphan@5.3.0/files/anuphan-${subset}-${weight}-normal.woff`,
- );
- if (!response.ok)
- throw new Error(`Unable to load Anuphan ${subset} ${weight}`);
- return {
- name: "Anuphan",
- data: await response.arrayBuffer(),
- weight,
- style: "normal" as const,
- };
- }),
-).catch(() => []);
-
-const relationFont = readFile(
- join(process.cwd(), "public", "Kanit-SemiBold.ttf"),
-);
-
-function metric(value: string): string {
- return `${Number(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
-}
-
-function statLabel(value: string): string {
- const labels: Record = {
- HP: "พลังชีวิต",
- ATK: "พลังโจมตี",
- DEF: "พลังป้องกัน",
- EM: "ความชำนาญธาตุ",
- ER: "ฟื้นฟูพลังงาน",
- "CRIT RATE": "อัตราคริ",
- "CRIT DAMAGE": "ความแรงคริ",
- "ATK%": "พลังโจมตี%",
- "HP%": "พลังชีวิต%",
- "DEF%": "พลังป้องกัน%",
- "CRIT Rate%/CRIT DMG%": "อัตราคริ/ความแรงคริ",
- "Pyro DMG%": "ความเสียหายธาตุไฟ%",
- "Hydro DMG%": "ความเสียหายธาตุน้ำ%",
- "Cryo DMG%": "ความเสียหายธาตุน้ำแข็ง%",
- "Electro DMG%": "ความเสียหายธาตุไฟฟ้า%",
- "Anemo DMG%": "ความเสียหายธาตุลม%",
- "Geo DMG%": "ความเสียหายธาตุดิน%",
- "Dendro DMG%": "ความเสียหายธาตุไม้%",
- "Physical DMG%": "ความเสียหายกายภาพ%",
- "Healing Bonus%": "โบนัสการรักษา%",
- };
- return labels[value] ?? value;
-}
-
-async function pngDataUrl(url: string): Promise {
- try {
- const response = await fetch(url);
- if (!response.ok) return null;
- const png = await sharp(Buffer.from(await response.arrayBuffer()))
- .resize(96, 96, { fit: "contain" })
- .png()
- .toBuffer();
- return `data:image/png;base64,${png.toString("base64")}`;
- } catch {
- return null;
- }
-}
-
-async function localPngDataUrl(
- fileName: string,
- size: number | null = 96,
- white = false,
-): Promise {
- let image = sharp(join(process.cwd(), "public", "icon", fileName));
- if (white) image = image.negate({ alpha: false });
- const png = await (
- size === null ? image : image.resize(size, size, { fit: "contain" })
- )
- .png()
- .toBuffer();
- return `data:image/png;base64,${png.toString("base64")}`;
-}
-
-export async function GET(
- _request: Request,
- context: RouteContext<"/api/guides/[guideId]/artifacts/image">,
-) {
- const { guideId } = await context.params;
- const guide = await getCachedPublicArtifactExport(guideId);
- if (!guide) return new Response("ไม่พบไกด์", { status: 404 });
- const talents = normalizeTalentPriorities(guide.profile?.talents ?? []);
-
- const [fontAssets, groupAssets, statIconAssets, talentAssets, targetStatAssets] =
- await Promise.all([
- Promise.all([anuphanFonts, relationFont]),
- Promise.all(
- guide.groups.map((group) =>
- Promise.all(
- group.rows.map(async (row) => ({
- first: await pngDataUrl(row.first.imageUrl),
- second: row.second ? await pngDataUrl(row.second.imageUrl) : null,
- })),
- ),
- ),
- ),
- Promise.all([
- localPngDataUrl("Icon_Sands_of_Eon.webp"),
- localPngDataUrl("Icon_Goblet_of_Eonothem.webp"),
- localPngDataUrl("Icon_Circlet_of_Logos.webp"),
- ]),
- Promise.all(
- talents.map((talent) => {
- const catalogTalent = guide.character.talents.find(
- (item) => item.type === talent.type,
- );
- return catalogTalent?.iconUrl
- ? pngDataUrl(catalogTalent.iconUrl)
- : null;
- }),
- ),
- Promise.all(
- targetStatOptions.map((stat) =>
- localPngDataUrl(`stats/${targetStatIconName(stat)}.webp`, 96, true),
- ),
- ),
- ]);
- const [fonts, relationFontData] = fontAssets;
-
- const profile = guide.profile;
- const recommendedStatAsset = (value: string) => {
- const iconName = recommendedStatIconName(value);
- const iconIndex = targetStatOptions.findIndex(
- (stat) => targetStatIconName(stat) === iconName,
- );
- return iconIndex >= 0 ? targetStatAssets[iconIndex] : null;
- };
- const groupRowHeights = guide.groups.map((group) =>
- group.rows.map((row) =>
- Math.max(76, 76 + (row.note ? Math.ceil(row.note.length / 58) * 22 : 0)),
- ),
- );
- const tierListHeight =
- guide.groups.reduce(
- (sum, _group, groupIndex) =>
- sum +
- 68 +
- groupRowHeights[groupIndex].reduce(
- (groupSum, rowHeight) => groupSum + rowHeight,
- 0,
- ),
- 0,
- ) +
- Math.max(0, guide.groups.length - 1) * 16;
- const detailHeight = profile
- ? 60 +
- profile.targets.length * 40 +
- (profile.talents.length ? 82 : 0) +
- (profile.note
- ? Math.max(48, Math.ceil(profile.note.length / 58) * 24)
- : 0)
- : 0;
- const mainStatExtraHeight = profile
- ? Math.max(
- 0,
- Math.max(
- profile.sands.length,
- profile.goblet.length,
- profile.circlet.length,
- ) - 2,
- ) * 24
- : 0;
- const profileHeight = profile
- ? 242 +
- mainStatExtraHeight +
- (profile.substats.length ? 92 + profile.substats.length * 18 : 0)
- : 0;
- const leftHeight = tierListHeight + (profile ? profileHeight + 16 : 0);
- const rightHeight = detailHeight > 60 ? detailHeight : 0;
- const height = Math.max(520, 190 + Math.max(leftHeight, rightHeight));
-
- return new ImageResponse(
-
-
-
-
- อาร์ติแฟกต์และสแตทที่แนะนำ
-
-
- Artifact & Stats
-
-
-
-
-
60 ? 1.2 : 1,
- flexDirection: "column",
- gap: 16,
- }}
- >
- {guide.groups.map((group, groupIndex) => (
-
-
- {group.name === "Late Game" ? "เลทเกม" : group.name}
-
-
- {group.rows.map((row, rowIndex) => (
-
-
- {groupAssets[groupIndex][rowIndex].first ? (
-
-

-
- ) : null}
- {row.second &&
- groupAssets[groupIndex][rowIndex].second ? (
-
-

-
- ) : null}
-
-
-
- {artifactTierCount(row.second?.name ?? null)}{" "}
- {row.first.name}
- {row.second ? ` + ${row.second.name}` : ""}
-
- {row.note ? (
-
- {row.note}
-
- ) : null}
-
-
- {Number(row.percent) === 0 ? "-%" : metric(row.percent)}
-
-
- ))}
-
-
- ))}
-
- {profile ? (
-
-
- ออฟหลักที่ดีที่สุด
-
-
- {[
- {
- label: "นาฬิกา",
- values: profile.sands,
- icon: statIconAssets[0],
- },
- {
- label: "ถ้วย",
- values: profile.goblet,
- icon: statIconAssets[1],
- },
- {
- label: "หัว",
- values: profile.circlet,
- icon: statIconAssets[2],
- },
- ].map((stat) => (
-
-

-
- {stat.label}
-
-
-
- {stat.values.length
- ? stat.values.map((value, index) => (
-
- {recommendedStatAsset(value) ? (
-
- ) : null}
- {statLabel(value)}
-
- ))
- : "-"}
-
-
- ))}
-
-
- ) : null}
-
- {profile?.substats.length ? (
-
-
ออฟรองที่ดีที่สุด
-
- {profile.substats.map((stat, index) => (
-
- {recommendedStatAsset(stat) ? (
-
- ) : null}
- {statLabel(stat)}
-
- ))}
-
-
- ) : null}
-
-
- {profile && detailHeight > 60 ? (
-
-
- {profile.targets.length ? (
-
-
- ค่าสถานะต่ำๆที่ควรจะปั้นไว้
-
- {profile.targets.map((target, index) => {
- const iconIndex = targetStatOptions.findIndex(
- (stat) => stat === normalizeTargetStat(target.stat),
- );
- return (
-
-
- {iconIndex >= 0 ? (
-
- ) : null}
- {normalizeTargetStat(target.stat) ?? target.stat}
-
-
{target.value}
-
- );
- })}
-
- ) : null}
- {talents.length ? (
-
-
- ลำดับการอัพพรสวรรค์
-
-
- {talents.map((talent, index) => (
-
- {talentAssets[index] ? (
-

- ) : null}
-
-
- {talent.type}
- {talent.level ? ` เลเวล ${talent.level}+` : ""}
-
-
- {talentTypeLabel(talent.type)}
-
-
- {index < talents.length - 1 ? (
-
- {talentRelationSymbol(talent.relationToNext)}
-
- ) : null}
-
- ))}
-
-
- ) : null}
- {profile.note ? (
-
-
- โน็ต
-
-
- {profile.note}
-
-
- ) : null}
-
-
- ) : null}
-
-
-
,
- {
- width: 1200,
- height,
- fonts: [
- ...fonts,
- {
- name: "Kanit",
- data: relationFontData,
- weight: 600,
- style: "normal",
- },
- ],
- headers: {
- "Cache-Control": "no-store",
- "Content-Disposition": imageContentDisposition(
- `${guide.guideName}-Artifact`,
- "artifacts",
- ),
- "Content-Type": "image/png",
- },
- },
- );
-}
diff --git a/app/api/guides/[guideId]/weapons/image/route.tsx b/app/api/guides/[guideId]/weapons/image/route.tsx
deleted file mode 100644
index 066d775..0000000
--- a/app/api/guides/[guideId]/weapons/image/route.tsx
+++ /dev/null
@@ -1,334 +0,0 @@
-/* eslint-disable @next/next/no-img-element */
-import { ImageResponse } from "next/og";
-import sharp from "sharp";
-
-import { rarityGradient, rarityTextColor } from "@/lib/catalog/rarity";
-import { getCachedPublicWeaponExport } from "@/lib/cache/public";
-import { imageContentDisposition } from "@/lib/guides/export-image";
-
-const anuphanFonts = Promise.all(
- [
- { subset: "latin", weight: 400 as const },
- { subset: "thai", weight: 400 as const },
- { subset: "latin", weight: 500 as const },
- { subset: "thai", weight: 500 as const },
- { subset: "latin", weight: 600 as const },
- { subset: "thai", weight: 600 as const },
- { subset: "latin", weight: 700 as const },
- { subset: "thai", weight: 700 as const },
- ].map(async ({ subset, weight }) => {
- const response = await fetch(
- `https://unpkg.com/@fontsource/anuphan@5.3.0/files/anuphan-${subset}-${weight}-normal.woff`,
- );
- if (!response.ok)
- throw new Error(`Unable to load Anuphan ${subset} ${weight}`);
- return {
- name: "Anuphan",
- data: await response.arrayBuffer(),
- weight,
- style: "normal" as const,
- };
- }),
-).catch(() => []);
-
-function metric(value: string): string {
- return Number(value) === 0
- ? "-%"
- : `${Number(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
-}
-
-function wrappedLineCount(value: string, lineLength: number): number {
- return value
- .split(/\r?\n/u)
- .reduce(
- (lines, line) => lines + Math.max(1, Math.ceil(line.length / lineLength)),
- 0,
- );
-}
-
-async function pngDataUrl(url: string): Promise {
- try {
- const response = await fetch(url);
- if (!response.ok) return null;
- const png = await sharp(Buffer.from(await response.arrayBuffer()))
- .resize(80, 80, { fit: "contain" })
- .png()
- .toBuffer();
- return `data:image/png;base64,${png.toString("base64")}`;
- } catch {
- return null;
- }
-}
-
-export async function GET(
- _request: Request,
- context: { params: Promise<{ guideId: string }> },
-) {
- const { guideId } = await context.params;
- const guide = await getCachedPublicWeaponExport(guideId);
- if (!guide) return new Response("ไม่พบไกด์", { status: 404 });
-
- const [fonts, weaponImages] = await Promise.all([
- anuphanFonts,
- Promise.all(guide.weapons.map((row) => pngDataUrl(row.weapon.imageUrl))),
- ]);
- const weaponHeights = guide.weapons.map((row) => {
- const noteLines = row.note ? wrappedLineCount(row.note, 58) : 0;
- return Math.max(112, noteLines ? 64 + noteLines * 22 : 112);
- });
- const noteLines = guide.note ? wrappedLineCount(guide.note, 70) : 0;
- const headerHeight = 72 + (noteLines ? 12 + noteLines * 24 : 0);
- const height = Math.max(
- 360,
- 64 +
- headerHeight +
- 24 +
- weaponHeights.reduce((sum, rowHeight) => sum + rowHeight, 0) +
- Math.max(0, guide.weapons.length - 1) * 12,
- );
-
- return new ImageResponse(
-
-
-
-
- อาวุธที่แนะนำ
-
-
- Weapons
-
- {guide.note ? (
-
- {guide.note}
-
- ) : null}
-
- {guide.weapons.map((row, index) => (
-
-
-
- {weaponImages[index] ? (
-

- ) : (
-
- {index + 1}
-
- )}
-
-
- {index + 1}. {row.weapon.name} R{row.refinement}
-
- {row.note ? (
-
- {row.note}
-
- ) : null}
-
-
-
-
- {row.overallPercent ? (
-
-
- {metric(row.overallPercent)}
-
-
- ดาเมททั้งทีม
-
-
- ) : null}
- {row.personalPercent ? (
-
-
- {metric(row.personalPercent)}
-
-
- ดาเมจส่วนตัว
-
-
- ) : null}
-
-
- ))}
-
-
,
- {
- width: 1000,
- height,
- fonts: fonts.length ? fonts : undefined,
- headers: {
- "Cache-Control": "no-store",
- "Content-Disposition": imageContentDisposition(
- `${guide.guideName}-Weapon`,
- "weapons",
- ),
- "Content-Type": "image/png",
- },
- },
- );
-}
diff --git a/components/public/guide-image-export.tsx b/components/public/guide-image-export.tsx
new file mode 100644
index 0000000..eef9e07
--- /dev/null
+++ b/components/public/guide-image-export.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { useEffect, useRef, useState, type ReactNode } from "react";
+import { createPortal } from "react-dom";
+import { DownloadIcon } from "lucide-react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import { Spinner } from "@/components/ui/spinner";
+
+const EXPORT_WIDTH = 1200;
+
+function downloadFileName(value: string): string {
+ return value.replace(/[\\/:*?"<>|]/gu, "-");
+}
+
+export function GuideImageExport({
+ children,
+ fileName,
+}: {
+ children: ReactNode;
+ fileName: string;
+}) {
+ const captureRef = useRef(null);
+ const [capturing, setCapturing] = useState(false);
+
+ useEffect(() => {
+ if (!capturing) return;
+
+ let cancelled = false;
+
+ async function capture() {
+ try {
+ const node = captureRef.current;
+ if (!node) throw new Error("Capture target is unavailable");
+
+ const { domToBlob, waitUntilLoad } = await import("modern-screenshot");
+ await Promise.all([document.fonts.ready, waitUntilLoad(node)]);
+ const blob = await domToBlob(node, {
+ backgroundColor: getComputedStyle(node).backgroundColor,
+ scale: 1,
+ width: EXPORT_WIDTH,
+ });
+ if (cancelled) return;
+
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.download = downloadFileName(fileName);
+ link.href = url;
+ document.body.append(link);
+ link.click();
+ link.remove();
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
+ } catch {
+ if (!cancelled) toast.error("ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง");
+ } finally {
+ if (!cancelled) setCapturing(false);
+ }
+ }
+
+ void capture();
+ return () => {
+ cancelled = true;
+ };
+ }, [capturing, fileName]);
+
+ return (
+ <>
+
+ {capturing
+ ? createPortal(
+ ,
+ document.body,
+ )
+ : null}
+ >
+ );
+}
diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx
index a14ef8d..74f17fc 100644
--- a/components/public/structured-guide.tsx
+++ b/components/public/structured-guide.tsx
@@ -1,9 +1,8 @@
import Image from "next/image";
import Link from "next/link";
-import { ArrowUpRightIcon, ChevronLeftIcon, DownloadIcon } from "lucide-react";
+import { ArrowUpRightIcon, ChevronLeftIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
-import { buttonVariants } from "@/components/ui/button";
import {
Card,
CardAction,
@@ -16,6 +15,7 @@ import { Separator } from "@/components/ui/separator";
import { SiteHeader } from "@/components/public/site-header";
import { ScrollableNav } from "@/components/public/scrollable-nav";
import { TeamSearch } from "@/components/public/team-search";
+import { GuideImageExport } from "@/components/public/guide-image-export";
import { LiveRefresh } from "@/components/live-refresh";
import { rarityGradientClass, rarityTextClass } from "@/lib/catalog/rarity";
import type { PublicGuide } from "@/lib/guides/queries";
@@ -262,6 +262,27 @@ function metric(value: string | null) {
}
function WeaponSection({ guide }: { guide: PublicGuide }) {
+ return (
+
+
+
+ }
+ />
+ );
+}
+
+function WeaponSectionContent({
+ guide,
+ action,
+ exporting = false,
+}: {
+ guide: PublicGuide;
+ action?: React.ReactNode;
+ exporting?: boolean;
+}) {
const note = guide.sections.find(
(section) => section.slug === "weapon",
)?.note;
@@ -277,22 +298,25 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
{note}
) : null}
-
-
- ส่งออกภาพ
-
+ {action}
{guide.weapons.map((row, index) => (
-
+
@@ -301,12 +325,18 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
src={row.weapon.imageUrl}
alt={row.weapon.name}
fill
+ loading={exporting ? "eager" : undefined}
sizes="(min-width: 768px) 5rem, 4rem"
className="object-contain"
/>
-
+
{index + 1}. {row.weapon.name} R{row.refinement}
{row.note ? (
@@ -319,7 +349,8 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
@@ -327,14 +358,22 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
? "-%"
: metric(row.overallPercent)}
-
ดาเมททั้งทีม
+
+ ดาเมททั้งทีม
+
) : null}
{row.personalPercent ? (
@@ -342,7 +381,14 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
? "-%"
: metric(row.personalPercent)}
-
ดาเมจส่วนตัว
+
+ ดาเมจส่วนตัว
+
) : null}
@@ -355,6 +401,27 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {
}
function ArtifactSection({ guide }: { guide: PublicGuide }) {
+ return (
+
+
+
+ }
+ />
+ );
+}
+
+function ArtifactSectionContent({
+ guide,
+ action,
+ exporting = false,
+}: {
+ guide: PublicGuide;
+ action?: React.ReactNode;
+ exporting?: boolean;
+}) {
const profile = guide.artifactProfile;
return (
@@ -365,16 +432,14 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
Artifact & Stats
-
-
- ส่งออกภาพ
-
+ {action}
-
+
{" "}
{profile ? (
@@ -387,16 +452,19 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
iconSrc="/icon/Icon_Sands_of_Eon.webp"
label="นาฬิกา"
values={profile.sands}
+ exporting={exporting}
/>
@@ -411,6 +479,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
key={`${stat}-${index}`}
value={stat}
index={index}
+ exporting={exporting}
/>
))}
@@ -441,7 +510,8 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
@@ -449,6 +519,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
src={row.first.imageUrl}
alt={row.first.name}
fill
+ loading={exporting ? "eager" : undefined}
sizes="(min-width: 640px) 4rem, 3.5rem"
className="object-contain"
/>
@@ -456,7 +527,8 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
{row.second ? (
@@ -464,6 +536,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
src={row.second.imageUrl}
alt={row.second.name}
fill
+ loading={exporting ? "eager" : undefined}
sizes="(min-width: 640px) 4rem, 3.5rem"
className="object-contain"
/>
@@ -517,6 +590,7 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
alt=""
width={14}
height={14}
+ loading={exporting ? "eager" : undefined}
className="size-3.5 shrink-0 object-contain brightness-0 invert"
/>
) : null}
@@ -537,7 +611,12 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
ลำดับการอัพพรสวรรค์
-
+
{normalizeTalentPriorities(profile.talents).map(
(talent, index, talents) => {
const catalogTalent = guide.character.talents.find(
@@ -547,15 +626,29 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
return (
-
-
+
+
{catalogTalent?.iconUrl ? (
) : (
@@ -569,16 +662,31 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
? `เลเวล ${talent.level}+`
: "-"}
-
+
{talentName}
-
+
{talentTypeLabel(talent.type)}
{index < talents.length - 1 ? (
+
{label}
-
+
{values.length ? (
values.map((value, index) => (
))
) : (
@@ -677,7 +801,15 @@ function ArtifactStat({
);
}
-function ArtifactStatValue({ value, index }: { value: string; index: number }) {
+function ArtifactStatValue({
+ value,
+ index,
+ exporting = false,
+}: {
+ value: string;
+ index: number;
+ exporting?: boolean;
+}) {
const iconName = recommendedStatIconName(value);
const hasIcon =
iconName &&
@@ -695,6 +827,7 @@ function ArtifactStatValue({ value, index }: { value: string; index: number }) {
alt=""
width={20}
height={20}
+ loading={exporting ? "eager" : undefined}
className="size-5 shrink-0 object-contain brightness-0 invert"
/>
) : null}
diff --git a/lib/cache/public.ts b/lib/cache/public.ts
index 90f0dfd..3b5a891 100644
--- a/lib/cache/public.ts
+++ b/lib/cache/public.ts
@@ -4,10 +4,8 @@ import { cacheLife, cacheTag } from "next/cache";
import { getPublicPage, listPublicCharacters } from "@/lib/content/queries";
import {
- getPublicArtifactExport,
getPublicGuide,
getPublicTeamExport,
- getPublicWeaponExport,
listGuideCards,
} from "@/lib/guides/queries";
import {
@@ -71,19 +69,3 @@ export async function getCachedPublicTeamExport(teamId: string) {
if (result) cacheTag(pageCacheTag(result.guideId));
return result;
}
-
-export async function getCachedPublicWeaponExport(guideId: string) {
- "use cache: remote";
- cacheLife({ stale: 15, revalidate: 60, expire: 60 * 60 });
- const result = await getPublicWeaponExport(guideId);
- cacheTag(pageCacheTag(guideId));
- return result;
-}
-
-export async function getCachedPublicArtifactExport(guideId: string) {
- "use cache: remote";
- cacheLife({ stale: 15, revalidate: 60, expire: 60 * 60 });
- const result = await getPublicArtifactExport(guideId);
- cacheTag(pageCacheTag(guideId));
- return result;
-}
diff --git a/lib/guides/queries.ts b/lib/guides/queries.ts
index b48e142..cfddd62 100644
--- a/lib/guides/queries.ts
+++ b/lib/guides/queries.ts
@@ -187,102 +187,4 @@ export async function getPublicTeamExport(teamId: string) {
};
}
-export async function getPublicWeaponExport(guideId: string) {
- const [row] = await getDb()
- .select({ guide: guides, note: guideSections.note })
- .from(guides)
- .innerJoin(
- guideSections,
- and(
- eq(guideSections.guideId, guides.id),
- eq(guideSections.slug, "weapon"),
- eq(guideSections.enabled, true),
- ),
- )
- .where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
- .limit(1);
- if (!row) return null;
-
- const weapons = await getDb()
- .select({ recommendation: weaponRecommendations, weapon: catalogWeapons })
- .from(weaponRecommendations)
- .innerJoin(
- catalogWeapons,
- eq(catalogWeapons.key, weaponRecommendations.weaponKey),
- )
- .where(eq(weaponRecommendations.guideId, guideId))
- .orderBy(asc(weaponRecommendations.sortOrder));
-
- return {
- guideName: row.guide.name,
- note: row.note,
- weapons: weapons.map(({ recommendation, weapon }) => ({
- ...recommendation,
- weapon: { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) },
- })),
- };
-}
-
-export async function getPublicArtifactExport(guideId: string) {
- const [row] = await getDb()
- .select({ guide: guides, character: catalogCharacters })
- .from(guides)
- .innerJoin(
- guideSections,
- and(
- eq(guideSections.guideId, guides.id),
- eq(guideSections.slug, "artifact"),
- eq(guideSections.enabled, true),
- ),
- )
- .innerJoin(
- catalogCharacters,
- eq(catalogCharacters.key, guides.characterKey),
- )
- .where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
- .limit(1);
- if (!row) return null;
-
- const [groups, profile] = await Promise.all([
- getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, guideId)).orderBy(asc(artifactGroups.sortOrder)),
- getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, guideId)).limit(1),
- ]);
- const recommendations = groups.length
- ? await getDb()
- .select({ recommendation: artifactRecommendations, artifact: catalogArtifacts })
- .from(artifactRecommendations)
- .innerJoin(
- catalogArtifacts,
- eq(catalogArtifacts.key, artifactRecommendations.firstArtifactKey),
- )
- .where(inArray(artifactRecommendations.groupId, groups.map((group) => group.id)))
- .orderBy(asc(artifactRecommendations.sortOrder))
- : [];
- const secondKeys = recommendations
- .map(({ recommendation }) => recommendation.secondArtifactKey)
- .filter((key): key is string => Boolean(key));
- const secondArtifacts = secondKeys.length
- ? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys))
- : [];
-
- return {
- guideName: row.guide.name,
- character: catalogCharacterWithUrls(row.character),
- groups: groups.map((group) => ({
- ...group,
- rows: recommendations
- .filter(({ recommendation }) => recommendation.groupId === group.id)
- .map(({ recommendation, artifact }) => {
- const second = secondArtifacts.find((item) => item.key === recommendation.secondArtifactKey);
- return {
- ...recommendation,
- first: { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) },
- second: second ? { ...second, imageUrl: publicMediaUrl(second.imageKey) } : null,
- };
- }),
- })),
- profile: profile[0] ?? null,
- };
-}
-
export type PublicGuide = NonNullable
>>;