commit 35e2897a734f014382022292e28c90060a1ec148 Author: Yvv Date: Fri Feb 20 12:55:10 2026 +0100 initiation librodrome diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..440e9e3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Admin authentication +NUXT_ADMIN_PASSWORD=changeme +NUXT_ADMIN_SECRET=change-this-to-a-random-secret-at-least-32-chars diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a7f73a --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..25b5821 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Nuxt Minimal Starter + +Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. + +## Setup + +Make sure to install dependencies: + +```bash +# npm +npm install + +# pnpm +pnpm install + +# yarn +yarn install + +# bun +bun install +``` + +## Development Server + +Start the development server on `http://localhost:3000`: + +```bash +# npm +npm run dev + +# pnpm +pnpm dev + +# yarn +yarn dev + +# bun +bun run dev +``` + +## Production + +Build the application for production: + +```bash +# npm +npm run build + +# pnpm +pnpm build + +# yarn +yarn build + +# bun +bun run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview + +# pnpm +pnpm preview + +# yarn +yarn preview + +# bun +bun run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/app/app.config.ts b/app/app.config.ts new file mode 100644 index 0000000..2eeb477 --- /dev/null +++ b/app/app.config.ts @@ -0,0 +1,29 @@ +export default defineAppConfig({ + site: { + name: 'Le Librodrome', + description: 'Une économie du don — enfin concevable. Un livre et 9 chansons, lecture guidée et écoute libre.', + url: 'https://librodrome.org', + }, + header: { + height: '4rem', + nav: [ + { label: 'Accueil', to: '/' }, + { label: 'Lire', to: '/lire' }, + { label: 'Écouter', to: '/ecouter' }, + { label: 'À propos', to: '/a-propos' }, + ], + }, + footer: { + credits: '© 2026 Le Librodrome — Production collective', + links: [ + { label: 'Mentions légales', to: '/mentions-legales' }, + ], + }, + gratewizard: { + url: '/gratewizard-app/', + popup: { + width: 420, + height: 720, + }, + }, +}) diff --git a/app/app.vue b/app/app.vue new file mode 100644 index 0000000..4ca3e73 --- /dev/null +++ b/app/app.vue @@ -0,0 +1,28 @@ + + + diff --git a/app/assets/css/animations.css b/app/assets/css/animations.css new file mode 100644 index 0000000..4c50d17 --- /dev/null +++ b/app/assets/css/animations.css @@ -0,0 +1,166 @@ +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(24px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in-down { + from { + opacity: 0; + transform: translateY(-24px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slide-in-left { + from { + opacity: 0; + transform: translateX(-24px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slide-in-right { + from { + opacity: 0; + transform: translateX(24px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes glow-pulse { + 0%, 100% { + box-shadow: 0 0 8px hsl(12 76% 48% / 0.3); + } + 50% { + box-shadow: 0 0 24px hsl(12 76% 48% / 0.6); + } +} + +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes bounce-subtle { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +/* Utility classes for animations */ +.animate-fade-in { + animation: fade-in 0.5s var(--ease-out-expo) both; +} + +.animate-fade-in-up { + animation: fade-in-up 0.6s var(--ease-out-expo) both; +} + +.animate-fade-in-down { + animation: fade-in-down 0.6s var(--ease-out-expo) both; +} + +.animate-slide-in-left { + animation: slide-in-left 0.5s var(--ease-out-expo) both; +} + +.animate-slide-in-right { + animation: slide-in-right 0.5s var(--ease-out-expo) both; +} + +.animate-scale-in { + animation: scale-in 0.4s var(--ease-out-expo) both; +} + +.animate-glow-pulse { + animation: glow-pulse 2s ease-in-out infinite; +} + +/* Staggered animation delays */ +.stagger-1 { animation-delay: 0.1s; } +.stagger-2 { animation-delay: 0.2s; } +.stagger-3 { animation-delay: 0.3s; } +.stagger-4 { animation-delay: 0.4s; } +.stagger-5 { animation-delay: 0.5s; } + +/* Scroll reveal base state */ +.scroll-reveal { + opacity: 0; + transform: translateY(24px); + transition: opacity 0.6s var(--ease-out-expo), + transform 0.6s var(--ease-out-expo); +} + +.scroll-reveal.is-visible { + opacity: 1; + transform: translateY(0); +} + +/* Book Player 3D animations */ +@keyframes fade-scale-in { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes book-open { + from { + transform: rotateY(0deg); + } + to { + transform: rotateY(-180deg); + } +} + +@keyframes page-turn { + 0% { + transform: rotateY(0deg); + } + 100% { + transform: rotateY(-180deg); + } +} + +.animate-fade-scale-in { + animation: fade-scale-in 0.4s cubic-bezier(0.645, 0.045, 0.355, 1) both; +} diff --git a/app/assets/css/fonts.css b/app/assets/css/fonts.css new file mode 100644 index 0000000..6bad2cc --- /dev/null +++ b/app/assets/css/fonts.css @@ -0,0 +1,14 @@ +/* Font declarations are loaded via Bunny Fonts CDN in nuxt.config.ts */ +/* This file provides fallback and utility classes */ + +.font-display { + font-family: 'Syne', system-ui, sans-serif; +} + +.font-sans { + font-family: 'Space Grotesk', system-ui, sans-serif; +} + +.font-mono { + font-family: 'JetBrains Mono', ui-monospace, monospace; +} diff --git a/app/assets/css/main.css b/app/assets/css/main.css new file mode 100644 index 0000000..8d30d5e --- /dev/null +++ b/app/assets/css/main.css @@ -0,0 +1,90 @@ +@import './fonts.css'; +@import './animations.css'; +@import './typography.css'; + +:root { + --color-primary: 12 76% 48%; + --color-accent: 36 80% 52%; + --color-bg: 20 8% 3.5%; + --color-surface: 20 8% 8%; + --color-surface-light: 20 8% 13%; + --color-text: 0 0% 100%; + --color-text-muted: 0 0% 100% / 0.6; + + --header-height: 4rem; + --player-height: 5rem; + --sidebar-width: 280px; + + --font-display: 'Syne', sans-serif; + --font-sans: 'Space Grotesk', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out-expo: cubic-bezier(0.87, 0, 0.13, 1); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: var(--font-sans); + background-color: hsl(var(--color-bg)); + color: hsl(var(--color-text)); + color-scheme: dark; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + min-height: 100dvh; +} + +::selection { + background-color: hsl(var(--color-primary) / 0.3); + color: white; +} + +:focus-visible { + outline: 2px solid hsl(var(--color-primary)); + outline-offset: 2px; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: hsl(var(--color-text) / 0.15); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--color-text) / 0.25); +} + +/* Page transitions */ +.page-enter-active, +.page-leave-active { + transition: opacity 300ms var(--ease-out-expo), + transform 300ms var(--ease-out-expo); +} + +.page-enter-from { + opacity: 0; + transform: translateY(8px); +} + +.page-leave-to { + opacity: 0; + transform: translateY(-8px); +} diff --git a/app/assets/css/typography.css b/app/assets/css/typography.css new file mode 100644 index 0000000..501bc74 --- /dev/null +++ b/app/assets/css/typography.css @@ -0,0 +1,173 @@ +/* Prose styles for book chapter reading */ +.prose { + font-family: var(--font-sans); + font-size: 1.125rem; + line-height: 1.8; + color: hsl(0 0% 100% / 0.90); + max-width: 65ch; +} + +.prose h1 { + font-family: var(--font-display); + font-size: clamp(2rem, 5vw, 2.75rem); + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + color: white; + margin-top: 0; + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid hsl(12 76% 48% / 0.4); +} + +.prose h2 { + font-family: var(--font-display); + font-size: clamp(1.625rem, 4vw, 2.125rem); + font-weight: 700; + line-height: 1.3; + letter-spacing: -0.01em; + color: white; + margin-top: 3.5rem; + margin-bottom: 1rem; + padding-left: 0.75rem; + border-left: 3px solid hsl(12 76% 48% / 0.5); +} + +.prose h3 { + font-family: var(--font-display); + font-size: clamp(1.25rem, 3vw, 1.625rem); + font-weight: 600; + line-height: 1.4; + color: hsl(0 0% 100% / 0.92); + margin-top: 3rem; + margin-bottom: 0.75rem; +} + +.prose h3::before { + content: ''; + display: inline-block; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: hsl(36 80% 52%); + margin-right: 0.625rem; + vertical-align: middle; + position: relative; + top: -0.1em; +} + +.prose h4 { + font-family: var(--font-display); + font-size: clamp(1.065rem, 2.5vw, 1.25rem); + font-weight: 600; + line-height: 1.45; + color: hsl(0 0% 100% / 0.85); + margin-top: 2.5rem; + margin-bottom: 0.625rem; +} + +.prose h4::before { + content: '//'; + font-family: var(--font-mono); + color: hsl(36 80% 52%); + margin-right: 0.5rem; + font-weight: 500; +} + +/* Lede paragraph — first p after H2/H3 */ +.prose h2 + p, +.prose h3 + p { + font-size: 1.175rem; + color: hsl(0 0% 100% / 0.75); + line-height: 1.85; +} + +.prose p { + margin-top: 0; + margin-bottom: 1.5rem; +} + +.prose a { + color: hsl(12 76% 68%); + text-decoration: underline; + text-decoration-color: hsl(12 76% 58% / 0.3); + text-underline-offset: 3px; + transition: text-decoration-color 0.2s; +} + +.prose a:hover { + text-decoration-color: hsl(12 76% 58%); +} + +.prose blockquote { + margin: 2rem 0; + padding: 1rem 1.5rem; + border-left: 3px solid hsl(12 76% 58%); + background: hsl(240 10% 8%); + border-radius: 0 0.5rem 0.5rem 0; + font-style: italic; + color: hsl(0 0% 100% / 0.75); +} + +.prose blockquote p:last-child { + margin-bottom: 0; +} + +.prose code { + font-family: var(--font-mono); + font-size: 0.875em; + background: hsl(240 10% 12%); + padding: 0.2em 0.4em; + border-radius: 0.25rem; + color: hsl(31 97% 66%); +} + +.prose pre { + margin: 2rem 0; + padding: 1.5rem; + background: hsl(240 10% 6%); + border: 1px solid hsl(0 0% 100% / 0.08); + border-radius: 0.75rem; + overflow-x: auto; +} + +.prose pre code { + background: none; + padding: 0; + color: hsl(0 0% 100% / 0.87); +} + +.prose ul, +.prose ol { + margin-top: 0; + margin-bottom: 1.5rem; + padding-left: 1.5rem; +} + +.prose li { + margin-bottom: 0.5rem; +} + +.prose li::marker { + color: hsl(12 76% 58%); +} + +.prose hr { + margin: 3rem 0; + border: none; + border-top: 1px solid hsl(0 0% 100% / 0.1); +} + +.prose strong { + color: white; + font-weight: 600; +} + +.prose em { + color: hsl(0 0% 100% / 0.9); +} + +.prose img { + border-radius: 0.75rem; + margin: 2rem 0; +} diff --git a/app/components/admin/AdminFieldList.vue b/app/components/admin/AdminFieldList.vue new file mode 100644 index 0000000..93706cf --- /dev/null +++ b/app/components/admin/AdminFieldList.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/app/components/admin/AdminFieldText.vue b/app/components/admin/AdminFieldText.vue new file mode 100644 index 0000000..5d3ee62 --- /dev/null +++ b/app/components/admin/AdminFieldText.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/app/components/admin/AdminFieldTextarea.vue b/app/components/admin/AdminFieldTextarea.vue new file mode 100644 index 0000000..6d209b8 --- /dev/null +++ b/app/components/admin/AdminFieldTextarea.vue @@ -0,0 +1,60 @@ +