diff --git a/src/app/app/dashboard/templates/page.tsx b/src/app/app/dashboard/templates/page.tsx index ed68cef..b21e274 100644 --- a/src/app/app/dashboard/templates/page.tsx +++ b/src/app/app/dashboard/templates/page.tsx @@ -1,31 +1,205 @@ -import { useState, useRef } from "react"; -import { useTranslations } from "@/i18n/compat/client"; +import { useEffect, useRef, useState } from "react"; +import { useLocale, useTranslations } from "@/i18n/compat/client"; import { motion } from "framer-motion"; import { useRouter } from "@/lib/navigation"; import { cn } from "@/lib/utils"; import { DEFAULT_TEMPLATES } from "@/config"; import { useResumeStore } from "@/store/useResumeStore"; -import { Card, CardContent } from "@/components/ui/card"; +import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import ResumeTemplateComponent from "@/components/templates"; import { initialResumeState, initialResumeStateEn } from "@/config/initialResumeData"; -import { useLocale } from "@/i18n/compat/client"; +import type { ResumeTemplate } from "@/types/template"; -const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.1, +const A4_WIDTH_PX = 793.700787; +const PREVIEW_MODAL_SCALE = 0.529166667; + +const PRESET_COLORS = [ + { name: "default", value: "" }, + { name: "blue", value: "#3b82f6" }, + { name: "green", value: "#10b981" }, + { name: "purple", value: "#8b5cf6" }, + { name: "orange", value: "#f97316" }, + { name: "red", value: "#ef4444" }, + { name: "slate", value: "#475569" }, + { name: "black", value: "#000000" }, +]; + +const getTemplateKey = (templateId: string) => + templateId === "left-right" ? "leftRight" : templateId; + +type TemplatePreviewBaseData = + | typeof initialResumeState + | typeof initialResumeStateEn; + +const buildTemplatePreviewData = ( + baseData: TemplatePreviewBaseData, + template: ResumeTemplate, + selectedColor: string, + mockId: string +) => + ({ + ...baseData, + id: mockId, + templateId: template.id, + globalSettings: { + ...baseData.globalSettings, + themeColor: selectedColor || template.colorScheme.primary, + sectionSpacing: template.spacing.sectionGap, + paragraphSpacing: template.spacing.itemGap, + pagePadding: template.spacing.contentPadding, }, - }, -}; + basic: { + ...baseData.basic, + layout: template.basic.layout, + }, + } as any); -const item = { - hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0 }, +interface TemplateCardItemProps { + index: number; + template: ResumeTemplate; + templateName: string; + templateDescription: string; + baseData: TemplatePreviewBaseData; + selectedColor: string; + onPreview: () => void; + onUseTemplate: () => void; + previewLabel: string; + useTemplateLabel: string; +} + +const TemplateCardItem = ({ + index, + template, + templateName, + templateDescription, + baseData, + selectedColor, + onPreview, + onUseTemplate, + previewLabel, + useTemplateLabel, +}: TemplateCardItemProps) => { + const containerRef = useRef(null); + const [scale, setScale] = useState(0.24); + + useEffect(() => { + if (!containerRef.current) return; + + const observer = new ResizeObserver((entries) => { + const { width } = entries[0].contentRect; + if (width > 0) { + setScale(width / A4_WIDTH_PX); + } + }); + + observer.observe(containerRef.current); + return () => observer.disconnect(); + }, []); + + const previewData = buildTemplatePreviewData( + baseData, + template, + selectedColor, + `template-preview-${template.id}` + ); + + return ( + + + +
+
+
+ +
+
+
+ +
+
+
+ + {templateName} + + + {templateDescription} + +
+
+ + + +
+ + + + + + + +
+
+ + + ); }; const TemplatesPage = () => { @@ -33,39 +207,27 @@ const TemplatesPage = () => { const locale = useLocale(); const router = useRouter(); const createResume = useResumeStore((state) => state.createResume); - const [previewTemplate, setPreviewTemplate] = useState<{ - id: string; - open: boolean; - } | null>(null); - - const PRESET_COLORS = [ - { name: "default", value: "" }, - { name: "blue", value: "#3b82f6" }, - { name: "green", value: "#10b981" }, - { name: "purple", value: "#8b5cf6" }, - { name: "orange", value: "#f97316" }, - { name: "red", value: "#ef4444" }, - { name: "slate", value: "#475569" }, - { name: "black", value: "#000000" }, - ]; + const [previewTemplate, setPreviewTemplate] = useState(null); const [selectedColor, setSelectedColor] = useState(PRESET_COLORS[0].value); - const autoPlayRef = useRef(null); + const autoPlayRef = useRef | null>(null); - // Auto-cycle colors every 3 seconds - useState(() => { - // Only run on client - if (typeof window !== "undefined") { - let currentIndex = 0; - autoPlayRef.current = setInterval(() => { - currentIndex = (currentIndex + 1) % PRESET_COLORS.length; - setSelectedColor(PRESET_COLORS[currentIndex].value); - }, 3000); - } - }); + useEffect(() => { + let currentIndex = 0; + autoPlayRef.current = setInterval(() => { + currentIndex = (currentIndex + 1) % PRESET_COLORS.length; + setSelectedColor(PRESET_COLORS[currentIndex].value); + }, 3000); + + return () => { + if (autoPlayRef.current) { + clearInterval(autoPlayRef.current); + autoPlayRef.current = null; + } + }; + }, []); const handleColorSelect = (value: string) => { setSelectedColor(value); - // Stop autoplay when user manually selects a color if (autoPlayRef.current) { clearInterval(autoPlayRef.current); autoPlayRef.current = null; @@ -73,9 +235,12 @@ const TemplatesPage = () => { }; const baseData = locale === "en" ? initialResumeStateEn : initialResumeState; + const activePreviewTemplate = + DEFAULT_TEMPLATES.find((template) => template.id === previewTemplate) ?? + null; const handleCreateResume = (templateId: string) => { - const template = DEFAULT_TEMPLATES.find((t) => t.id === templateId); + const template = DEFAULT_TEMPLATES.find((entry) => entry.id === templateId); if (!template) return; const resumeId = createResume(templateId); @@ -103,225 +268,122 @@ const TemplatesPage = () => { return ( -
+
-

{t("title")}

- -
- {PRESET_COLORS.map((color) => ( - - ))} -
-
+

{t("title")}

- - {DEFAULT_TEMPLATES.map((template) => { - const templateKey = - template.id === "left-right" ? "leftRight" : template.id; - return ( - - + {PRESET_COLORS.map((color) => ( + - -
- - - - ); - })} - - - { - if (!open) setPreviewTemplate(null); - }} - > - {previewTemplate && ( - -
-
- - {t( - `${ - previewTemplate.id === "left-right" - ? "leftRight" - : previewTemplate.id - }.name` - )} - -
-
-
+ {color.value ? (
t.id === previewTemplate.id)?.spacing.contentPadding || 32}px`, - fontFamily: "Alibaba PuHuiTi, sans-serif", + className="w-full h-full rounded-full border border-black/10 dark:border-white/10 shadow-sm" + style={{ backgroundColor: color.value }} + /> + ) : ( +
+ + Tpl + +
+ )} + + ))} +
+
+ +
+ {DEFAULT_TEMPLATES.map((template, index) => { + const templateKey = getTemplateKey(template.id); + return ( + setPreviewTemplate(template.id)} + onUseTemplate={() => handleCreateResume(template.id)} + previewLabel={t("preview")} + useTemplateLabel={t("useTemplate")} + /> + ); + })} +
+ + { + if (!open) setPreviewTemplate(null); + }} + > + {activePreviewTemplate && ( + +
+
+ + {t(`${getTemplateKey(activePreviewTemplate.id)}.name`)} + +
+
+
+
+ +
+
+
+
+
+ {t("useTemplate")} +
-
- -
-
-
- )} -
-
+ + )} + +
);