From ee18cc2d9016f3a93cf01ecb801bdfe4ea6b5fc7 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Fri, 31 Jul 2026 14:49:19 +0200 Subject: [PATCH] feat(frontend): add design tokens, dark mode hook, and i18n scaffold Part of the Ombrora Convert redesign (Task 1/5). --- frontend/package-lock.json | 158 +++++++++++++++++++++++- frontend/package.json | 6 +- frontend/src/components/ThemeToggle.jsx | 10 ++ frontend/src/hooks/useTheme.js | 27 ++++ frontend/src/i18n.js | 16 +++ frontend/src/index.css | 138 ++++++++------------- frontend/src/locales/en.json | 47 +++++++ frontend/src/locales/fr.json | 47 +++++++ 8 files changed, 359 insertions(+), 90 deletions(-) create mode 100644 frontend/src/components/ThemeToggle.jsx create mode 100644 frontend/src/hooks/useTheme.js create mode 100644 frontend/src/i18n.js create mode 100644 frontend/src/locales/en.json create mode 100644 frontend/src/locales/fr.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9474f85..d9411d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,12 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@phosphor-icons/react": "^2.1.10", + "i18next": "^26.3.6", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-i18next": "^17.0.11", + "react-router-dom": "^7.18.2" }, "devDependencies": { "@types/react": "^19.2.17", @@ -19,6 +23,15 @@ "vite": "^8.1.1" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -432,6 +445,19 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -771,6 +797,19 @@ } } }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -821,6 +860,43 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/html-parse-stringify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", + "license": "MIT", + "funding": { + "url": "https://locize.com" + } + }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -1232,6 +1308,71 @@ "react": "^19.2.8" } }, + "node_modules/react-i18next": { + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz", + "integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^4.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -1272,6 +1413,12 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1307,6 +1454,15 @@ "license": "0BSD", "optional": true }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index f34c1da..053a332 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@phosphor-icons/react": "^2.1.10", + "i18next": "^26.3.6", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-i18next": "^17.0.11", + "react-router-dom": "^7.18.2" }, "devDependencies": { "@types/react": "^19.2.17", diff --git a/frontend/src/components/ThemeToggle.jsx b/frontend/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..5a3af36 --- /dev/null +++ b/frontend/src/components/ThemeToggle.jsx @@ -0,0 +1,10 @@ +import { Moon, Sun } from '@phosphor-icons/react'; + +export function ThemeToggle({ theme, onToggle, label }) { + const isDark = theme === 'dark'; + return ( + + ); +} diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js new file mode 100644 index 0000000..cf569da --- /dev/null +++ b/frontend/src/hooks/useTheme.js @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +const STORAGE_KEY = 'theme'; + +function getInitialTheme() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark') return stored; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +export function useTheme() { + const [theme, setTheme] = useState(getInitialTheme); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); + + function toggleTheme() { + setTheme((current) => { + const next = current === 'dark' ? 'light' : 'dark'; + localStorage.setItem(STORAGE_KEY, next); + return next; + }); + } + + return { theme, toggleTheme }; +} diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js new file mode 100644 index 0000000..6004413 --- /dev/null +++ b/frontend/src/i18n.js @@ -0,0 +1,16 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import fr from './locales/fr.json'; +import en from './locales/en.json'; + +i18n.use(initReactI18next).init({ + resources: { + fr: { translation: fr }, + en: { translation: en }, + }, + lng: 'fr', + fallbackLng: 'fr', + interpolation: { escapeValue: false }, +}); + +export default i18n; diff --git a/frontend/src/index.css b/frontend/src/index.css index 2c84af0..35aae58 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,111 +1,73 @@ +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap'); + :root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + --color-primary: #2563eb; + --color-on-primary: #ffffff; + --color-secondary: #3b82f6; + --color-accent: #d97706; + --color-background: #f8fafc; + --color-foreground: #0f172a; + --color-muted: #f1f5fd; + --color-border: #e4ecfc; + --color-destructive: #dc2626; + --color-ring: #2563eb; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; + --font-sans: 'Plus Jakarta Sans', system-ui, 'Segoe UI', Roboto, sans-serif; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } + font: 16px/1.5 var(--font-sans); } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } +:root[data-theme='dark'] { + --color-primary: #3b82f6; + --color-on-primary: #0f172a; + --color-secondary: #60a5fa; + --color-accent: #f59e0b; + --color-background: #0f172a; + --color-foreground: #f1f5f9; + --color-muted: #1e293b; + --color-border: #334155; + --color-destructive: #f87171; + --color-ring: #3b82f6; +} - #social .button-icon { - filter: invert(1) brightness(2); - } +*, *::before, *::after { + box-sizing: border-box; } body { margin: 0; + background: var(--color-background); + color: var(--color-foreground); + transition: background-color 200ms ease, color 200ms ease; } #root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); min-height: 100svh; display: flex; flex-direction: column; - box-sizing: border-box; } -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); +a { + color: var(--color-primary); } -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; +button, input, select { + font-family: inherit; +} + +button { + cursor: pointer; +} + +:focus-visible { + outline: 2px solid var(--color-ring); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + transition: none !important; + animation: none !important; } } -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json new file mode 100644 index 0000000..b92e0d9 --- /dev/null +++ b/frontend/src/locales/en.json @@ -0,0 +1,47 @@ +{ + "brand": "Ombrora Convert", + "nav": { + "switchToEnglish": "English", + "switchToFrench": "Français", + "themeToLight": "Switch to light mode", + "themeToDark": "Switch to dark mode" + }, + "hero": { + "title": "Convert your files online, for free", + "subtitle": "Drag and drop your files, pick the output format, download the result. No install, no account.", + "dropzoneLabel": "Drag your files here or click to browse", + "convert": "Convert", + "unsupportedFormat": "Unsupported format" + }, + "quality": { + "label": "Quality ({{value}})", + "compression": "Compression ({{value}})", + "iconSize": "Icon size", + "compressPdf": "Compress as JPEG" + }, + "job": { + "converting": "Converting...", + "download": "Download" + }, + "reassurance": { + "title": "Why you can trust us", + "autoDelete": "Files deleted automatically after 1h", + "noAccount": "No account required", + "online": "100% online — nothing to install", + "maxSize": "Up to 100MB per file" + }, + "formats": { + "title": "Supported formats", + "images": "Images", + "documents": "Documents", + "fonts": "Fonts", + "ebooks": "Ebooks" + }, + "footer": { + "rights": "Ombrora Convert — Online file conversion" + }, + "notFound": { + "title": "Page not found", + "backHome": "Back to home" + } +} diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json new file mode 100644 index 0000000..1225e56 --- /dev/null +++ b/frontend/src/locales/fr.json @@ -0,0 +1,47 @@ +{ + "brand": "Ombrora Convert", + "nav": { + "switchToEnglish": "English", + "switchToFrench": "Français", + "themeToLight": "Activer le mode clair", + "themeToDark": "Activer le mode sombre" + }, + "hero": { + "title": "Convertissez vos fichiers en ligne, gratuitement", + "subtitle": "Glissez-déposez vos fichiers, choisissez le format de sortie, téléchargez le résultat. Aucune installation, aucun compte.", + "dropzoneLabel": "Glissez vos fichiers ici ou cliquez pour parcourir", + "convert": "Convertir", + "unsupportedFormat": "Format non supporté" + }, + "quality": { + "label": "Qualité ({{value}})", + "compression": "Compression ({{value}})", + "iconSize": "Taille de l'icône", + "compressPdf": "Compresser en JPEG" + }, + "job": { + "converting": "Conversion en cours...", + "download": "Télécharger" + }, + "reassurance": { + "title": "Pourquoi nous faire confiance", + "autoDelete": "Fichiers supprimés automatiquement après 1h", + "noAccount": "Aucun compte requis", + "online": "100% en ligne — rien à installer", + "maxSize": "Jusqu'à 100 Mo par fichier" + }, + "formats": { + "title": "Formats pris en charge", + "images": "Images", + "documents": "Documents", + "fonts": "Polices", + "ebooks": "Ebooks" + }, + "footer": { + "rights": "Ombrora Convert — Conversion de fichiers en ligne" + }, + "notFound": { + "title": "Page introuvable", + "backHome": "Retour à l'accueil" + } +}