Skip to content

Commit 95c219c

Browse files
authored
Merge pull request #24 from Dibeo/main
Deploy
2 parents d41e601 + f4bbe58 commit 95c219c

7 files changed

Lines changed: 316 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cv-editor",
33
"private": true,
4-
"version": "1.0.1",
4+
"version": "1.0.2",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

public/lang/en/bar.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"sidebar": {
33
"edit": "Editor",
44
"new": "New",
5+
"exemple": "Sample",
56
"design": "Template's Design",
67
"pdfLanguage": "CV language",
78
"searchTheme": "Search theme",

public/lang/fr/bar.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"sidebar": {
33
"edit": "Editeur",
44
"new": "Nouveau",
5+
"exemple": "Exemple",
56
"design": "Design Template",
67
"pdfLanguage": "Langue du CV",
78
"searchTheme": "Chercher un theme",

src/core/store/useCvStore.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface CvState {
99
data: CVData;
1010
updateData: (newData: Partial<CVData>) => void;
1111
reset: () => void;
12+
exemple: () => void;
1213
}
1314

1415
export const useCvStore = create<CvState>()(
@@ -43,6 +44,29 @@ export const useCvStore = create<CvState>()(
4344
});
4445
}
4546
},
47+
48+
exemple: async () => {
49+
const result = await Swal.fire({
50+
title: "Êtes-vous sûr ?",
51+
text: "Toutes vos données locales seront supprimées définitivement.",
52+
icon: "warning",
53+
showCancelButton: true,
54+
confirmButtonText: "Oui, effacer tout",
55+
cancelButtonText: "Annuler",
56+
});
57+
58+
if (result.isConfirmed) {
59+
set({ data: INITIAL_CV_DATA });
60+
61+
Swal.fire({
62+
title: "Réinitialisé !",
63+
text: "Votre CV est de nouveau vierge.",
64+
icon: "success",
65+
timer: 1500,
66+
showConfirmButton: false,
67+
});
68+
}
69+
},
4670
}),
4771
{
4872
name: "cv-storage",
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { ExternalLink, Globe } from "lucide-react";
2+
import type { CVData } from "../../../core/domain/cv.types";
3+
import {
4+
CONTACT_ICONS,
5+
SOCIAL_ICONS,
6+
type ContactType,
7+
type SocialType,
8+
} from "../../shared/icon";
9+
import { useTranslation } from "react-i18next";
10+
11+
export const BoldTheme = ({ data }: { data: CVData }) => {
12+
const { t } = useTranslation("theme", {
13+
lng: data.metadata.language || "fr",
14+
});
15+
16+
return (
17+
<div
18+
className="flex min-h-[297mm] w-[210mm] bg-white font-sans"
19+
id="cv-to-print">
20+
<div className="w-[62%] flex flex-col">
21+
<header className="bg-amber-400 px-8 pt-8 pb-6 relative overflow-hidden">
22+
<div className="absolute -top-6 -left-6 w-32 h-32 bg-amber-300 rounded-full opacity-50" />
23+
<div className="absolute bottom-0 right-0 w-24 h-24 bg-amber-500 rounded-full opacity-30 translate-x-8 translate-y-8" />
24+
<div className="relative z-10">
25+
<h1 className="text-4xl font-black text-slate-900 leading-none uppercase tracking-tight">
26+
{data.personalInfo.fullName || t("defaultName")}
27+
</h1>
28+
<p className="text-sm font-black text-slate-700 mt-1 uppercase tracking-[0.15em]">
29+
{data.personalInfo.title || t("defaultTitle")}
30+
</p>
31+
</div>
32+
</header>
33+
34+
<div className="flex-1 bg-white px-8 py-6 flex flex-col gap-7">
35+
{data.personalInfo.summary && (
36+
<section>
37+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-500 mb-2">
38+
{t("sections.summary")}
39+
</h2>
40+
<p className="text-[12px] text-slate-600 leading-relaxed border-l-4 border-amber-400 pl-3">
41+
{data.personalInfo.summary}
42+
</p>
43+
</section>
44+
)}
45+
46+
{data.experiences?.length > 0 && (
47+
<section>
48+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-500 mb-4">
49+
{t("sections.experience")}
50+
</h2>
51+
<div className="flex flex-col gap-5">
52+
{data.experiences.map((exp) => (
53+
<article key={exp.id} className="group">
54+
<div className="flex justify-between items-start mb-1">
55+
<div>
56+
<h3 className="text-[14px] font-black uppercase text-slate-900 leading-tight">
57+
{exp.role}
58+
</h3>
59+
<p className="text-[11px] font-black text-amber-500 uppercase tracking-wide mt-0.5">
60+
{exp.company}
61+
</p>
62+
</div>
63+
<span className="text-[10px] font-bold text-white bg-slate-900 px-2 py-0.5 rounded-full shrink-0 ml-2">
64+
{exp.startDate}{exp.endDate}
65+
</span>
66+
</div>
67+
{exp.mission?.length > 0 && (
68+
<ul className="mt-1.5 flex flex-col gap-1">
69+
{exp.mission.map((m, i) => (
70+
<li
71+
key={i}
72+
className="text-[11px] text-slate-600 flex gap-2">
73+
<span className="text-amber-400 font-black shrink-0">
74+
75+
</span>
76+
{m}
77+
</li>
78+
))}
79+
</ul>
80+
)}
81+
</article>
82+
))}
83+
</div>
84+
</section>
85+
)}
86+
87+
{data.educations?.length > 0 && (
88+
<section>
89+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-500 mb-4">
90+
{t("sections.education")}
91+
</h2>
92+
<div className="flex flex-col gap-3">
93+
{data.educations.map((edu) => (
94+
<article
95+
key={edu.id}
96+
className="flex justify-between items-start gap-3">
97+
<div>
98+
<h3 className="text-[12px] font-black uppercase text-slate-900 leading-tight">
99+
{edu.degree}
100+
</h3>
101+
<p className="text-[11px] font-bold text-amber-500 uppercase mt-0.5">
102+
{edu.school}
103+
</p>
104+
</div>
105+
<span className="text-[10px] font-black text-slate-400 shrink-0 italic">
106+
{edu.year}
107+
</span>
108+
</article>
109+
))}
110+
</div>
111+
</section>
112+
)}
113+
114+
{data.certifications?.length > 0 && (
115+
<section>
116+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-500 mb-3">
117+
{t("sections.certifications")}
118+
</h2>
119+
<div className="flex flex-col gap-2">
120+
{data.certifications.map((cert) => (
121+
<article
122+
key={cert.id}
123+
className="flex justify-between items-center bg-amber-50 border border-amber-100 rounded-xl px-3 py-2">
124+
<div>
125+
<p className="text-[11px] font-black uppercase text-slate-900">
126+
{cert.name}
127+
</p>
128+
{cert.issuer && (
129+
<p className="text-[10px] font-bold text-amber-500 uppercase">
130+
{cert.issuer}
131+
</p>
132+
)}
133+
</div>
134+
<span className="text-[10px] font-black text-slate-400 shrink-0">
135+
{cert.year}
136+
</span>
137+
</article>
138+
))}
139+
</div>
140+
</section>
141+
)}
142+
</div>
143+
</div>
144+
145+
<aside className="w-[38%] bg-slate-900 text-white px-6 py-8 flex flex-col gap-7">
146+
{data.personalInfo.photoUrl && (
147+
<div className="flex justify-center">
148+
<img
149+
src={data.personalInfo.photoUrl}
150+
className="w-28 h-28 rounded-2xl object-cover border-4 border-amber-400 shadow-xl"
151+
alt={data.personalInfo.fullName}
152+
/>
153+
</div>
154+
)}
155+
156+
{data.personalInfo.contacts?.length > 0 && (
157+
<section>
158+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-400 mb-3">
159+
Contact
160+
</h2>
161+
<div className="flex flex-col gap-3">
162+
{data.personalInfo.contacts.map((c) => {
163+
const Icon =
164+
CONTACT_ICONS[c.label.toLowerCase() as ContactType] || Globe;
165+
return (
166+
<div key={c.id} className="flex items-center gap-2.5">
167+
<div className="w-7 h-7 rounded-lg bg-amber-400 flex items-center justify-center shrink-0">
168+
<Icon size={13} className="text-slate-900" />
169+
</div>
170+
<div>
171+
<p className="text-[8px] font-black uppercase text-slate-500 leading-none mb-0.5">
172+
{c.label}
173+
</p>
174+
<p className="text-[11px] font-bold text-slate-200 break-all">
175+
{c.value || "—"}
176+
</p>
177+
</div>
178+
</div>
179+
);
180+
})}
181+
</div>
182+
</section>
183+
)}
184+
185+
{data.personalInfo.socials?.length > 0 && (
186+
<section>
187+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-400 mb-3">
188+
{t("sections.socials") || "Réseaux"}
189+
</h2>
190+
<div className="flex flex-col gap-2.5">
191+
{data.personalInfo.socials.map((social) => {
192+
const Icon =
193+
SOCIAL_ICONS[social.platform.toLowerCase() as SocialType] ||
194+
ExternalLink;
195+
return (
196+
<div key={social.id} className="flex items-center gap-2.5">
197+
<div className="w-7 h-7 rounded-lg bg-amber-400 flex items-center justify-center shrink-0">
198+
<Icon size={13} className="text-slate-900" />
199+
</div>
200+
<div className="min-w-0">
201+
<p className="text-[8px] font-black uppercase text-slate-500 leading-none mb-0.5">
202+
{social.platform}
203+
</p>
204+
<a
205+
href={
206+
social.url.startsWith("http")
207+
? social.url
208+
: `https://${social.url}`
209+
}
210+
target="_blank"
211+
rel="noopener noreferrer"
212+
className="text-[11px] font-bold text-slate-200 truncate block hover:text-amber-400 transition-colors">
213+
{social.url.replace(/^https?:\/\/(www\.)?/, "")}
214+
</a>
215+
</div>
216+
</div>
217+
);
218+
})}
219+
</div>
220+
</section>
221+
)}
222+
223+
{data.skills?.length > 0 && (
224+
<section>
225+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-400 mb-3">
226+
{t("sections.skills")}
227+
</h2>
228+
<div className="flex flex-wrap gap-1.5">
229+
{data.skills.map((s, i) => (
230+
<span
231+
key={i}
232+
className="px-2 py-0.5 bg-slate-800 text-slate-300 border border-slate-700 rounded-lg text-[10px] font-bold uppercase">
233+
{s}
234+
</span>
235+
))}
236+
</div>
237+
</section>
238+
)}
239+
240+
{data.languages?.length > 0 && (
241+
<section>
242+
<h2 className="text-[10px] font-black uppercase tracking-[0.25em] text-amber-400 mb-3">
243+
{t("sections.languages")}
244+
</h2>
245+
<div className="flex flex-col gap-2">
246+
{data.languages.map((lang) => (
247+
<div
248+
key={lang.id}
249+
className="flex justify-between items-center">
250+
<span className="text-[11px] font-black uppercase text-slate-200">
251+
{lang.name}
252+
</span>
253+
<span className="text-[10px] font-bold text-amber-400 italic">
254+
{lang.level}
255+
</span>
256+
</div>
257+
))}
258+
</div>
259+
</section>
260+
)}
261+
262+
<div className="mt-auto">
263+
<div className="w-full h-1 bg-amber-400 rounded-full mb-2" />
264+
<div className="w-2/3 h-1 bg-slate-700 rounded-full mb-2" />
265+
<div className="w-1/3 h-1 bg-slate-800 rounded-full" />
266+
</div>
267+
</aside>
268+
</div>
269+
);
270+
};

src/features/cv-preview/themes/ThemeIndex.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { StandardTheme } from "./StandardTheme";
55
import { HipsterTheme } from "./HipsterTheme";
66
import { ElegantTheme } from "./ElegantTheme";
77
import { NeoBentoTheme } from "./NeoBentoTheme";
8+
import { BoldTheme } from "./BoldTheme";
89

910
export const CV_THEMES = {
11+
boldtheme: { component: BoldTheme, label: "Bold" },
1012
classic: { component: ClassicTheme, label: "Classique" },
1113
elegant: { component : ElegantTheme, label : "Elegant" },
1214
hipster: { component : HipsterTheme, label : "Hipster" },

src/features/shared/components/Sidebar.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { LANGUAGES } from "../../../core/config/language";
88

99
export const Sidebar = () => {
1010
const { t } = useTranslation("bar");
11-
const { data, updateData, reset } = useCvStore();
11+
const { data, updateData, reset, exemple } = useCvStore();
1212
const { layout: currentUrlLayout } = useParams();
1313

1414
const [isThemeOpen, setIsThemeOpen] = useState(false);
@@ -85,6 +85,22 @@ export const Sidebar = () => {
8585
</NavLink>
8686
</div>
8787

88+
<div className="space-y-2">
89+
<NavLink
90+
onClick={exemple}
91+
to={`/editor/${activeLayoutId}`}
92+
className={({ isActive }) =>
93+
`flex items-center gap-3 p-3 rounded-lg transition-colors ${
94+
isActive
95+
? "bg-blue-600 text-white"
96+
: "text-slate-400 hover:bg-slate-700 hover:text-white"
97+
}`
98+
}
99+
>
100+
<PlusCircle size={20} /> {t("sidebar.exemple")}
101+
</NavLink>
102+
</div>
103+
88104
<div className="mt-auto space-y-6">
89105
<div className="px-2 relative" ref={langRef}>
90106
<label className="flex items-center gap-2 text-slate-400 text-[10px] font-bold uppercase mb-2">

0 commit comments

Comments
 (0)