From bbc34c1b127155e2fc9a0cd96e4362a3c4f8d562 Mon Sep 17 00:00:00 2001 From: BinarySandia04 Date: Mon, 8 Jun 2026 19:36:45 +0200 Subject: [PATCH] A lot of AI slop --- AGENTS.md | 112 ++++ frontend/app/assets/css/colors.scss | 26 +- frontend/app/assets/css/main.scss | 112 +++- frontend/app/components/Container.vue | 158 +++-- frontend/app/components/Footer.vue | 10 +- frontend/app/components/parts/HeaderLinks.vue | 128 +++- .../app/components/parts/MinimalHeader.vue | 55 +- frontend/app/components/parts/PageHeader.vue | 63 +- .../app/components/parts/StickyHeader.vue | 69 +- frontend/app/components/parts/TableHeader.vue | 105 +-- .../parts/site_options/LanguageSelector.vue | 31 +- .../parts/site_options/ThemeSelector.vue | 76 ++- frontend/app/pages/art/[slug].vue | 2 +- frontend/app/pages/art/index.vue | 172 ++++- frontend/app/pages/blog/[slug].vue | 2 +- frontend/app/pages/blog/index.vue | 159 ++++- frontend/app/pages/index.vue | 619 ++++++++++++++++-- frontend/i18n/locales/ca.json | 10 +- frontend/i18n/locales/en.json | 11 +- frontend/i18n/locales/es.json | 10 +- 20 files changed, 1606 insertions(+), 324 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9058d2d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# aranroig.com + +Monorepo for aranroig.com — a personal portfolio website with blog, art gallery, and project showcase. + +## Stack + +- **Frontend**: Nuxt 4 + Vue 3 + TypeScript + SCSS +- **Backend**: Express 5 + Mongoose (MongoDB) +- **i18n**: @nuxtjs/i18n (English, Spanish, Catalan) +- **Content**: Nuxt Content v3 with Zod-sourced Markdown collections +- **Image optimization**: @nuxt/image +- **Build**: Vite +- **Deployment**: Docker Compose + Nginx reverse proxy + Gitea Actions CI/CD +- **Process management**: PM2 (production) + +## Project Structure + +``` +├── backend/ # Express API server (port 5000) +│ └── src/ +│ ├── index.js # Entry point, single /api/test endpoint +│ └── db.js # MongoDB connection via Mongoose +├── frontend/ # Nuxt application +│ ├── app/ +│ │ ├── assets/css/ # Global SCSS: colors.scss (themes), fonts.scss, main.scss +│ │ ├── components/ # Vue components (TUI-styled) +│ │ │ ├── layouts/ # Page layout wrappers +│ │ │ ├── parts/ # Header, footer, navigation pieces +│ │ │ └── content/ # Blog, art content components +│ │ ├── composables/ # useApi(), theme.ts (light/dark + accent colors) +│ │ ├── i18n.config.ts# VueI18n fallback locale setup +│ │ └── pages/ # File-based routing (index, blog, art, contact) +│ ├── content/ # Markdown collections organized by locale +│ │ ├── fixed/ # Static pages per locale +│ │ ├── blog/ # Blog posts +│ │ ├── art/ # Art pieces +│ │ └── projects/ # Project cards +│ ├── i18n/locales/ # en.json, es.json, ca.json translation files +│ ├── content.config.ts # Content collection schemas (Zod) +│ └── nuxt.config.ts # Nuxt config: modules, i18n, runtime config +├── docker-compose.yml # Services: nginx, frontend, backend +├── nginx.conf # Reverse proxy routing /api/ → backend:5000 +├── ecosystem.config.js # PM2 production deployment config +└── .gitea/workflows/ # Gitea Actions CI/CD (deploy on master push) +``` + +## Commands + +From repo root: + +| Command | Description | +|---------|-------------| +| `npm run dev` | Run frontend + backend concurrently with logging prefixes `[BACKEND]` and `[FRONTEND]` | +| `npm install` | Install monorepo dependencies (triggers `frontend/postinstall`) | + +In `frontend/`: + +| Command | Description | +|---------|-------------| +| `npm run dev` | Nuxt dev server with live reload | +| `npm run build` | Production build with `.env.production` | +| `npm run generate` | Static site generation | +| `npm run preview` | Preview built production output | + +In `backend/`: + +| Command | Description | +|---------|-------------| +| `npm run dev` | Nodemon on `src/index.js` (auto-restart, port 5000) | + +## Development + +- Use the monorepo root script: `npm run dev` — runs both frontend and backend in parallel. +- Frontend uses the `~/` alias mapping to `frontend/app/`. Auto-imported composables include `useAsyncData`, `useI18n`, `useRoute`, `useLocalePath`, etc. +- Backend reads `.env.development` or `.env.production` based on `NODE_ENV`. Requires `DB_URI` for MongoDB. +- Frontend runtime config key `public.apiBaseUrl` sets the backend API URL used by `useApi()`. +- Content lives in `frontend/content/` under collection folders (`blog`, `fixed`, `art`, `projects`), each with locale subdirectories (`en`, `es`, `ca`). + +## Code Style + +- Vue SFCs use ` + diff --git a/frontend/app/components/Footer.vue b/frontend/app/components/Footer.vue index 07b649e..23198d3 100644 --- a/frontend/app/components/Footer.vue +++ b/frontend/app/components/Footer.vue @@ -1,5 +1,8 @@ + + \ No newline at end of file + + diff --git a/frontend/app/components/parts/HeaderLinks.vue b/frontend/app/components/parts/HeaderLinks.vue index 91c73da..4f5b15e 100644 --- a/frontend/app/components/parts/HeaderLinks.vue +++ b/frontend/app/components/parts/HeaderLinks.vue @@ -1,33 +1,131 @@ \ No newline at end of file + +@media screen and (max-width: 900px) { + .tui-tab { + padding: 3px 8px; + font-size: 0.75rem; + + .tab-sep { + margin-right: 4px; + } + } +} + +@media screen and (max-width: 600px) { + .tui-tab { + padding: 3px 5px; + font-size: 0.7rem; + + .tab-sep { + margin-right: 3px; + } + } +} + + diff --git a/frontend/app/components/parts/MinimalHeader.vue b/frontend/app/components/parts/MinimalHeader.vue index 5f5ab2f..8c8ff3f 100644 --- a/frontend/app/components/parts/MinimalHeader.vue +++ b/frontend/app/components/parts/MinimalHeader.vue @@ -4,46 +4,45 @@ import SiteOptions from './site_options/SiteOptions.vue'; \ No newline at end of file + +.sticky-title { + font-family: 'Hurmit', monospace; + font-size: 0.8rem; + color: var(--color-text); + letter-spacing: 1px; + white-space: nowrap; + + @media screen and (max-width: 900px) { + font-size: 0.75rem; + } +} + + diff --git a/frontend/app/components/parts/PageHeader.vue b/frontend/app/components/parts/PageHeader.vue index d84803b..e0eeca8 100644 --- a/frontend/app/components/parts/PageHeader.vue +++ b/frontend/app/components/parts/PageHeader.vue @@ -2,13 +2,51 @@ import HeaderLinks from './HeaderLinks.vue'; import SiteOptions from './site_options/SiteOptions.vue'; import StickyHeader from './StickyHeader.vue'; + +const asciiLines = [ + "░█▀█░█▀▄░█▀█░█▀█░█▀▄░█▀█░▀█▀░█▀▀", + "░█▀█░█▀▄░█▀█░█░█░█▀▄░█░█░░█░░█░█", + "░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀" +]; + +const revealedLines = ref(0); +let asciiTimer: ReturnType | null = null; +const HAS_ANIMATED_KEY = 'ascii-animated'; + +function startAsciiAnimation() { + if (sessionStorage.getItem(HAS_ANIMATED_KEY)) { + revealedLines.value = asciiLines.length; + return; + } + + const delay = 400; + let count = 0; + + asciiTimer = setInterval(() => { + count++; + revealedLines.value = count; + if (count >= asciiLines.length) { + clearInterval(asciiTimer!); + asciiTimer = null; + sessionStorage.setItem(HAS_ANIMATED_KEY, '1'); + } + }, delay); +} + +onMounted(() => { + startAsciiAnimation(); +}); + +onBeforeUnmount(() => { + if (asciiTimer) clearInterval(asciiTimer); +}); diff --git a/frontend/app/pages/art/index.vue b/frontend/app/pages/art/index.vue index 27f84d4..caa8359 100644 --- a/frontend/app/pages/art/index.vue +++ b/frontend/app/pages/art/index.vue @@ -1,8 +1,9 @@ \ No newline at end of file + +.show-more-toggle { + margin-top: 16px; + padding-left: 14px; + + button { + background: none; + border: none; + color: var(--color-link); + font-family: 'Hurmit', monospace; + font-size: 0.85rem; + cursor: pointer; + text-shadow: 0 0 4px var(--color-link); + padding: 0; + + &:hover { + text-shadow: 0 0 10px var(--color-link), 0 0 3px var(--color-link); + } + } +} + +@media (max-width: 900px) { + .grid { + grid-template-columns: repeat(2, minmax(200px, 1fr)); + } + + .selector { + height: 200px; + } +} + +@media (max-width: 640px) { + .section-title { + font-size: 1rem; + } + + .grid { + grid-template-columns: repeat(1, minmax(200px, 1fr)); + padding: 16px 0; + } + + .selector { + height: 180px; + } +} + diff --git a/frontend/app/pages/blog/[slug].vue b/frontend/app/pages/blog/[slug].vue index 291d851..63b1082 100644 --- a/frontend/app/pages/blog/[slug].vue +++ b/frontend/app/pages/blog/[slug].vue @@ -15,7 +15,7 @@ const { data: post } = await useAsyncData(`blog-${slug}`, () => - +
diff --git a/frontend/app/pages/blog/index.vue b/frontend/app/pages/blog/index.vue index 531d88e..93762bd 100644 --- a/frontend/app/pages/blog/index.vue +++ b/frontend/app/pages/blog/index.vue @@ -1,8 +1,10 @@ \ No newline at end of file + +.tui-list { + list-style: none; + margin: 0; + padding: 0; +} + +.blog-entry { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px 16px; + border-left: 2px solid var(--color-border-color); + margin-bottom: 4px; + transition: all 0.1s steps(2, end); + + &:hover { + border-left-color: var(--color-link); + background-color: var(--color-hover); + } + + &::before { + display: none; + } +} + +.entry-link { + text-decoration: none; + color: var(--color-text); + + &:hover { + color: var(--color-link); + text-shadow: 0 0 6px var(--color-link); + } +} + +.entry-title { + font-family: 'Hurmit', monospace; + font-size: 1rem; + transition: all 0.1s steps(2, end); +} + +.entry-meta { + font-family: 'Hurmit', monospace; + color: var(--color-text); + font-size: 0.8rem; + opacity: 0.6; +} + +.show-more-toggle { + margin-top: 12px; + padding-left: 14px; + + button { + background: none; + border: none; + color: var(--color-link); + font-family: 'Hurmit', monospace; + font-size: 0.85rem; + cursor: pointer; + text-shadow: 0 0 4px var(--color-link); + padding: 0; + + &:hover { + text-shadow: 0 0 10px var(--color-link), 0 0 3px var(--color-link); + } + } +} + +@keyframes blink-cursor { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +@media screen and (max-width: 600px) { + .section-title { + font-size: 1rem; + } + + .blog-entry { + padding: 6px 12px; + } + + .entry-title { + font-size: 0.9rem; + } +} + diff --git a/frontend/app/pages/index.vue b/frontend/app/pages/index.vue index 5b065bd..392190e 100644 --- a/frontend/app/pages/index.vue +++ b/frontend/app/pages/index.vue @@ -7,7 +7,9 @@ const { get, post } = api(); const { locale } = useI18n(); const { t } = useI18n(); -// Move useAsyncData to top level — NOT inside onMounted +const currentPath = computed(() => route.path); +const route = useRoute(); + const { data: markdown } = await useAsyncData(`fixed-root`, async () => await queryCollection(`fixed`).path(`/fixed/${locale.value}/root`).first() ,{watch: [locale]}) @@ -17,6 +19,66 @@ const { data: projects } = await useAsyncData(`projects`, async () => .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()), { watch: [locale] }) +const { data: posts } = await useAsyncData('blog-posts', + () => queryCollection(`blog`).where('path', 'LIKE', `/blog/${locale.value}/%`).order('date', 'DESC').all(), + { watch: [locale] }); + +const { data: contactMarkdown } = await useAsyncData(`fixed-contact`, async () => + await queryCollection(`fixed`).path(`/fixed/${locale.value}/contact`).first() + ,{watch: [locale]}) + +const localePath = useLocalePath(); + +const artPosts = useState('art-posts', () => null as any); +if (!artPosts.value?.length) { + const currentLocale = locale.value; + (async () => { + try { + if (currentLocale === 'en') { + artPosts.value = await queryCollection('art') + .where('path', 'LIKE', `/art/en/%`) + .order('date', 'DESC') + .all(); + } else { + const enPosts = await queryCollection('art') + .where('path', 'LIKE', `/art/en/%`) + .order('date', 'DESC') + .all(); + const localePosts = await queryCollection('art') + .where('path', 'LIKE', `/art/${currentLocale}/%`) + .order('date', 'DESC') + .all(); + const translatedSlugs = new Set( + localePosts.map((p) => p.path.replace(`/art/${currentLocale}/`, '')) + ); + const enFallbacks = enPosts.filter( + (p) => !translatedSlugs.has(p.path.replace('/art/en/', '')) + ); + artPosts.value = [...localePosts, ...enFallbacks].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + } + } catch (e) { + console.error('Error loading art posts:', e); + } + })(); +} + +const isArtFallback = (art) => art.path.startsWith('/art/en/') && locale.value !== 'en'; + +const HOME_BLOG_LIMIT = 3; +const HOME_ART_LIMIT = 6; + +const displayedBlogPosts = computed(() => { + const all = posts.value || []; + return all.slice(0, HOME_BLOG_LIMIT); +}); + +const displayedArtPosts = computed(() => { + const all = artPosts.value || []; + return all.slice(0, HOME_ART_LIMIT); +}); + onMounted(async () => { try { const response = await get('/test'); @@ -24,17 +86,46 @@ onMounted(async () => { console.error('API Error:', error); } }); + +function scrollToTop() { + window.scrollTo({ top: 0, behavior: 'smooth' }); +} + +function scrollToSection(e: Event, targetId: string) { + e.preventDefault(); + const el = document.querySelector(targetId); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else { + window.location.hash = targetId.substring(1); + } +} + +const sectionTargets = { + index: '#top', + blog: '#scroll-blog', + contact: '#scroll-contact', + art: '#scroll-art' +} + \ No newline at end of file + diff --git a/frontend/i18n/locales/ca.json b/frontend/i18n/locales/ca.json index 912c9d9..e275ffe 100644 --- a/frontend/i18n/locales/ca.json +++ b/frontend/i18n/locales/ca.json @@ -28,15 +28,21 @@ } }, "pages": { + "intro_heading": "INTRO", + "contact_heading": "Contacte", "projects_heading": "Projectes", + "blog_heading": "Entrades de Blog", + "art_heading": "Galeria d'Art", "stats_heading": "Estad\u00edstiques i Info", "stat_lines_of_code": "L\u00ednies de Codi Escrites", "stat_projects": "Projectes Completats", "stat_coffee": "Tasses de Caf\u00e8 Consumides", "stat_board_games": "Jocs de Taula Propis", "stat_years_programming": "Anys Programant", - "stat_deployments": "Desplegaments a Producci\u00f3" + "stat_deployments": "Desplegaments a Producci\u00f3", + "show_more": "mostrar m\u00e9s...", + "show_less": "mostrar menys" }, - "prefooter": "Aquesta pàgina web ha sigut construida per un humà.", + "prefooter": "Compilada des de la res at las 00:00:00", "footer": "(C) 2026 Aran Roig. Tots els drets no sé que." } \ No newline at end of file diff --git a/frontend/i18n/locales/en.json b/frontend/i18n/locales/en.json index 04f4fba..cc6bc42 100644 --- a/frontend/i18n/locales/en.json +++ b/frontend/i18n/locales/en.json @@ -28,16 +28,21 @@ } }, "pages": { - "root": "", + "intro_heading": "INTRO", + "contact_heading": "Contact", "projects_heading": "Projects", + "blog_heading": "Blog Entries", + "art_heading": "Art Gallery", "stats_heading": "Stats & Info", "stat_lines_of_code": "Lines of Code Written", "stat_projects": "Projects Completed", "stat_coffee": "Cups of Coffee Consumed", "stat_board_games": "Board Games Owned", "stat_years_programming": "Years Programming", - "stat_deployments": "Deployments to Production" + "stat_deployments": "Deployments to Production", + "show_more": "show more...", + "show_less": "show less" }, - "prefooter": "This webpage has been made by a human.", + "prefooter": "", "footer": "(C) 2026 Aran Roig. All rights whatever." } \ No newline at end of file diff --git a/frontend/i18n/locales/es.json b/frontend/i18n/locales/es.json index 401ffaf..2075093 100644 --- a/frontend/i18n/locales/es.json +++ b/frontend/i18n/locales/es.json @@ -28,15 +28,21 @@ } }, "pages": { + "intro_heading": "INTRODUCCI\u00d3N", + "contact_heading": "Contacto", "projects_heading": "Proyectos", + "blog_heading": "Entradas de Blog", + "art_heading": "Galer\u00eda de Arte", "stats_heading": "Estad\u00edsticas e Info", "stat_lines_of_code": "L\u00edneas de C\u00f3digo Escritas", "stat_projects": "Proyectos Completados", "stat_coffee": "Tazas de Caf\u00e9 Consumidas", "stat_board_games": "Juegos de Mesa Propios", "stat_years_programming": "A\u00f1os Programando", - "stat_deployments": "Desplegues a Producci\u00f3n" + "stat_deployments": "Desplegues a Producci\u00f3n", + "show_more": "mostrar m\u00e1s...", + "show_less": "mostrar menos" }, - "prefooter": "Esta página web ha sido construida por un humano.", + "prefooter": "Compilada desde la nada a las 00:00:00", "footer": "(C) 2026 Aran Roig. Todos los derechos no se que." } \ No newline at end of file