feat : adding dialog to choose language
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
ExportLanguageOption,
|
||||
type ExportLanguage,
|
||||
} from "@/components/public/export-language";
|
||||
|
||||
export function ExportLanguageDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelect: (language: ExportLanguage) => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>เลือกภาษาในภาพ</DialogTitle>
|
||||
<DialogDescription>What language do you want?</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => onSelect("th")}
|
||||
>
|
||||
<ExportLanguageOption language="th" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => onSelect("en")}
|
||||
>
|
||||
<ExportLanguageOption language="en" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
export type ExportLanguage = "th" | "en";
|
||||
|
||||
const LANGUAGE_OPTIONS = {
|
||||
th: { label: "ไทย" },
|
||||
en: { label: "English" },
|
||||
} satisfies Record<ExportLanguage, { label: string }>;
|
||||
|
||||
const STORAGE_KEY = "buzz-sheet:export-language";
|
||||
const LANGUAGE_EVENT = "buzz-sheet:export-language-change";
|
||||
let memoryLanguage: ExportLanguage = "th";
|
||||
@@ -21,7 +26,7 @@ function storedLanguage(): ExportLanguage {
|
||||
}
|
||||
|
||||
export function useExportLanguage() {
|
||||
const language = useSyncExternalStore(
|
||||
const language = useSyncExternalStore<ExportLanguage>(
|
||||
(onStoreChange) => {
|
||||
window.addEventListener(LANGUAGE_EVENT, onStoreChange);
|
||||
window.addEventListener("storage", onStoreChange);
|
||||
@@ -69,17 +74,85 @@ export function ExportLanguageToggle({
|
||||
<ToggleGroupItem
|
||||
value="th"
|
||||
>
|
||||
ไทย
|
||||
<ExportLanguageOption language="th" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="en"
|
||||
>
|
||||
English
|
||||
<ExportLanguageOption language="en" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExportLanguageOption({
|
||||
language,
|
||||
}: {
|
||||
language: ExportLanguage;
|
||||
}) {
|
||||
const option = LANGUAGE_OPTIONS[language];
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageFlag language={language} />
|
||||
{option.label}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageFlag({ language }: { language: ExportLanguage }) {
|
||||
if (language === "th") {
|
||||
return (
|
||||
<svg
|
||||
data-icon="inline-start"
|
||||
data-language-flag="th"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 9 6"
|
||||
width="24"
|
||||
height="16"
|
||||
>
|
||||
<rect width="9" height="6" fill="#a51931" />
|
||||
<rect y="1" width="9" height="4" fill="#f4f5f8" />
|
||||
<rect y="2" width="9" height="2" fill="#2d2a4a" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
data-icon="inline-start"
|
||||
data-language-flag="en"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 19 10"
|
||||
width="24"
|
||||
height="16"
|
||||
>
|
||||
<rect width="19" height="10" fill="#fff" />
|
||||
{Array.from({ length: 7 }, (_, index) => (
|
||||
<rect
|
||||
key={index}
|
||||
y={index * 20 / 13}
|
||||
width="19"
|
||||
height={10 / 13}
|
||||
fill="#b22234"
|
||||
/>
|
||||
))}
|
||||
<rect width="7.6" height={70 / 13} fill="#3c3b6e" />
|
||||
{Array.from({ length: 9 }, (_, row) =>
|
||||
Array.from({ length: row % 2 === 0 ? 6 : 5 }, (_, column) => (
|
||||
<circle
|
||||
key={`${row}-${column}`}
|
||||
cx={0.65 + column * 1.25 + (row % 2 === 0 ? 0 : 0.625)}
|
||||
cy={0.35 + row * 0.58}
|
||||
r="0.12"
|
||||
fill="#fff"
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExportText({ th, en }: { th: string; en: string }) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DownloadIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExportLanguageDialog } from "@/components/public/export-language-dialog";
|
||||
import {
|
||||
ExportLanguageToggle,
|
||||
useExportLanguage,
|
||||
@@ -53,6 +54,7 @@ export function GuideImageExport({
|
||||
buttonLabel = "ส่งออกภาพ",
|
||||
language: controlledLanguage,
|
||||
showLanguageSelector = true,
|
||||
promptForLanguage = false,
|
||||
localizeFileName = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
@@ -66,6 +68,7 @@ export function GuideImageExport({
|
||||
buttonLabel?: string;
|
||||
language?: ExportLanguage;
|
||||
showLanguageSelector?: boolean;
|
||||
promptForLanguage?: boolean;
|
||||
localizeFileName?: boolean;
|
||||
}) {
|
||||
const captureRef = useRef<HTMLDivElement>(null);
|
||||
@@ -75,6 +78,16 @@ export function GuideImageExport({
|
||||
const capturing = captureMode !== null;
|
||||
const [storedLanguage, setStoredLanguage] = useExportLanguage();
|
||||
const language = controlledLanguage ?? storedLanguage;
|
||||
const [pendingCaptureMode, setPendingCaptureMode] = useState<
|
||||
"with-notes" | "without-notes" | null
|
||||
>(null);
|
||||
|
||||
function selectLanguage(nextLanguage: ExportLanguage) {
|
||||
if (!pendingCaptureMode) return;
|
||||
setStoredLanguage(nextLanguage);
|
||||
setCaptureMode(pendingCaptureMode);
|
||||
setPendingCaptureMode(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!capturing) return;
|
||||
@@ -153,7 +166,10 @@ export function GuideImageExport({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={capturing || disabled}
|
||||
onClick={() => setCaptureMode(mode)}
|
||||
onClick={() => {
|
||||
if (promptForLanguage) setPendingCaptureMode(mode);
|
||||
else setCaptureMode(mode);
|
||||
}}
|
||||
>
|
||||
{captureMode === mode ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
@@ -168,6 +184,13 @@ export function GuideImageExport({
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<ExportLanguageDialog
|
||||
open={pendingCaptureMode !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPendingCaptureMode(null);
|
||||
}}
|
||||
onSelect={selectLanguage}
|
||||
/>
|
||||
{capturing
|
||||
? createPortal(
|
||||
<div
|
||||
|
||||
@@ -1174,6 +1174,8 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
||||
width={1500}
|
||||
height={1500}
|
||||
captureClassName="p-0"
|
||||
showLanguageSelector={false}
|
||||
promptForLanguage
|
||||
>
|
||||
<ConstellationImage
|
||||
guide={guide}
|
||||
|
||||
@@ -469,7 +469,14 @@ export function TeamSearch({ groups }: { groups: PublicGuide["teamGroups"] }) {
|
||||
key={team.id}
|
||||
team={team}
|
||||
action={
|
||||
<GuideImageExport fileName={`${team.title}.png`} width={1080} captureClassName="p-0" allowWithoutNotes={false}>
|
||||
<GuideImageExport
|
||||
fileName={`${team.title}.png`}
|
||||
width={1080}
|
||||
captureClassName="p-0"
|
||||
allowWithoutNotes={false}
|
||||
showLanguageSelector={false}
|
||||
promptForLanguage
|
||||
>
|
||||
<TeamExportImage team={team} />
|
||||
</GuideImageExport>
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ describe("image export controls", () => {
|
||||
expect(html).toContain("ส่งออกภาพโดยไม่รวมโน้ต");
|
||||
expect(html).toContain("ไทย");
|
||||
expect(html).toContain("English");
|
||||
expect(html).toContain('data-language-flag="th"');
|
||||
expect(html).toContain('data-language-flag="en"');
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(4);
|
||||
});
|
||||
|
||||
@@ -27,6 +29,22 @@ describe("image export controls", () => {
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("hides the persistent language chooser when language is prompted on export", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<GuideImageExport
|
||||
fileName="guide.png"
|
||||
allowWithoutNotes={false}
|
||||
showLanguageSelector={false}
|
||||
promptForLanguage
|
||||
>
|
||||
Content
|
||||
</GuideImageExport>,
|
||||
);
|
||||
expect(html).not.toContain("ภาษาในภาพส่งออก");
|
||||
expect(html).not.toContain("English");
|
||||
expect(html.match(/<button\b/gu)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("disables both modes when an export selection is empty", () => {
|
||||
const html = renderToStaticMarkup(<GuideImageExport fileName="guide.png" disabled>No sections</GuideImageExport>);
|
||||
expect(html.match(/\sdisabled=""/gu)).toHaveLength(2);
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
"ioredis": "^6.0.0",
|
||||
"lucide-react": "^1.35.0",
|
||||
"modern-screenshot": "^4.7.0",
|
||||
"next": "16.3.3",
|
||||
"next": "^16.3.5",
|
||||
"postgres": "^3.4.9",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "19.2.8",
|
||||
|
||||
Reference in New Issue
Block a user