mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
style: dashboard tempate card ui
This commit is contained in:
@@ -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<HTMLDivElement>(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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.08 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
"group border transition-all duration-200 aspect-[210/297] flex flex-col overflow-hidden",
|
||||
"hover:border-primary/40 hover:shadow-lg",
|
||||
"dark:hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<CardContent
|
||||
className="p-0 flex-1 relative bg-gray-50 dark:bg-gray-900 overflow-hidden cursor-pointer"
|
||||
onClick={onPreview}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 pb-6 flex items-center justify-center pointer-events-none transition-transform duration-300 group-hover:scale-[1.02] overflow-hidden"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="w-full h-full relative origin-top bg-white">
|
||||
<div
|
||||
className="absolute top-0 left-0 bg-white"
|
||||
style={{
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
padding: `${template.spacing.contentPadding}px`,
|
||||
fontFamily: "Alibaba PuHuiTi, sans-serif",
|
||||
}}
|
||||
>
|
||||
<ResumeTemplateComponent data={previewData} template={template} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 top-[60%] pointer-events-none bg-gradient-to-t from-white via-white/90 to-transparent dark:from-gray-950 dark:via-gray-950/90 z-0" />
|
||||
<div className="absolute inset-x-0 bottom-0 pt-12 pb-3 px-4 flex items-end border-t border-transparent z-10 transition-colors group-hover:bg-white/50 dark:group-hover:bg-gray-950/50">
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="text-[15px] font-semibold truncate text-gray-900 dark:text-gray-100 drop-shadow-sm">
|
||||
{templateName}
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-600 dark:text-gray-300 mt-0.5 font-medium truncate">
|
||||
{templateDescription}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="pt-2 pb-2 px-2 bg-white dark:bg-gray-950 border-t border-gray-100 dark:border-gray-800 z-10">
|
||||
<div className="grid grid-cols-2 gap-2 w-full">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-sm hover:bg-gray-100 dark:border-primary/50 dark:hover:bg-primary/10"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPreview();
|
||||
}}
|
||||
>
|
||||
{previewLabel}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<Button
|
||||
className="w-full text-sm"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUseTemplate();
|
||||
}}
|
||||
>
|
||||
{useTemplateLabel}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
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<string | null>(null);
|
||||
const [selectedColor, setSelectedColor] = useState<string>(PRESET_COLORS[0].value);
|
||||
const autoPlayRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoPlayRef = useRef<ReturnType<typeof setInterval> | 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 (
|
||||
<ScrollArea className="h-[calc(100vh-2rem)] w-full">
|
||||
<div className="w-full max-w-[1600px] mx-auto py-8 px-6 lg:px-8">
|
||||
<div className="w-full max-w-[1600px] mx-auto py-8 px-4 sm:px-6">
|
||||
<div className="flex flex-col space-y-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight">{t("title")}</h2>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-gray-50/50 dark:bg-gray-900/50 p-2 rounded-full border border-gray-100 dark:border-gray-800 backdrop-blur-sm self-start sm:self-auto overflow-x-auto">
|
||||
{PRESET_COLORS.map((color) => (
|
||||
<button
|
||||
key={color.name}
|
||||
onClick={() => handleColorSelect(color.value)}
|
||||
className={cn(
|
||||
"relative w-8 h-8 rounded-full flex items-center justify-center shrink-0 transition-transform hover:scale-110",
|
||||
selectedColor === color.value ? "ring-2 ring-primary ring-offset-2 dark:ring-offset-gray-950 scale-110" : ""
|
||||
)}
|
||||
title={color.name === "default" ? "Default" : color.name}
|
||||
>
|
||||
{color.value ? (
|
||||
<div
|
||||
className="w-full h-full rounded-full border border-black/10 dark:border-white/10 shadow-sm"
|
||||
style={{ backgroundColor: color.value }}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full rounded-full bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 border border-gray-300 dark:border-gray-700 shadow-sm flex items-center justify-center">
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium tracking-tighter">Tpl</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">{t("title")}</h2>
|
||||
|
||||
<motion.div
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6"
|
||||
>
|
||||
{DEFAULT_TEMPLATES.map((template) => {
|
||||
const templateKey =
|
||||
template.id === "left-right" ? "leftRight" : template.id;
|
||||
return (
|
||||
<motion.div key={template.id} variants={item}>
|
||||
<Card
|
||||
<div className="flex items-center space-x-2 bg-gray-50/50 dark:bg-gray-900/50 p-2 rounded-full border border-gray-100 dark:border-gray-800 backdrop-blur-sm self-start sm:self-auto overflow-x-auto">
|
||||
{PRESET_COLORS.map((color) => (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => handleColorSelect(color.value)}
|
||||
className={cn(
|
||||
"group cursor-pointer overflow-hidden transition-all hover:shadow-lg",
|
||||
"border border-gray-200 hover:border-primary/40 dark:border-gray-800 rounded-xl"
|
||||
"relative w-8 h-8 rounded-full flex items-center justify-center shrink-0 transition-transform hover:scale-110",
|
||||
selectedColor === color.value
|
||||
? "ring-2 ring-primary ring-offset-2 dark:ring-offset-gray-950 scale-110"
|
||||
: ""
|
||||
)}
|
||||
title={color.name === "default" ? "Default" : color.name}
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="relative aspect-[210/297] w-full overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div className="absolute top-0 right-0 z-10">
|
||||
<div className="flex items-center px-3 py-1.5 bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm border-l border-b border-gray-200 dark:border-gray-700 rounded-bl-md">
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-200">
|
||||
{t(`${templateKey}.name`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full py-6 transition-all duration-300 group-hover:scale-[1.02] flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
className="relative overflow-hidden bg-white shadow-sm ring-1 ring-gray-200/50 rounded-sm"
|
||||
style={{
|
||||
width: "210px",
|
||||
height: "297px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 bg-white"
|
||||
style={{
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
transform: "scale(0.264583333)", // (210px / 210mm) approximation
|
||||
transformOrigin: "top left",
|
||||
padding: `${template.spacing.contentPadding}px`,
|
||||
fontFamily: "Alibaba PuHuiTi, sans-serif",
|
||||
}}
|
||||
>
|
||||
<ResumeTemplateComponent
|
||||
data={{
|
||||
...baseData,
|
||||
id: "preview-mock-id",
|
||||
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}
|
||||
template={template}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 text-white transform translate-y-full transition-transform duration-300 group-hover:translate-y-0">
|
||||
<p className="text-sm text-gray-100 leading-snug">
|
||||
{t(`${templateKey}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-white dark:bg-gray-950 flex space-x-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewTemplate({ id: template.id, open: true });
|
||||
}}
|
||||
>
|
||||
{t("preview")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 text-sm font-medium shadow-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCreateResume(template.id);
|
||||
}}
|
||||
>
|
||||
{t("useTemplate")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
|
||||
<Dialog
|
||||
open={previewTemplate?.open || false}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPreviewTemplate(null);
|
||||
}}
|
||||
>
|
||||
{previewTemplate && (
|
||||
<DialogContent className="max-w-[680px] p-0 overflow-hidden border-0 shadow-lg rounded-xl bg-white dark:bg-gray-900">
|
||||
<div className="flex flex-col">
|
||||
<div className="border-b border-gray-100 dark:border-gray-800 px-4 py-4">
|
||||
<DialogTitle className="text-lg font-medium">
|
||||
{t(
|
||||
`${
|
||||
previewTemplate.id === "left-right"
|
||||
? "leftRight"
|
||||
: previewTemplate.id
|
||||
}.name`
|
||||
)}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="overflow-hidden flex items-center justify-center bg-gray-50 dark:bg-gray-950 py-8 pointer-events-none">
|
||||
<div
|
||||
className="relative bg-white shadow-md ring-1 ring-gray-200/50 overflow-hidden"
|
||||
style={{
|
||||
width: "420px",
|
||||
height: "594px",
|
||||
}}
|
||||
>
|
||||
{color.value ? (
|
||||
<div
|
||||
className="absolute top-0 left-0 bg-white"
|
||||
style={{
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
transform: "scale(0.529166667)", // (420px / 210mm) approximation
|
||||
transformOrigin: "top left",
|
||||
padding: `${DEFAULT_TEMPLATES.find(t => 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 }}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full rounded-full bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 border border-gray-300 dark:border-gray-700 shadow-sm flex items-center justify-center">
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium tracking-tighter">
|
||||
Tpl
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 sm:gap-6">
|
||||
{DEFAULT_TEMPLATES.map((template, index) => {
|
||||
const templateKey = getTemplateKey(template.id);
|
||||
return (
|
||||
<TemplateCardItem
|
||||
key={template.id}
|
||||
index={index}
|
||||
template={template}
|
||||
templateName={t(`${templateKey}.name`)}
|
||||
templateDescription={t(`${templateKey}.description`)}
|
||||
baseData={baseData}
|
||||
selectedColor={selectedColor}
|
||||
onPreview={() => setPreviewTemplate(template.id)}
|
||||
onUseTemplate={() => handleCreateResume(template.id)}
|
||||
previewLabel={t("preview")}
|
||||
useTemplateLabel={t("useTemplate")}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={!!previewTemplate}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setPreviewTemplate(null);
|
||||
}}
|
||||
>
|
||||
{activePreviewTemplate && (
|
||||
<DialogContent className="max-w-[680px] p-0 overflow-hidden border-0 shadow-lg rounded-xl bg-white dark:bg-gray-900">
|
||||
<div className="flex flex-col">
|
||||
<div className="border-b border-gray-100 dark:border-gray-800 px-4 py-4">
|
||||
<DialogTitle className="text-lg font-medium">
|
||||
{t(`${getTemplateKey(activePreviewTemplate.id)}.name`)}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="overflow-hidden flex items-center justify-center bg-gray-50 dark:bg-gray-950 py-8 pointer-events-none">
|
||||
<div
|
||||
className="relative bg-white shadow-md ring-1 ring-gray-200/50 overflow-hidden"
|
||||
style={{ width: "420px", height: "594px" }}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 bg-white"
|
||||
style={{
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
transform: `scale(${PREVIEW_MODAL_SCALE})`,
|
||||
transformOrigin: "top left",
|
||||
padding: `${activePreviewTemplate.spacing.contentPadding}px`,
|
||||
fontFamily: "Alibaba PuHuiTi, sans-serif",
|
||||
}}
|
||||
>
|
||||
<ResumeTemplateComponent
|
||||
data={buildTemplatePreviewData(
|
||||
baseData,
|
||||
activePreviewTemplate,
|
||||
selectedColor,
|
||||
"preview-mock-id-large"
|
||||
)}
|
||||
template={activePreviewTemplate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 pt-2 border-t border-gray-100 dark:border-gray-800 flex justify-center">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
const templateId = activePreviewTemplate.id;
|
||||
setPreviewTemplate(null);
|
||||
handleCreateResume(templateId);
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const tpl = DEFAULT_TEMPLATES.find(t => t.id === previewTemplate.id) || DEFAULT_TEMPLATES[0];
|
||||
return (
|
||||
<ResumeTemplateComponent
|
||||
data={{
|
||||
...baseData,
|
||||
id: "preview-mock-id-large",
|
||||
templateId: tpl.id,
|
||||
globalSettings: {
|
||||
...baseData.globalSettings,
|
||||
themeColor: selectedColor || tpl.colorScheme.primary,
|
||||
sectionSpacing: tpl.spacing.sectionGap,
|
||||
paragraphSpacing: tpl.spacing.itemGap,
|
||||
pagePadding: tpl.spacing.contentPadding,
|
||||
},
|
||||
basic: {
|
||||
...baseData.basic,
|
||||
layout: tpl.basic.layout,
|
||||
},
|
||||
} as any}
|
||||
template={tpl}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{t("useTemplate")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 pt-2 border-t border-gray-100 dark:border-gray-800 flex justify-center">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setPreviewTemplate(null);
|
||||
handleCreateResume(previewTemplate.id);
|
||||
}}
|
||||
>
|
||||
{t("useTemplate")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user