chore init

This commit is contained in:
devcodetools committed 2026-09-16 21:34:09 +02:00
1 parent 1b7406b1d3
commit a0d312a884
60 files changed
+5046 -125

No files matched your search

+115
View File
@@ -0,0 +1,115 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Documentation — CV-Maker",
description: "Guide de prise en main de CV-Maker et syntaxe Markdown supportée.",
};
export default function DocumentationPage() {
return (
<main className="mx-auto w-full max-w-3xl flex-1 px-6 py-10">
<Link
href="/"
className="text-sm font-medium text-muted underline-offset-2 hover:text-accent hover:underline"
>
← Retour à l&apos;éditeur
</Link>
<h1 className="mt-4 text-2xl font-bold text-ink">Documentation</h1>
<section className="mt-6">
<h2 className="text-xl font-semibold text-ink">Prise en main</h2>
<ol className="mt-3 list-decimal space-y-2 pl-5 text-sm leading-relaxed text-ink">
<li>
<strong>Onglets 1 à 5</strong> — sélectionnez une zone de saisie (accroche,
état civil + contacts, formations, soft skills, centres d&apos;intérêt).
Un seul onglet est actif à la fois ; chaque zone conserve son texte.
</li>
<li>
<strong>Saisie Markdown</strong> — le texte est écrit en Markdown dans le
panneau gauche et rendu en HTML en temps réel dans la feuille A4 à droite.
</li>
<li>
<strong>Expériences professionnelles</strong> — bouton « Ajouter » pour créer
une expérience (titre + contenu). La liste est numérotée ; chaque entrée peut
être modifiée ou supprimée. L&apos;ordre de saisie détermine l&apos;affichage : la
première expérience ajoutée apparaît en haut de la section « Expériences
professionnelles » de la feuille, les suivantes en dessous.
</li>
<li>
<strong>Sauvegarde automatique</strong> — chaque frappe est enregistrée dans le
navigateur (localStorage, clé <code>cv-maker:v1</code>). Rechargez la page :
vos données sont restaurées. Aucune donnée n&apos;est envoyée sur Internet.
</li>
<li>
<strong>Impression / PDF</strong> — le bouton « Imprimer / PDF » ouvre
l&apos;aperçu d&apos;impression du navigateur. Enregistrez en PDF : seule la
feuille A4 est exportée, sans les panneaux d&apos;édition.
</li>
<li>
<strong>Export / import JSON</strong> — le bouton « Exporter JSON »
télécharge votre CV au format JSON (fichier <code>cv-maker.json</code>).
Sur un autre appareil, « Importer JSON » charge ce fichier et remplace
le contenu actuel (après confirmation). C&apos;est le moyen de partager ou
de sauvegarder un CV entre plusieurs navigateurs.
</li>
<li>
<strong>Réinitialiser</strong> — le bouton « Réinitialiser » (après
confirmation) vide toutes les zones et la liste des expériences.
</li>
</ol>
</section>
<section className="mt-8">
<h2 className="text-xl font-semibold text-ink">Syntaxe Markdown supportée</h2>
<p className="mt-2 text-sm leading-relaxed text-muted">
Les 5 zones et le contenu des expériences sont rendus en Markdown (GFM).
</p>
<div className="mt-4 overflow-x-auto rounded-md border border-border bg-white">
<table className="w-full text-sm text-ink">
<thead>
<tr className="border-b border-border bg-paper text-left">
<th className="px-3 py-2 font-semibold">Élément</th>
<th className="px-3 py-2 font-semibold">Syntaxe</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{[
["Titre niveau 3", "### Développeur Full-Stack"],
["Gras", "**Jean Dupont**"],
["Italique", "*Contact*"],
["Liste à puces", "- Compétence 1\n- Compétence 2"],
["Liste numérotée", "1. Diplôme 1\n2. Diplôme 2"],
["Lien", "[GitHub](https://github.com/user)"],
["Lien brut", "<https://example.com>"],
["Code en ligne", "`npm run dev`"],
["Bloc de code", "```\nconst x = 42;\n```"],
["Citation", "> Citation d'accroche"],
["Séparateur", "---"],
].map(([label, syntax]) => (
<tr key={label}>
<td className="px-3 py-2 font-medium">{label}</td>
<td className="px-3 py-2 font-mono text-xs whitespace-pre">
{syntax}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mt-8 rounded-md border border-border bg-white p-4">
<h2 className="text-sm font-semibold text-ink">Conseil : structurer son CV</h2>
<p className="mt-2 text-sm leading-relaxed text-ink">
Utilisez l&apos;onglet 1 pour le nom et le poste visé, l&apos;onglet 2 pour vos
coordonnées (e-mail, téléphone, site web, GitHub), l&apos;onglet 3 pour vos
formations, l&apos;onglet 4 pour vos soft skills et l&apos;onglet 5 pour vos
centres d&apos;intérêt. Complétez avec vos expériences professionnelles, de la plus
récente à la plus ancienne.
</p>
</section>
</main>
);
}
+130 -20
View File
@@ -1,26 +1,136 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
@theme {
--color-surface: #ffffff;
--color-paper: #f1f5f9;
--color-ink: #0f172a;
--color-muted: #64748b;
--color-accent: #2563eb;
--color-danger: #dc2626;
--color-border: #e2e8f0;
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
background: var(--color-paper);
color: var(--color-ink);
}
/* Tailwind v4 ne force plus cursor:pointer sur les boutons ; on le restaure */
/* (le curseur « not-allowed » des boutons désactivés reste prioritaire) */
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
/* ── Feuille A4 (preview) ─────────────────────────────────────── */
.cv-sheet {
width: 210mm;
max-width: 100%;
min-height: 297mm;
background: var(--color-surface);
box-shadow: 0 10px 30px rgb(15 23 42 / 0.15);
border: 1px solid var(--color-border);
padding: 16mm;
}
.cv-sheet-body {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 12mm;
}
/* ── Rendu Markdown du CV ─────────────────────────────────────── */
.cv-md {
line-height: 1.45;
}
.cv-md p {
margin: 0.25em 0;
}
.cv-md h1,
.cv-md h2,
.cv-md h3,
.cv-md h4 {
font-weight: 700;
line-height: 1.2;
margin: 0.35em 0 0.25em;
}
.cv-md h1 {
font-size: 1.1rem;
}
.cv-md h2 {
font-size: 1rem;
}
.cv-md h3 {
font-size: 0.9rem;
}
.cv-md h4 {
font-size: 0.85rem;
}
.cv-md ul,
.cv-md ol {
margin: 0.25em 0;
padding-left: 1.2em;
}
.cv-md li {
margin: 0.1em 0;
}
.cv-md a {
color: var(--color-accent);
text-decoration: underline;
}
.cv-md code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--color-paper);
padding: 0.1em 0.3em;
border-radius: 0.25em;
}
.cv-md pre {
background: var(--color-paper);
padding: 0.6em;
border-radius: 0.375em;
overflow-x: auto;
white-space: pre-wrap;
}
.cv-md blockquote {
border-left: 3px solid var(--color-border);
padding-left: 0.75em;
margin: 0.25em 0;
color: var(--color-muted);
}
/* ── Impression PDF ───────────────────────────────────────────── */
@page {
size: A4;
margin: 0;
}
@media print {
body {
background: #ffffff;
}
.print-hidden {
display: none !important;
}
.cv-sheet {
box-shadow: none;
border: 0;
margin: 0;
width: auto;
height: auto;
padding: 16mm;
}
}
+23 -6
View File
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { SerwistProvider } from "@serwist/next/react";
import { Footer } from "@/components/ui/Footer";
import "./globals.css";
const geistSans = Geist({
@@ -13,17 +15,32 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "CV-Maker — Créez votre CV",
description:
"Créez votre CV en Markdown avec aperçu temps réel au format A4, 100 % local dans votre navigateur.",
};
export const viewport: Viewport = {
themeColor: "#2563eb",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
lang="fr"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full flex flex-col bg-paper text-ink">
<SerwistProvider
swUrl="/sw.js"
disable={process.env.NODE_ENV === "development"}
>
<div className="flex flex-1 flex-col">
{children}
<Footer />
</div>
</SerwistProvider>
</body>
</html>
);
}
}
+25
View File
@@ -0,0 +1,25 @@
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "CV-Maker",
short_name: "CV-Maker",
description: "Créez votre CV en Markdown avec aperçu temps réel au format A4.",
start_url: "/",
display: "standalone",
background_color: "#f1f5f9",
theme_color: "#2563eb",
icons: [
{
src: "/icons/icon-192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icons/icon-512.png",
sizes: "512x512",
type: "image/png",
},
],
};
}
+115
View File
@@ -0,0 +1,115 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Mentions légales — CV-Maker",
description:
"Mentions légales de l'application CV-Maker : éditeur, données traitées, stockage et droits.",
};
export default function MentionsLegalesPage() {
return (
<main className="mx-auto w-full max-w-3xl flex-1 px-6 py-10">
<Link
href="/"
className="text-sm font-medium text-muted underline-offset-2 hover:text-accent hover:underline"
>
← Retour à l&apos;éditeur
</Link>
<h1 className="mt-4 text-2xl font-bold text-ink">Mentions légales</h1>
<div className="mt-6 space-y-6 text-sm leading-relaxed text-ink">
<section>
<h2 className="text-base font-semibold text-ink">Éditeur</h2>
<p className="mt-2">
L&apos;utilisateur de l&apos;application est le seul responsable et
destinataire des données saisies. Aucune entité tiers ne traite ces
données.
</p>
</section>
<section>
<h2 className="text-base font-semibold text-ink">Données traitées</h2>
<p className="mt-2">
Les zones de saisie (message d&apos;accroche, état civil, contacts,
formations, soft skills, centres d&apos;intérêt) et les expériences
professionnelles contiennent des informations que vous choisissez de
saisir : nom, prénom, adresse, e-mail, téléphone, site web, GitHub,
parcours professionnel et formations.
</p>
</section>
<section>
<h2 className="text-base font-semibold text-ink">Finalité</h2>
<p className="mt-2">
Les données sont utilisées exclusivement à la production d&apos;un CV,
destiné à être imprimé ou exporté en PDF par l&apos;utilisateur.
</p>
</section>
<section>
<h2 className="text-base font-semibold text-ink">Stockage</h2>
<p className="mt-2">
Les données sont stockées dans le{" "}
<code className="rounded bg-paper px-1.5 py-0.5 font-mono text-xs">
localStorage
</code>{" "}
du navigateur, sous la clé{" "}
<code className="rounded bg-paper px-1.5 py-0.5 font-mono text-xs">
cv-maker:v1
</code>
. Aucune donnée n&apos;est envoyée à un serveur. Le stockage est
entièrement local à votre navigateur.
</p>
</section>
<section>
<h2 className="text-base font-semibold text-ink">Durée de conservation</h2>
<p className="mt-2">
Les données sont conservées jusqu&apos;à leur effacement par
l&apos;utilisateur (via le bouton « Réinitialiser » dans
l&apos;application, ou en vidant le cache de votre navigateur).
</p>
</section>
<section>
<h2 className="text-base font-semibold text-ink">
Droits de la personne concernée
</h2>
<p className="mt-2">
En tant que seul responsable des données, vous disposez d&apos;un accès
complet à l&apos;ensemble des informations saisies. Vous pouvez les
modifier à tout moment depuis l&apos;éditeur, ou les supprimer en
utilisant :
</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>
Le bouton <strong>« Réinitialiser »</strong> dans la barre d&apos;outils
(après confirmation).
</li>
<li>
Les outils de votre navigateur : paramètres → vie privée → vider les
données de ce site pour cette application.
</li>
</ul>
</section>
<section>
<h2 className="text-base font-semibold text-ink">Sécurité</h2>
<p className="mt-2">
Le contenu Markdown saisi est assaini avant rendu via{" "}
<strong>DOMPurify</strong> pour neutraliser tout script ou contenu
dangereux (protection contre les attaques XSS). Aucune donnée
n&apos;est transmise sur le réseau ; l&apos;application fonctionne
entièrement hors ligne une fois chargée.
</p>
</section>
<p className="text-xs text-muted">
Dernière mise à jour : 16 septembre 2026
</p>
</div>
</main>
);
}
+4 -68
View File
@@ -1,69 +1,5 @@
import Image from "next/image";
import { CvWorkspace } from "@/components/cv/CvWorkspace";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}
export default function HomePage() {
return <CvWorkspace />;
}
+24
View File
@@ -0,0 +1,24 @@
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { Serwist } from "serwist";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { defaultCache } from "@serwist/next/worker";
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}
declare const self: ServiceWorkerGlobalScope;
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
serwist.addEventListeners();