mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: enhance template selection with preview functionality and update localization
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { motion } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
import classic from "@/assets/images/template-cover/classic.png";
|
||||
import modern from "@/assets/images/template-cover/modern.png";
|
||||
@@ -16,11 +18,11 @@ import timeline from "@/assets/images/template-cover/timeline.png";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DEFAULT_TEMPLATES } from "@/config";
|
||||
|
||||
const templateImages: { [key: string]: any } = {
|
||||
const templateImages: Record<string, StaticImageData> = {
|
||||
classic,
|
||||
modern,
|
||||
"left-right": leftRight,
|
||||
timeline
|
||||
timeline,
|
||||
};
|
||||
|
||||
const container = {
|
||||
@@ -28,20 +30,24 @@ const container = {
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0 }
|
||||
show: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
export default function TemplatesPage() {
|
||||
const TemplatesPage = () => {
|
||||
const t = useTranslations("dashboard.templates");
|
||||
const router = useRouter();
|
||||
const createResume = useResumeStore((state) => state.createResume);
|
||||
const [previewTemplate, setPreviewTemplate] = useState<{
|
||||
id: string;
|
||||
open: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const handleCreateResume = (templateId: string) => {
|
||||
const template = DEFAULT_TEMPLATES.find((t) => t.id === templateId);
|
||||
@@ -58,12 +64,12 @@ export default function TemplatesPage() {
|
||||
themeColor: template.colorScheme.primary,
|
||||
sectionSpacing: template.spacing.sectionGap,
|
||||
paragraphSpacing: template.spacing.itemGap,
|
||||
pagePadding: template.spacing.contentPadding
|
||||
pagePadding: template.spacing.contentPadding,
|
||||
},
|
||||
basic: {
|
||||
...resume.basic,
|
||||
layout: template.basic.layout
|
||||
}
|
||||
layout: template.basic.layout,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +87,7 @@ export default function TemplatesPage() {
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
{DEFAULT_TEMPLATES.map((template) => {
|
||||
const templateKey =
|
||||
@@ -90,34 +96,59 @@ export default function TemplatesPage() {
|
||||
<motion.div key={template.id} variants={item}>
|
||||
<Card
|
||||
className={cn(
|
||||
"group cursor-pointer overflow-hidden transition-all hover:shadow-lg max-w-[280px] mx-auto",
|
||||
"border-2 hover:border-primary"
|
||||
"group cursor-pointer overflow-hidden transition-all hover:shadow-md max-w-[300px] mx-auto",
|
||||
"border border-gray-200 hover:border-primary/40 dark:border-gray-800 rounded-xl"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="relative aspect-[210/297] w-full overflow-hidden rounded-lg bg-gray-100">
|
||||
<Image
|
||||
src={templateImages[template.id]}
|
||||
alt={t(`${templateKey}.name`)}
|
||||
fill
|
||||
className="object-contain transition-transform duration-300 group-hover:scale-105 p-1"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
||||
<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 p-3 transition-all duration-300 group-hover:scale-[1.01]">
|
||||
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-sm">
|
||||
<Image
|
||||
src={templateImages[template.id]}
|
||||
alt={t(`${templateKey}.name`)}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
priority
|
||||
/>
|
||||
</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">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t(`${templateKey}.name`)}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-200 mt-1">
|
||||
<p className="text-sm text-gray-100 leading-snug">
|
||||
{t(`${templateKey}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
|
||||
<div className="p-3 bg-white dark:bg-gray-950 flex space-x-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleCreateResume(template.id)}
|
||||
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>
|
||||
@@ -128,7 +159,63 @@ export default function TemplatesPage() {
|
||||
);
|
||||
})}
|
||||
</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 p-4">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={templateImages[previewTemplate.id]}
|
||||
alt={t(
|
||||
`${
|
||||
previewTemplate.id === "left-right"
|
||||
? "leftRight"
|
||||
: previewTemplate.id
|
||||
}.name`
|
||||
)}
|
||||
width={600}
|
||||
height={848}
|
||||
className="rounded-md shadow-sm"
|
||||
priority
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TemplatesPage;
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"templates": {
|
||||
"title": "Templates",
|
||||
"useTemplate": "Use Template",
|
||||
"preview": "Preview",
|
||||
"switchTemplate": "Switch Template",
|
||||
"classic": {
|
||||
"name": "Classic",
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"templates": {
|
||||
"title": "模板",
|
||||
"useTemplate": "使用此模板",
|
||||
"preview": "预览",
|
||||
"switchTemplate": "切换模版",
|
||||
"classic": {
|
||||
"name": "经典模板",
|
||||
|
||||
Reference in New Issue
Block a user