Refactor Dockerfile, update README, and enhance Calculator component

- Refactored Dockerfile for improved multi-stage builds and added development and production configurations.
- Updated README with clearer project description, local development instructions, and added contact information.
- Enhanced Calculator component to manage theme and history using localStorage, improving user experience.
- Added new pages for legal mentions and privacy policy, including relevant metadata.
- Updated docker-compose.yml for better service management and added environment variables.
- Introduced a new LICENSE file outlining usage rights and responsibilities.
This commit is contained in:
Puechberty Arthur
2026-04-01 23:30:51 +02:00
parent 572f29c442
commit 8400e26c0a
9 changed files with 318 additions and 162 deletions
+16 -24
View File
@@ -1,34 +1,26 @@
# === Étape 1 : Build ===
FROM node:20-alpine AS builder
FROM node:22-alpine AS calculatrice-base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# Copier les fichiers de dépendances pour profiter du cache Docker
COPY package*.json ./
# Installer uniquement ce qu'il faut pour le build
FROM calculatrice-base AS calculatrice-deps
COPY package.json package-lock.json ./
RUN npm ci
# Copier tout le code
FROM calculatrice-deps AS calculatrice-dev
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "--port", "3000"]
# Build Next.js pour la production
FROM calculatrice-deps AS calculatrice-builder
COPY . .
RUN npm run build
# === Étape 2 : Runner léger ===
FROM node:20-alpine AS runner
WORKDIR /app
# Copier uniquement ce qui est nécessaire pour la prod
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
# Mode production
FROM calculatrice-base AS calculatrice-runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
COPY --from=calculatrice-builder /app/.next/standalone ./
COPY --from=calculatrice-builder /app/.next/static ./.next/static
COPY --from=calculatrice-builder /app/public ./public
EXPOSE 3000
# Lancer le serveur Next.js
CMD ["npm", "start"]
CMD ["node", "server.js"]
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2026 Arthur
Licence pour tous les projets Arthur
1. Définition
Cette licence définit les droits et obligations concernant l'utilisation, la modification et la redistribution du code fourni par l'auteur.
2. Autorisation d'utilisation
Vous êtes libre d'utiliser ce code pour vos projets personnels ou commerciaux. L'utilisation doit inclure une mention de l'auteur dune manière libre (ex: "inspiré de ArthurP").
3. Modification
Vous pouvez modifier, adapter ou améliorer le code pour vos besoins. Les modifications doivent être identifiées comme telles et ne doivent pas être présentées comme l'original.
4. Redistribution
- Le code original **ne peut pas être redistribué tel quel**.
- Les versions modifiées peuvent être partagées, sous réserve de mentionner l'auteur original.
5. Usage commercial
Lusage commercial des versions modifiées est autorisé. Vous pouvez générer des revenus avec votre version modifiée.
6. Attribution
L'auteur original doit être cité dune manière libre, mais visible, sur tout projet utilisant ce code ou ses dérivés.
7. Responsabilité
Le code est fourni "tel quel", sans garantie daucune sorte. Lauteur décline toute responsabilité pour tout dommage direct ou indirect résultant de lutilisation du code.
+24 -22
View File
@@ -1,33 +1,32 @@
# Calculatrice
Application web de calculatrice réalisée avec Next.js.
Calculatrice web (simple + scientifique) construite avec Next.js.
## Site en ligne
## Liens
- Projet: https://calculatrice.arthurp.fr
- Site: https://calculatrice.arthurp.fr
- Site principal: https://arthurp.fr
- Contact: https://contact.arthurp.fr
- Email: contact@arthurp.fr
## Objectif
Proposer une calculatrice simple, rapide et responsive, utilisable sur desktop et mobile.
## Stack technique
## Stack
- Next.js 16
- React 19
- TypeScript
## Lancement en local
## Developpement local
Prerequis: Node.js 20+
Prerequis: Node.js 22+
```bash
npm install
npm ci
npm run dev
```
Application accessible sur http://localhost:3000
Application: http://localhost:3000
## Scripts utiles
## Scripts
```bash
npm run dev
@@ -36,21 +35,24 @@ npm run start
npm run lint
```
## Deploiement
## Docker
Build de production:
Mode dev:
```bash
npm run build
docker compose --profile dev up --build
```
Ensuite deployer sur votre plateforme cible (Vercel, VPS, etc.).
Mode production:
## Backlinks
```bash
docker compose --profile prod up --build -d
```
- Calculatrice en ligne: https://calculatrice.arthurp.fr
- Site principal: https://arthurp.fr
- Dev: http://localhost:3000
- Prod: http://localhost:3014
## Licence
## Pages legales
Projet personnel.
- /mentions-legales
- /politique-de-confidentialite
+142 -107
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { useState, useEffect, useCallback, useRef } from "react";
/* ══════════════════════════════════════════════
@@ -151,39 +152,37 @@ export default function Calculator() {
const [error, setError] = useState(""); // Message d'erreur
const [steps, setSteps] = useState<string[]>([]); // Étapes de calcul
const [showSteps, setShowSteps] = useState(false); // Afficher les étapes
const [history, setHistory] = useState<HistoryEntry[]>([]); // Historique
const [history, setHistory] = useState<HistoryEntry[]>(() => {
if (typeof window === "undefined") return [];
try {
const savedHistory = localStorage.getItem(HISTORY_KEY);
return savedHistory ? JSON.parse(savedHistory) : [];
} catch {
return [];
}
}); // Historique
const [showHistory, setShowHistory] = useState(false); // Panneau historique visible
const [isScientific, setIsScientific] = useState(false); // Mode scientifique
const [isDark, setIsDark] = useState(false); // Thème sombre
const [isDark, setIsDark] = useState(() => {
if (typeof window === "undefined") return false;
try {
const savedTheme = localStorage.getItem(THEME_KEY);
if (savedTheme === "dark") return true;
if (savedTheme === "light") return false;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
} catch {
return false;
}
}); // Thème sombre
const [copied, setCopied] = useState(false); // Feedback copie
const [lastKey, setLastKey] = useState(""); // Dernière touche pressée (feedback visuel)
const displayRef = useRef<HTMLDivElement>(null);
const simpleRef = useRef<HTMLButtonElement>(null);
const sciRef = useRef<HTMLButtonElement>(null);
// ── Chargement initial depuis localStorage ──
// ── Synchroniser la classe de thème avec l'état courant ──
useEffect(() => {
try {
const savedHistory = localStorage.getItem(HISTORY_KEY);
if (savedHistory) setHistory(JSON.parse(savedHistory));
const savedTheme = localStorage.getItem(THEME_KEY);
if (savedTheme === "dark") {
setIsDark(true);
document.documentElement.classList.add("dark");
} else if (savedTheme === "light") {
setIsDark(false);
document.documentElement.classList.remove("dark");
} else {
// Détecter la préférence système
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setIsDark(prefersDark);
}
} catch {
// localStorage non disponible
}
}, []);
document.documentElement.classList.toggle("dark", isDark);
}, [isDark]);
// ── Sauvegarder l'historique dans localStorage ──
useEffect(() => {
@@ -430,7 +429,7 @@ export default function Calculator() {
════════════════════════════════════════════ */
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 sm:p-8 transition-colors duration-300">
<div className="min-h-screen flex flex-col items-center p-4 sm:p-8 transition-colors duration-300">
{/* Lien d'accessibilité : aller au contenu principal */}
<a
href="#calculatrice"
@@ -463,23 +462,16 @@ export default function Calculator() {
<div
className="mode-slider"
style={{
left: isScientific
? (simpleRef.current?.offsetWidth ?? 80) + 2 + "px"
: "2px",
width: isScientific
? (sciRef.current?.offsetWidth ?? 100) + "px"
: (simpleRef.current?.offsetWidth ?? 80) + "px",
transform: isScientific ? "translateX(100%)" : "translateX(0)",
}}
/>
<button
ref={simpleRef}
className={!isScientific ? "active" : ""}
onClick={() => setIsScientific(false)}
>
Simple
</button>
<button
ref={sciRef}
className={isScientific ? "active" : ""}
onClick={() => setIsScientific(true)}
>
@@ -747,102 +739,145 @@ export default function Calculator() {
</div>
)}
{/* ── Footer SEO avec contenu sémantique ── */}
<footer className="mt-12 w-full max-w-2xl animate-fade-in" style={{ animationDelay: "0.3s" }}>
{/* Contenu textuel riche pour le SEO */}
<article className="mb-8 px-4">
<h2 className="text-xl font-bold mb-3" style={{ color: "var(--foreground)" }}>
Calculatrice en ligne gratuite
{/* ── Contenu SEO ── */}
<section className="mt-10 w-full max-w-3xl space-y-5" aria-labelledby="seo-content-heading">
<h2 id="seo-content-heading" className="text-xl font-bold" style={{ color: "var(--foreground)" }}>
Calculatrice en ligne simple et scientifique
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Notre calculatrice en ligne gratuite vous permet d&apos;effectuer tous vos calculs
directement dans votre navigateur, sans installation ni inscription. Que vous ayez
besoin d&apos;une simple addition ou d&apos;un calcul scientifique complexe avec des
fonctions trigonométriques, notre outil s&apos;adapte à vos besoins.
<p className="text-sm leading-relaxed" style={{ color: "var(--muted)" }}>
Cette calculatrice en ligne gratuite permet de faire vos opérations du quotidien et vos calculs plus avances,
directement depuis votre navigateur. Aucun compte, aucune installation et une interface rapide sur mobile comme
sur ordinateur.
</p>
<p className="text-sm leading-relaxed" style={{ color: "var(--muted)" }}>
Le mode simple couvre addition, soustraction, multiplication et division. Le mode scientifique ajoute sin, cos,
tan, logarithmes, racine carree, puissances, factorielle et constantes pi / e. L&apos;historique est conserve
localement pour retrouver vos derniers calculs.
</p>
<h2 className="text-lg font-semibold mb-2" style={{ color: "var(--foreground)" }}>
Mode simple : les opérations essentielles
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Le mode simple couvre les quatre opérations fondamentales : addition (+),
soustraction (), multiplication (×) et division (÷). Vous pouvez utiliser
des parenthèses pour structurer vos expressions et obtenir des résultats
précis. La gestion des erreurs vous avertit automatiquement en cas de
division par zéro.
</p>
<h2 className="text-lg font-semibold mb-2" style={{ color: "var(--foreground)" }}>
Mode scientifique : des fonctions avancées
</h2>
<p className="text-sm leading-relaxed mb-4" style={{ color: "var(--muted)" }}>
Basculez en mode scientifique pour accéder aux fonctions trigonométriques
(sinus, cosinus, tangente), aux logarithmes (log décimal, logarithme naturel),
à la racine carrée, aux puissances, à la factorielle, ainsi qu&apos;aux
constantes mathématiques π et e. Idéal pour les étudiants, ingénieurs et
professionnels.
</p>
</article>
{/* Section FAQ pour le SEO */}
<section className="mb-8 px-4" aria-labelledby="faq-heading">
<h2 id="faq-heading" className="text-lg font-semibold mb-3" style={{ color: "var(--foreground)" }}>
Questions fréquentes
</h2>
<div className="space-y-3">
<div className="rounded-xl border p-4" style={{ borderColor: "var(--border-color)", background: "var(--surface)" }}>
<h3 className="text-base font-semibold" style={{ color: "var(--foreground)" }}>
Questions frequentes
</h3>
<div className="mt-3 space-y-2 text-sm" style={{ color: "var(--muted)" }}>
{[
{
q: "Comment utiliser la calculatrice en ligne ?",
a: "Cliquez sur les boutons ou utilisez votre clavier pour saisir une expression mathématique, puis appuyez sur « = » ou Entrée pour obtenir le résultat.",
q: "Comment faire un calcul rapidement ?",
a: "Saisissez votre expression puis appuyez sur Entree ou sur le bouton =.",
},
{
q: "Quelles fonctions scientifiques sont disponibles ?",
a: "Sinus, cosinus, tangente, logarithme décimal, logarithme naturel, racine carrée, puissances, factorielle, et les constantes π et e.",
q: "Puis-je utiliser le clavier ?",
a: "Oui. Les chiffres et operateurs sont pris en charge. Entree calcule, Echap efface et Retour arriere supprime le dernier caractere.",
},
{
q: "L'historique des calculs est-il sauvegardé ?",
a: "Oui, vos 50 derniers calculs sont sauvegardés automatiquement dans votre navigateur et persistent même après fermeture de la page.",
q: "A quoi sert le mode scientifique ?",
a: "Il ajoute les fonctions avancees: sin, cos, tan, log, ln, racine carree, puissances et factorielle.",
},
{
q: "La calculatrice est-elle vraiment gratuite ?",
a: "Oui, 100 % gratuite, sans publicité et sans inscription. Utilisez-la autant que vous le souhaitez.",
q: "Est-ce que l'historique est conserve ?",
a: "Oui, les 50 derniers calculs sont stockes localement dans votre navigateur.",
},
{
q: "Puis-je utiliser des raccourcis clavier ?",
a: "Oui ! Chiffres et opérateurs au clavier, Entrée pour calculer, Échap pour effacer, Retour arrière pour supprimer, Ctrl+C pour copier.",
q: "Puis-je reutiliser un ancien calcul ?",
a: "Oui, ouvrez l'historique puis cliquez sur une ligne pour recharger l'expression et le resultat.",
},
].map((faq, i) => (
{
q: "La calculatrice fonctionne-t-elle sur mobile ?",
a: "Oui, l'interface est adaptee aux ecrans mobiles et ordinateurs.",
},
{
q: "Dois-je creer un compte ?",
a: "Non, aucun compte n'est necessaire pour utiliser la calculatrice.",
},
{
q: "Comment contacter le proprietaire du site ?",
a: "Via contact.arthurp.fr ou par email a contact@arthurp.fr.",
},
].map((item, i) => (
<details
key={i}
className="rounded-lg overflow-hidden transition-all"
style={{
background: "var(--surface)",
border: "1px solid var(--border-color)",
}}
className="rounded-lg border px-3 py-2"
style={{ borderColor: "var(--border-color)", background: "var(--background)" }}
>
<summary
className="px-4 py-3 cursor-pointer text-sm font-medium select-none hover:bg-[var(--surface-hover)] transition-colors"
style={{ color: "var(--foreground)" }}
>
{faq.q}
<summary className="cursor-pointer font-medium" style={{ color: "var(--foreground)" }}>
{item.q}
</summary>
<p
className="px-4 pb-3 text-sm leading-relaxed"
style={{ color: "var(--muted)" }}
>
{faq.a}
</p>
<p className="mt-2 leading-relaxed">{item.a}</p>
</details>
))}
</div>
</div>
</section>
{/* Copyright */}
<div className="text-center text-xs pb-4" style={{ color: "var(--muted)" }}>
<p>© {new Date().getFullYear()} Calculatrice en ligne gratuite Simple &amp; Scientifique</p>
<p className="mt-1 opacity-60">
Outil de calcul en ligne rapide, gratuit et sans inscription.
</p>
{/* ── Footer normal en bas de page ── */}
<footer className="mt-auto w-full px-3 pt-10 pb-3">
<div
className="mx-auto w-full max-w-5xl rounded-2xl border p-4 sm:p-6 backdrop-blur-xl"
style={{
background: "color-mix(in srgb, var(--surface) 88%, transparent)",
borderColor: "var(--border-color)",
boxShadow: "var(--shadow-lg)",
}}
>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<section>
<h2 className="text-sm font-semibold tracking-wide uppercase" style={{ color: "var(--foreground)" }}>
Navigation
</h2>
<nav className="mt-3 flex flex-col gap-2 text-sm" aria-label="Navigation du site">
<Link href="/" className="hover:underline" style={{ color: "var(--muted)" }}>Accueil</Link>
<a href="https://arthurp.fr/projets" target="_blank" rel="noreferrer" className="hover:underline" style={{ color: "var(--muted)" }}>Projets</a>
<a href="https://contact.arthurp.fr" target="_blank" rel="noreferrer" className="hover:underline" style={{ color: "var(--muted)" }}>Contact</a>
</nav>
</section>
<section>
<h2 className="text-sm font-semibold tracking-wide uppercase" style={{ color: "var(--foreground)" }}>
Liens
</h2>
<div className="mt-3 flex flex-col gap-2 text-sm">
<a href="https://arthurp.fr" target="_blank" rel="noreferrer" className="hover:underline" style={{ color: "var(--muted)" }}>
arthurp.fr
</a>
<a href="https://github.com/arthur-pbty" target="_blank" rel="noreferrer" className="hover:underline" style={{ color: "var(--muted)" }}>
GitHub
</a>
</div>
</section>
<section>
<h2 className="text-sm font-semibold tracking-wide uppercase" style={{ color: "var(--foreground)" }}>
Légal
</h2>
<div className="mt-3 flex flex-col gap-2 text-sm">
<Link href="/mentions-legales" className="hover:underline" style={{ color: "var(--muted)" }}>
Mentions légales
</Link>
<Link href="/politique-de-confidentialite" className="hover:underline" style={{ color: "var(--muted)" }}>
Politique de confidentialité
</Link>
</div>
</section>
<section>
<h2 className="text-sm font-semibold tracking-wide uppercase" style={{ color: "var(--foreground)" }}>
Contact
</h2>
<div className="mt-3 flex flex-col gap-2 text-sm" style={{ color: "var(--muted)" }}>
<a href="https://contact.arthurp.fr" target="_blank" rel="noreferrer" className="hover:underline">
contact.arthurp.fr
</a>
<a href="mailto:contact@arthurp.fr" className="hover:underline">
contact@arthurp.fr
</a>
<p className="text-xs opacity-80">Fait avec et auto-hébergé sur Proxmox.</p>
</div>
</section>
</div>
<div className="mt-6 border-t pt-3 text-center text-xs" style={{ color: "var(--muted)", borderColor: "var(--border-color)" }}>
© {new Date().getFullYear()} Arthur P. Tous droits réservés.
</div>
</div>
</footer>
</div>
+6 -1
View File
@@ -285,6 +285,7 @@ body {
.mode-toggle {
position: relative;
display: flex;
min-width: 210px;
background: var(--surface);
border: 1px solid var(--border-color);
border-radius: 10px;
@@ -292,9 +293,11 @@ body {
}
.mode-toggle button {
flex: 1;
position: relative;
z-index: 1;
padding: 6px 16px;
text-align: center;
font-size: 0.85rem;
font-weight: 500;
color: var(--muted);
@@ -311,10 +314,12 @@ body {
.mode-slider {
position: absolute;
top: 2px;
left: 2px;
bottom: 2px;
width: calc(50% - 2px);
border-radius: 8px;
background: var(--primary);
transition: left 0.3s ease, width 0.3s ease;
transition: transform 0.3s ease;
}
/* ══════════════════════════════════════════════
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Mentions legales",
description: "Mentions legales du site calculatrice.arthurp.fr",
};
export default function MentionsLegalesPage() {
return (
<main className="min-h-screen px-4 py-12 sm:px-8">
<div className="mx-auto w-full max-w-3xl rounded-2xl border p-6 sm:p-8" style={{ background: "var(--surface)", borderColor: "var(--border-color)" }}>
<h1 className="text-2xl font-bold sm:text-3xl">Mentions legales</h1>
<section className="mt-6 space-y-3 text-sm leading-relaxed" style={{ color: "var(--muted)" }}>
<p>
Site: calculatrice.arthurp.fr
</p>
<p>
Proprietaire et editeur: Arthur P.
</p>
<p>
Contact principal: <a href="https://contact.arthurp.fr" className="hover:underline">contact.arthurp.fr</a>
</p>
<p>
Email: <a href="mailto:contact@arthurp.fr" className="hover:underline">contact@arthurp.fr</a>
</p>
<p>
Hebergement: infrastructure auto-hebergee sur Proxmox.
</p>
</section>
</div>
</main>
);
}
+31
View File
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Politique de confidentialite",
description: "Politique de confidentialite du site calculatrice.arthurp.fr",
};
export default function PolitiqueConfidentialitePage() {
return (
<main className="min-h-screen px-4 py-12 sm:px-8">
<div className="mx-auto w-full max-w-3xl rounded-2xl border p-6 sm:p-8" style={{ background: "var(--surface)", borderColor: "var(--border-color)" }}>
<h1 className="text-2xl font-bold sm:text-3xl">Politique de confidentialite</h1>
<section className="mt-6 space-y-3 text-sm leading-relaxed" style={{ color: "var(--muted)" }}>
<p>
Cette application ne requiert pas de compte et ne collecte pas de donnees personnelles cote serveur pour son fonctionnement courant.
</p>
<p>
L&apos;historique de calcul est stocke localement dans votre navigateur (localStorage) pour ameliorer l&apos;experience utilisateur.
</p>
<p>
Vous pouvez supprimer ces donnees a tout moment depuis le bouton &quot;Tout effacer&quot; dans l&apos;historique ou via les reglages de votre navigateur.
</p>
<p>
Pour toute demande, vous pouvez utiliser <a href="https://contact.arthurp.fr" className="hover:underline">contact.arthurp.fr</a> ou <a href="mailto:contact@arthurp.fr" className="hover:underline">contact@arthurp.fr</a>.
</p>
</section>
</div>
</main>
);
}
+12
View File
@@ -15,5 +15,17 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "monthly",
priority: 1.0,
},
{
url: `${baseUrl}/mentions-legales`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.4,
},
{
url: `${baseUrl}/politique-de-confidentialite`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.4,
},
];
}
+25 -5
View File
@@ -1,11 +1,31 @@
services:
web:
build: .
calculatrice-dev:
profiles: ["dev"]
build:
context: .
target: calculatrice-dev
container_name: calculatrice-dev
ports:
- "3000:3000"
volumes:
- ./:/app
- .:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=development
command: npm run dev
NEXT_TELEMETRY_DISABLED: "1"
NODE_ENV: development
calculatrice-prod:
profiles: ["prod"]
build:
context: .
target: calculatrice-runner
container_name: calculatrice-prod
restart: unless-stopped
ports:
- "3014:3000"
env_file:
- .env
environment:
NEXT_TELEMETRY_DISABLED: "1"
NODE_ENV: production