feat : improve preview for export dialog

This commit is contained in:
2026-09-28 19:57:47 +07:00 Unverified
parent 63341e892b
commit 5b283288f0
6 changed files with 66 additions and 21 deletions
+4 -1
View File
@@ -43,7 +43,10 @@ export function ConstellationImage({
: "text-[76px]",
)}
>
{`${guide.character.name}’s Constellations`}
<ExportText
th={`กลุ่มดาว ${guide.character.name}`}
en={`${guide.character.name}’s Constellations`}
/>
</h2>
<p className="mt-3 line-clamp-2 max-w-275 whitespace-pre-line text-[42px] leading-tight font-bold text-[#f1badd]">
<ExportText th="ดาเมจทั้งทีม" en="Team Damage" />
+28 -11
View File
@@ -180,6 +180,7 @@ export function GuideImageExport({
modeSelection = "dialog",
additionalOptions,
previewPosition = "top",
compactDialog = false,
}: {
children: ReactNode;
fileName: string;
@@ -195,9 +196,10 @@ export function GuideImageExport({
showLanguageSelector?: boolean;
promptForLanguage?: boolean;
localizeFileName?: boolean;
modeSelection?: "dialog" | "inline";
modeSelection?: "dialog" | "inline" | "direct";
additionalOptions?: ReactNode;
previewPosition?: "top" | "right";
compactDialog?: boolean;
}) {
const captureRef = useRef<HTMLDivElement>(null);
const [captureMode, setCaptureMode] = useState<
@@ -214,6 +216,7 @@ export function GuideImageExport({
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [previewing, setPreviewing] = useState(false);
const previewEnabled = modeSelection === "inline" || modeDialogOpen;
const squarePreview = height !== undefined && width === height;
function requestCapture() {
setModeDialogOpen(false);
@@ -228,7 +231,10 @@ export function GuideImageExport({
size="sm"
className={mobileIconOnly ? "max-sm:size-7 max-sm:px-0 max-sm:has-data-[icon=inline-start]:pl-0" : undefined}
disabled={capturing || disabled}
onClick={() => setModeDialogOpen(true)}
onClick={() => {
if (modeSelection === "direct") setCaptureMode("with-notes");
else setModeDialogOpen(true);
}}
>
{capturing ? (
<Spinner data-icon="inline-start" />
@@ -302,16 +308,26 @@ export function GuideImageExport({
const exportPreview = (
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">ตัวอย่างภาพส่งออก</p>
<div className="flex min-h-32 items-center justify-center overflow-hidden rounded-lg border bg-muted/30 p-2">
{previewing ? (
<Spinner className="size-6" />
) : previewUrl ? (
<div
className={cn(
"relative flex h-[clamp(8rem,50svh,32rem)] items-center justify-center overflow-hidden rounded-lg border bg-muted/30 p-2",
modeSelection === "dialog" && squarePreview && "lg:h-80",
)}
>
{previewUrl ? (
// This is a temporary client-generated blob, not an optimizable remote asset.
// eslint-disable-next-line @next/next/no-img-element
<img src={previewUrl} alt="ตัวอย่างภาพที่จะส่งออก" className="max-h-[50svh] max-w-full object-contain" />
<img src={previewUrl} alt="Preview image" className="size-full object-contain" />
) : previewing ? (
<Spinner className="size-6" />
) : (
<p className="text-sm text-muted-foreground">ไม่สามารถสร้างตัวอย่างได้</p>
<p className="text-sm text-muted-foreground">เกิดข้อผิดพลาดในการโชว์ Preview</p>
)}
{previewing && previewUrl ? (
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Spinner className="size-6" />
</div>
) : null}
</div>
</div>
);
@@ -444,7 +460,8 @@ export function GuideImageExport({
<DialogContent
className={cn(
"max-h-[90svh] overflow-y-auto sm:max-w-3xl",
previewPosition === "right" && "sm:w-fit sm:max-w-[calc(100%-2rem)]",
previewPosition === "right" && "lg:max-w-[calc(100%-2rem)]",
previewPosition === "right" && (squarePreview || compactDialog ? "lg:w-156" : "lg:w-236"),
)}
>
<DialogHeader>
@@ -452,12 +469,12 @@ export function GuideImageExport({
<DialogDescription>เลือกตัวเลือกที่ต้องการก่อนส่งออกภาพ</DialogDescription>
</DialogHeader>
{previewPosition === "right" ? (
<div className="grid min-h-0 gap-4 lg:grid-cols-[16rem_fit-content(42rem)]">
<div className="grid min-h-0 gap-4 lg:grid-cols-[16rem_minmax(0,1fr)]">
<div className="flex min-w-0 flex-col gap-4">
{additionalOptions}
{exportOptions}
</div>
<div className="min-w-0">{exportPreview}</div>
<div className="w-full min-w-0">{exportPreview}</div>
</div>
) : (
<>
+14 -7
View File
@@ -1,4 +1,5 @@
import Image from "next/image";
import localFont from "next/font/local";
import Link from "next/link";
import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react";
import type { ReactNode } from "react";
@@ -64,6 +65,11 @@ import {
import { publicGuideSections } from "@/lib/guides/public-sections";
import { cn } from "@/lib/utils";
const kanitSemiBold = localFont({
src: "../../public/Kanit-SemiBold.ttf",
weight: "600",
});
export function publicSections(guide: PublicGuide) {
return publicGuideSections(guide);
}
@@ -252,6 +258,7 @@ function GuideOverview({
showLanguageSelector={false}
localizeFileName={false}
captureClassName="p-0"
modeSelection="direct"
>
<MaterialPoster
character={guide.character}
@@ -416,7 +423,7 @@ function WeaponSectionContent({
) : null}
<header className="relative z-10 flex items-center justify-center text-center">
<div className="flex flex-col items-center gap-3">
<p className="text-sm font-medium text-primary">
<p className={cn("text-sm font-medium text-primary", exporting && "drop-shadow-[0_3px_2px_rgba(0,0,0,0.9)]")}>
อาวุธที่แนะนำ
</p>
<div className="flex items-center justify-center gap-3">
@@ -432,7 +439,7 @@ function WeaponSectionContent({
"drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]",
)}
/>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
<h1 className={cn("whitespace-nowrap font-heading text-4xl font-semibold", exporting && "drop-shadow-[0_3px_2px_rgba(0,0,0,0.9)]")}>
Weapons
</h1>
</div>
@@ -649,7 +656,7 @@ function ArtifactSectionContent({
const recommendedConditions = profile?.recommendedConditions ?? [];
const recommendedCards = recommendedConditions.map((condition, conditionIndex) => (
<div key={conditionIndex} data-artifact-export-section="best-stats" data-recommended-condition={conditionIndex} className="flex flex-col gap-5">
{exporting && recommendedConditions.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
{exporting && recommendedConditions.length > 1 ? <p className="text-center text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
{profile ? (
<Card data-artifact-export-section="best-stats" className="artifact-export-main-stats overflow-hidden pt-0">
@@ -706,7 +713,7 @@ function ArtifactSectionContent({
return (
<section
className={cn(
"flex flex-col gap-6",
"artifact-export-root flex flex-col gap-6",
exporting &&
"relative isolate overflow-hidden bg-[#070b20] p-10 text-white",
)}
@@ -716,9 +723,9 @@ function ArtifactSectionContent({
backgroundUrl={guide.character.splashImageUrl ?? guide.coverUrl}
/>
) : null}
<header className="relative z-10 flex items-center justify-center text-center">
<header className={cn("relative z-10 flex items-center justify-center text-center", exporting && kanitSemiBold.className)}>
<div className="flex flex-col items-center gap-3">
<p className="text-sm font-medium text-primary">
<p className={cn("text-sm font-medium text-primary", exporting && "drop-shadow-[0_3px_2px_rgba(0,0,0,0.9)]")}>
อาร์ติแฟกต์และสแตทที่แนะนำ
</p>
<div className="flex items-center justify-center gap-3">
@@ -734,7 +741,7 @@ function ArtifactSectionContent({
"drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]",
)}
/>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
<h1 className={cn("whitespace-nowrap text-4xl font-semibold", !exporting && "font-heading", exporting && "drop-shadow-[0_3px_2px_rgba(0,0,0,0.9)]")}>
Artifacts & Stats
</h1>
</div>
+1
View File
@@ -499,6 +499,7 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
showLanguageSelector={false}
promptForLanguage
previewPosition="right"
compactDialog
>
<TeamExportImage team={team} />
</GuideImageExport>
+4 -1
View File
@@ -54,7 +54,10 @@ export function WeaponComparisonImage({
<GuideExportBranding backgroundUrl={guide.character.splashImageUrl ?? guide.coverUrl} />
<header className="relative z-10 flex flex-col items-center px-48 text-center drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
<h2 className="max-w-full text-5xl leading-tight font-bold text-balance">
{`${guide.character.name}’s Weapon Comparison`}
<ExportText
th={`เปรียบเทียบอาวุธ ${guide.character.name}`}
en={`${guide.character.name}’s Weapon Comparison`}
/>
</h2>
<p className="mt-1 text-4xl font-bold text-white">
{metric === "overallPercent" ? (