fix: update portfolio branding to "Killian' DAL-CIN" across all documentation and components

- Corrected the name in various files including CLAUDE.md, README.md, and configuration files to reflect the updated branding.
- Ensured consistency in the use of the new name throughout the project, enhancing brand identity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 19:54:46 +02:00
parent 9779e4e133
commit c8dac9ac88
45 changed files with 750 additions and 665 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Portfolio Killian Dalcin — Migration Nuxt 4 # Portfolio Killian' DAL-CIN — Migration Nuxt 4
## What This Is ## What This Is
Migration complète d'un portfolio freelance de Vue 3 SPA vers Nuxt 4 avec SSR complet. Le site présente les projets, services et compétences de Killian Dalcin, développeur freelance, avec support bilingue FR/EN. L'objectif est un SEO parfait et un développement rapide via des composants prêts à l'emploi (Nuxt UI v3). Migration complète d'un portfolio freelance de Vue 3 SPA vers Nuxt 4 avec SSR complet. Le site présente les projets, services et compétences de Killian' DAL-CIN, développeur freelance, avec support bilingue FR/EN. L'objectif est un SEO parfait et un développement rapide via des composants prêts à l'emploi (Nuxt UI v3).
## Core Value ## Core Value
+1 -1
View File
@@ -1,4 +1,4 @@
# Requirements: Portfolio Killian Dalcin — Nuxt 4 Migration # Requirements: Portfolio Killian' DAL-CIN — Nuxt 4 Migration
**Defined:** 2026-04-07 **Defined:** 2026-04-07
**Core Value:** Chaque page du portfolio doit être crawlable par les moteurs de recherche sans JavaScript côté client **Core Value:** Chaque page du portfolio doit être crawlable par les moteurs de recherche sans JavaScript côté client
+1 -1
View File
@@ -1,4 +1,4 @@
# Roadmap: Portfolio Killian Dalcin — Nuxt 4 Migration # Roadmap: Portfolio Killian' DAL-CIN — Nuxt 4 Migration
## Overview ## Overview
+1 -1
View File
@@ -171,7 +171,7 @@ export interface TechStack {
```vue ```vue
<template> <template>
<div> <div>
<h1>Portfolio Killian Dalcin</h1> <h1>Portfolio Killian' DAL-CIN</h1>
<p>Nuxt 4 Foundation</p> <p>Nuxt 4 Foundation</p>
</div> </div>
</template> </template>
+31 -31
View File
@@ -120,7 +120,7 @@ export default defineAppConfig({
- `css: ['~/assets/css/main.css']` - `css: ['~/assets/css/main.css']`
- `colorMode` block: `{ preference: 'dark', fallback: 'dark', storage: 'cookie', storageKey: 'nuxt-color-mode', cookieName: 'nuxt-color-mode', classSuffix: '' }` - `colorMode` block: `{ preference: 'dark', fallback: 'dark', storage: 'cookie', storageKey: 'nuxt-color-mode', cookieName: 'nuxt-color-mode', classSuffix: '' }`
- `i18n.baseUrl: 'https://killiandalcin.fr'` - `i18n.baseUrl: 'https://killiandalcin.fr'`
- `site: { url: 'https://killiandalcin.fr', name: 'Killian Dalcin - Developpeur Full Stack' }` - `site: { url: 'https://killiandalcin.fr', name: 'Killian' DAL-CIN - Developpeur Full Stack' }`
Do NOT touch existing modules array or i18n locale config — they are correct from Phase 1. Do NOT touch existing modules array or i18n locale config — they are correct from Phase 1.
@@ -171,44 +171,44 @@ fr.json top-level structure:
"formation": "Formation" "formation": "Formation"
}, },
"footer": { "footer": {
"copyright": "© 2026 Killian Dalcin" "copyright": "© 2026 Killian' DAL-CIN"
}, },
"a11y": { "a11y": {
"logoLabel": "Killian Dalcin — Developpeur Full Stack — Retour a l'accueil", "logoLabel": "Killian' DAL-CIN — Developpeur Full Stack — Retour a l'accueil",
"openMenu": "Ouvrir le menu de navigation", "openMenu": "Ouvrir le menu de navigation",
"closeMenu": "Fermer le menu de navigation", "closeMenu": "Fermer le menu de navigation",
"closeDrawer": "Fermer le menu", "closeDrawer": "Fermer le menu",
"langToggle": "Changer la langue — actuellement Francais", "langToggle": "Changer la langue — actuellement Francais",
"themeDark": "Activer le mode clair", "themeDark": "Activer le mode clair",
"themeLight": "Activer le mode sombre", "themeLight": "Activer le mode sombre",
"github": "GitHub de Killian Dalcin (nouvelle fenetre)", "github": "GitHub de Killian' DAL-CIN (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian Dalcin (nouvelle fenetre)", "linkedin": "LinkedIn de Killian' DAL-CIN (nouvelle fenetre)",
"fiverr": "Fiverr de Killian Dalcin (nouvelle fenetre)" "fiverr": "Fiverr de Killian' DAL-CIN (nouvelle fenetre)"
}, },
"seo": { "seo": {
"home": { "home": {
"title": "Killian Dalcin — Developpeur Full Stack Freelance", "title": "Killian' DAL-CIN — Developpeur Full Stack Freelance",
"description": "Portfolio de Killian Dalcin, developpeur full stack freelance specialise en Vue.js, React et Node.js. Applications web performantes et solutions sur-mesure." "description": "Portfolio de Killian' DAL-CIN, developpeur full stack freelance specialise en Vue.js, React et Node.js. Applications web performantes et solutions sur-mesure."
}, },
"projects": { "projects": {
"title": "Projets — Killian Dalcin", "title": "Projets — Killian' DAL-CIN",
"description": "Decouvrez mes realisations en developpement web : applications Vue.js, API Node.js, bots Discord et solutions d'entreprise." "description": "Decouvrez mes realisations en developpement web : applications Vue.js, API Node.js, bots Discord et solutions d'entreprise."
}, },
"about": { "about": {
"title": "A propos — Killian Dalcin", "title": "A propos — Killian' DAL-CIN",
"description": "Biographie et competences de Killian Dalcin, developpeur full stack freelance base en France." "description": "Biographie et competences de Killian' DAL-CIN, developpeur full stack freelance base en France."
}, },
"contact": { "contact": {
"title": "Contact — Killian Dalcin", "title": "Contact — Killian' DAL-CIN",
"description": "Contactez Killian Dalcin pour discuter de votre projet de developpement web." "description": "Contactez Killian' DAL-CIN pour discuter de votre projet de developpement web."
}, },
"fiverr": { "fiverr": {
"title": "Services Fiverr — Killian Dalcin", "title": "Services Fiverr — Killian' DAL-CIN",
"description": "Services de developpement disponibles sur Fiverr : bots Discord, plugins Minecraft, applications web." "description": "Services de developpement disponibles sur Fiverr : bots Discord, plugins Minecraft, applications web."
}, },
"formation": { "formation": {
"title": "Formation — Killian Dalcin", "title": "Formation — Killian' DAL-CIN",
"description": "Formations et cours proposes par Killian Dalcin en developpement web." "description": "Formations et cours proposes par Killian' DAL-CIN en developpement web."
} }
} }
} }
@@ -226,44 +226,44 @@ en.json same structure with English translations:
"formation": "Training" "formation": "Training"
}, },
"footer": { "footer": {
"copyright": "© 2026 Killian Dalcin" "copyright": "© 2026 Killian' DAL-CIN"
}, },
"a11y": { "a11y": {
"logoLabel": "Killian Dalcin — Full Stack Developer — Back to homepage", "logoLabel": "Killian' DAL-CIN — Full Stack Developer — Back to homepage",
"openMenu": "Open navigation menu", "openMenu": "Open navigation menu",
"closeMenu": "Close navigation menu", "closeMenu": "Close navigation menu",
"closeDrawer": "Close menu", "closeDrawer": "Close menu",
"langToggle": "Change language — currently English", "langToggle": "Change language — currently English",
"themeDark": "Switch to light mode", "themeDark": "Switch to light mode",
"themeLight": "Switch to dark mode", "themeLight": "Switch to dark mode",
"github": "Killian Dalcin on GitHub (opens in new tab)", "github": "Killian' DAL-CIN on GitHub (opens in new tab)",
"linkedin": "Killian Dalcin on LinkedIn (opens in new tab)", "linkedin": "Killian' DAL-CIN on LinkedIn (opens in new tab)",
"fiverr": "Killian Dalcin on Fiverr (opens in new tab)" "fiverr": "Killian' DAL-CIN on Fiverr (opens in new tab)"
}, },
"seo": { "seo": {
"home": { "home": {
"title": "Killian Dalcin — Freelance Full Stack Developer", "title": "Killian' DAL-CIN — Freelance Full Stack Developer",
"description": "Portfolio of Killian Dalcin, freelance full stack developer specializing in Vue.js, React and Node.js. High-performance web applications and custom solutions." "description": "Portfolio of Killian' DAL-CIN, freelance full stack developer specializing in Vue.js, React and Node.js. High-performance web applications and custom solutions."
}, },
"projects": { "projects": {
"title": "Projects — Killian Dalcin", "title": "Projects — Killian' DAL-CIN",
"description": "Discover my web development projects: Vue.js applications, Node.js APIs, Discord bots and enterprise solutions." "description": "Discover my web development projects: Vue.js applications, Node.js APIs, Discord bots and enterprise solutions."
}, },
"about": { "about": {
"title": "About — Killian Dalcin", "title": "About — Killian' DAL-CIN",
"description": "Biography and skills of Killian Dalcin, freelance full stack developer based in France." "description": "Biography and skills of Killian' DAL-CIN, freelance full stack developer based in France."
}, },
"contact": { "contact": {
"title": "Contact — Killian Dalcin", "title": "Contact — Killian' DAL-CIN",
"description": "Contact Killian Dalcin to discuss your web development project." "description": "Contact Killian' DAL-CIN to discuss your web development project."
}, },
"fiverr": { "fiverr": {
"title": "Fiverr Services — Killian Dalcin", "title": "Fiverr Services — Killian' DAL-CIN",
"description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications." "description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications."
}, },
"formation": { "formation": {
"title": "Training — Killian Dalcin", "title": "Training — Killian' DAL-CIN",
"description": "Training and courses offered by Killian Dalcin in web development." "description": "Training and courses offered by Killian' DAL-CIN in web development."
} }
} }
} }
+2 -2
View File
@@ -103,7 +103,7 @@ Create `app/components/layout/AppHeader.vue` as a single-file component containi
**Left — Logo:** **Left — Logo:**
- `<NuxtLink to="localePath('/')" :aria-label="t('a11y.logoLabel')">` - `<NuxtLink to="localePath('/')" :aria-label="t('a11y.logoLabel')">`
- Contains `<NuxtImg src="/images/logo.webp" alt="Killian Dalcin" width="40" height="40" loading="eager" />` + `<span class="text-lg font-semibold">Killian</span>` - Contains `<NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="40" height="40" loading="eager" />` + `<span class="text-lg font-semibold">Killian</span>`
**Center-Right — Desktop nav (hidden md:flex):** **Center-Right — Desktop nav (hidden md:flex):**
- Use `<nav>` with `<NuxtLink>` for each route: home(/), projects(/projects), about(/about), contact(/contact), fiverr(/fiverr), formation(/formation) - Use `<nav>` with `<NuxtLink>` for each route: home(/), projects(/projects), about(/about), contact(/contact), fiverr(/fiverr), formation(/formation)
@@ -226,7 +226,7 @@ Template:
- Icon: `<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" />` - Icon: `<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" />`
- Focus ring on each `<a>`: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2` - Focus ring on each `<a>`: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2`
Note: a11y.github key text says "GitHub de Killian Dalcin" but links to Gitea — the executor should update the a11y key in fr.json/en.json to say "Gitea" instead of "GitHub" if not already correct. Check and fix if needed. Note: a11y.github key text says "GitHub de Killian' DAL-CIN" but links to Gitea — the executor should update the a11y key in fr.json/en.json to say "Gitea" instead of "GitHub" if not already correct. Check and fix if needed.
2. Create `app/layouts/default.vue` (per D-15): 2. Create `app/layouts/default.vue` (per D-15):
```vue ```vue
+2 -2
View File
@@ -128,7 +128,7 @@ useHead({
'@graph': [ '@graph': [
{ {
'@type': 'Person', '@type': 'Person',
name: 'Killian Dalcin', name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
jobTitle: 'Developpeur Full Stack Freelance', jobTitle: 'Developpeur Full Stack Freelance',
email: 'contact@killiandalcin.fr', email: 'contact@killiandalcin.fr',
@@ -140,7 +140,7 @@ useHead({
}, },
{ {
'@type': 'ProfessionalService', '@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack', name: 'Killian' DAL-CIN - Developpeur Full Stack',
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
logo: 'https://killiandalcin.fr/images/logo.webp', logo: 'https://killiandalcin.fr/images/logo.webp',
priceRange: '$$$', priceRange: '$$$',
+2 -2
View File
@@ -20,7 +20,7 @@ Chaque route rend la bonne langue (FR/EN), le bon thème (dark/light), et les bo
- **D-04:** Switch langue = bouton texte simple FR/EN (toggle au clic), pas de dropdown ni drapeaux - **D-04:** Switch langue = bouton texte simple FR/EN (toggle au clic), pas de dropdown ni drapeaux
### Footer ### Footer
- **D-05:** Footer minimaliste — une seule bande : copyright © 2026 Killian Dalcin + icônes réseaux sociaux (Gitea, LinkedIn, Fiverr). Note : siteConfig pointe vers gitea.kamisama.ovh, pas GitHub. - **D-05:** Footer minimaliste — une seule bande : copyright © 2026 Killian' DAL-CIN + icônes réseaux sociaux (Gitea, LinkedIn, Fiverr). Note : siteConfig pointe vers gitea.kamisama.ovh, pas GitHub.
### i18n SSR ### i18n SSR
- **D-06:** Enrichir les fichiers existants fr.json/en.json avec les clés navigation, footer et SEO — un seul fichier par langue - **D-06:** Enrichir les fichiers existants fr.json/en.json avec les clés navigation, footer et SEO — un seul fichier par langue
@@ -32,7 +32,7 @@ Chaque route rend la bonne langue (FR/EN), le bon thème (dark/light), et les bo
### SEO & Métadonnées ### SEO & Métadonnées
- **D-10:** useSeoMeta() par route — title, description, og:title, og:description uniques - **D-10:** useSeoMeta() par route — title, description, og:title, og:description uniques
- **D-11:** JSON-LD sur la page d'accueil : schéma Person + ProfessionalService pour Killian Dalcin - **D-11:** JSON-LD sur la page d'accueil : schéma Person + ProfessionalService pour Killian' DAL-CIN
- **D-12:** og:image statique dans public/ (og-image.png 1200x630) — nuxt-og-image dynamique reporté à Phase 3 suite aux risques Windows identifiés en recherche - **D-12:** og:image statique dans public/ (og-image.png 1200x630) — nuxt-og-image dynamique reporté à Phase 3 suite aux risques Windows identifiés en recherche
### Sitemap ### Sitemap
+7 -7
View File
@@ -15,13 +15,13 @@
- **D-02:** Mobile nav via UDrawer (Nuxt UI v3) — hamburger opens left-sliding drawer - **D-02:** Mobile nav via UDrawer (Nuxt UI v3) — hamburger opens left-sliding drawer
- **D-03:** Header sticky permanent - **D-03:** Header sticky permanent
- **D-04:** Language switch = simple text button FR/EN (toggle on click), no dropdown, no flags - **D-04:** Language switch = simple text button FR/EN (toggle on click), no dropdown, no flags
- **D-05:** Footer minimal — single band: copyright © 2026 Killian Dalcin + social icons (GitHub, LinkedIn, Fiverr) - **D-05:** Footer minimal — single band: copyright © 2026 Killian' DAL-CIN + social icons (GitHub, LinkedIn, Fiverr)
- **D-06:** Enrich existing fr.json/en.json with nav, footer, SEO keys — one file per language - **D-06:** Enrich existing fr.json/en.json with nav, footer, SEO keys — one file per language
- **D-07:** @nuxtjs/i18n already configured: strategy prefix_except_default, FR default, browser detection + cookie - **D-07:** @nuxtjs/i18n already configured: strategy prefix_except_default, FR default, browser detection + cookie
- **D-08:** Dark mode default for new visitors - **D-08:** Dark mode default for new visitors
- **D-09:** Cookie persistence via @nuxtjs/color-mode — no localStorage, no FOUC - **D-09:** Cookie persistence via @nuxtjs/color-mode — no localStorage, no FOUC
- **D-10:** useSeoMeta() per route — unique title, description, og:title, og:description - **D-10:** useSeoMeta() per route — unique title, description, og:title, og:description
- **D-11:** JSON-LD on homepage: Person + ProfessionalService schema for Killian Dalcin - **D-11:** JSON-LD on homepage: Person + ProfessionalService schema for Killian' DAL-CIN
- **D-12:** og:image dynamic via nuxt-og-image (advanced from SEOV2-01) - **D-12:** og:image dynamic via nuxt-og-image (advanced from SEOV2-01)
- **D-13:** All public pages in sitemap except 404 - **D-13:** All public pages in sitemap except 404
- **D-14:** hreflang FR/EN alternates automatic via @nuxtjs/sitemap + @nuxtjs/i18n integration - **D-14:** hreflang FR/EN alternates automatic via @nuxtjs/sitemap + @nuxtjs/i18n integration
@@ -240,7 +240,7 @@ useHead({
innerHTML: JSON.stringify({ innerHTML: JSON.stringify({
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Person', '@type': 'Person',
name: 'Killian Dalcin', name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
jobTitle: 'Développeur Full Stack Freelance', jobTitle: 'Développeur Full Stack Freelance',
sameAs: [ sameAs: [
@@ -326,7 +326,7 @@ useSeoMeta({
// nuxt.config.ts // nuxt.config.ts
site: { site: {
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
name: 'Killian Dalcin — Développeur Full Stack', name: 'Killian' DAL-CIN Développeur Full Stack',
}, },
``` ```
@@ -423,7 +423,7 @@ export default defineNuxtConfig({
site: { site: {
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
name: 'Killian Dalcin — Développeur Full Stack', name: 'Killian' DAL-CIN Développeur Full Stack',
}, },
colorMode: { colorMode: {
@@ -549,7 +549,7 @@ useHead({
{ {
'@type': 'Person', '@type': 'Person',
'@id': 'https://killiandalcin.fr/#person', '@id': 'https://killiandalcin.fr/#person',
name: 'Killian Dalcin', name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
jobTitle: 'Développeur Full Stack Freelance', jobTitle: 'Développeur Full Stack Freelance',
email: 'contact@killiandalcin.fr', email: 'contact@killiandalcin.fr',
@@ -561,7 +561,7 @@ useHead({
{ {
'@type': 'ProfessionalService', '@type': 'ProfessionalService',
'@id': 'https://killiandalcin.fr/#service', '@id': 'https://killiandalcin.fr/#service',
name: 'Killian Dalcin — Développeur Full Stack', name: 'Killian' DAL-CIN Développeur Full Stack',
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
provider: { '@id': 'https://killiandalcin.fr/#person' }, provider: { '@id': 'https://killiandalcin.fr/#person' },
priceRange: '€€€', priceRange: '€€€',
+6 -6
View File
@@ -145,7 +145,7 @@ Components delivered in this phase only:
### AppFooter (COMP-06) ### AppFooter (COMP-06)
- Single band: `py-6 bg-gray-100 dark:bg-gray-800` - Single band: `py-6 bg-gray-100 dark:bg-gray-800`
- Layout: flex row on md+, flex column on mobile — `items-center justify-between gap-4` - Layout: flex row on md+, flex column on mobile — `items-center justify-between gap-4`
- Left: copyright text — "© 2026 Killian Dalcin" - Left: copyright text — "© 2026 Killian' DAL-CIN"
- Right: social icon links — GitHub (`simple-icons:github`), LinkedIn (`simple-icons:linkedin`), Fiverr (`simple-icons:fiverr`) - Right: social icon links — GitHub (`simple-icons:github`), LinkedIn (`simple-icons:linkedin`), Fiverr (`simple-icons:fiverr`)
- Icon size: 20px (w-5 h-5). Hover: accent color with `transition-colors duration-150` - Icon size: 20px (w-5 h-5). Hover: accent color with `transition-colors duration-150`
- All links open in `_blank` with `rel="noopener noreferrer"` and `aria-label` - All links open in `_blank` with `rel="noopener noreferrer"` and `aria-label`
@@ -175,7 +175,7 @@ Phase 2 scope: header nav labels, footer copyright, mobile drawer, language/them
| Element | Copy (FR) | Copy (EN) | | Element | Copy (FR) | Copy (EN) |
|---------|-----------|-----------| |---------|-----------|-----------|
| Logo aria-label | "Killian Dalcin — Développeur Full Stack — Retour à l'accueil" | "Killian Dalcin — Full Stack Developer — Back to homepage" | | Logo aria-label | "Killian' DAL-CIN — Développeur Full Stack — Retour à l'accueil" | "Killian' DAL-CIN — Full Stack Developer — Back to homepage" |
| Nav: Home | "Accueil" | "Home" | | Nav: Home | "Accueil" | "Home" |
| Nav: Projects | "Projets" | "Projects" | | Nav: Projects | "Projets" | "Projects" |
| Nav: About | "À propos" | "About" | | Nav: About | "À propos" | "About" |
@@ -188,10 +188,10 @@ Phase 2 scope: header nav labels, footer copyright, mobile drawer, language/them
| Language toggle aria-label | "Changer la langue — actuellement Français" | "Change language — currently English" | | Language toggle aria-label | "Changer la langue — actuellement Français" | "Change language — currently English" |
| Theme toggle aria-label (dark) | "Activer le mode clair" | "Switch to light mode" | | Theme toggle aria-label (dark) | "Activer le mode clair" | "Switch to light mode" |
| Theme toggle aria-label (light) | "Activer le mode sombre" | "Switch to dark mode" | | Theme toggle aria-label (light) | "Activer le mode sombre" | "Switch to dark mode" |
| Footer copyright | "© 2026 Killian Dalcin" | "© 2026 Killian Dalcin" | | Footer copyright | "© 2026 Killian' DAL-CIN" | "© 2026 Killian' DAL-CIN" |
| GitHub icon aria-label | "GitHub de Killian Dalcin (nouvelle fenêtre)" | "Killian Dalcin on GitHub (opens in new tab)" | | GitHub icon aria-label | "GitHub de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on GitHub (opens in new tab)" |
| LinkedIn icon aria-label | "LinkedIn de Killian Dalcin (nouvelle fenêtre)" | "Killian Dalcin on LinkedIn (opens in new tab)" | | LinkedIn icon aria-label | "LinkedIn de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on LinkedIn (opens in new tab)" |
| Fiverr icon aria-label | "Fiverr de Killian Dalcin (nouvelle fenêtre)" | "Killian Dalcin on Fiverr (opens in new tab)" | | Fiverr icon aria-label | "Fiverr de Killian' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on Fiverr (opens in new tab)" |
Destructive confirmation: none — Phase 2 has no destructive actions. Destructive confirmation: none — Phase 2 has no destructive actions.
Empty state: none — Phase 2 has no data-driven content. Empty state: none — Phase 2 has no data-driven content.
@@ -640,7 +640,7 @@ if (!name || !email || !message || message.length > 5000) {
|---|-------|---------|-------------| |---|-------|---------|-------------|
| A1 | `@types/nodemailer` est le package de types correct pour nodemailer 8.x | Standard Stack | Types manquants — TypeScript strict échouera ; vérifier avec `npm view @types/nodemailer` | | A1 | `@types/nodemailer` est le package de types correct pour nodemailer 8.x | Standard Stack | Types manquants — TypeScript strict échouera ; vérifier avec `npm view @types/nodemailer` |
| A2 | OVH SMTP fonctionne sur ssl0.ovh.net:465 avec auth PLAIN | Pattern 3 | L'envoi échoue — tester avec les vraies credentials avant de fermer la phase | | A2 | OVH SMTP fonctionne sur ssl0.ovh.net:465 avec auth PLAIN | Pattern 3 | L'envoi échoue — tester avec les vraies credentials avant de fermer la phase |
| A3 | Docker est disponible sur la machine de déploiement de Killian | Environment Availability | INFRA-01 bloqué — confirmer avec `docker --version` | | A3 | Docker est disponible sur la machine de déploiement de Killian'| Environment Availability | INFRA-01 bloqué — confirmer avec `docker --version` |
| A4 | `UCarousel` expose `emblaApi` directement via `useTemplateRef` dans Nuxt UI v3 | Pattern 1 | Navigation thumbnail cassée — fallback : gérer index manuellement avec `watch` | | A4 | `UCarousel` expose `emblaApi` directement via `useTemplateRef` dans Nuxt UI v3 | Pattern 1 | Navigation thumbnail cassée — fallback : gérer index manuellement avec `watch` |
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
# Architecture Patterns # Architecture Patterns
**Project:** Portfolio Killian Dalcin — Nuxt 4 SSR Migration **Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
**Researched:** 2026-04-07 **Researched:** 2026-04-07
**Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis) **Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis)
+1 -1
View File
@@ -1,6 +1,6 @@
# Technology Stack # Technology Stack
**Project:** Portfolio Killian Dalcin — Nuxt 4 SSR Migration **Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
**Researched:** 2026-04-07 **Researched:** 2026-04-07
**Knowledge cutoff:** August 2025 — all versions marked LOW confidence must be verified against npm before pinning **Knowledge cutoff:** August 2025 — all versions marked LOW confidence must be verified against npm before pinning
+1 -1
View File
@@ -1,6 +1,6 @@
# Research Summary # Research Summary
**Project:** Portfolio Killian Dalcin — Vue 3 SPA → Nuxt 4 SSR Migration **Project:** Portfolio Killian' DAL-CIN — Vue 3 SPA → Nuxt 4 SSR Migration
**Date:** 2026-04-07 **Date:** 2026-04-07
**Sources:** STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md **Sources:** STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md
+2 -2
View File
@@ -1,9 +1,9 @@
<!-- GSD:project-start source:PROJECT.md --> <!-- GSD:project-start source:PROJECT.md -->
## Project ## Project
**Portfolio Killian Dalcin — Migration Nuxt 4** **Portfolio Killian' Dalcin — Migration Nuxt 4**
Migration complète d'un portfolio freelance de Vue 3 SPA vers Nuxt 4 avec SSR complet. Le site présente les projets, services et compétences de Killian Dalcin, développeur freelance, avec support bilingue FR/EN. L'objectif est un SEO parfait et un développement rapide via des composants prêts à l'emploi (Nuxt UI v3). Migration complète d'un portfolio freelance de Vue 3 SPA vers Nuxt 4 avec SSR complet. Le site présente les projets, services et compétences de Killian' Dalcin, développeur freelance, avec support bilingue FR/EN. L'objectif est un SEO parfait et un développement rapide via des composants prêts à l'emploi (Nuxt UI v3).
**Core Value:** Chaque page du portfolio doit être crawlable par les moteurs de recherche sans JavaScript côté client — le SSR est la raison d'être de cette migration. **Core Value:** Chaque page du portfolio doit être crawlable par les moteurs de recherche sans JavaScript côté client — le SSR est la raison d'être de cette migration.
+2 -2
View File
@@ -6,7 +6,7 @@ A modern, responsive personal portfolio website showcasing professional skills,
## 🎯 Purpose ## 🎯 Purpose
This portfolio serves as a professional showcase for **Killian Dal Cin**, a Full Stack Developer specializing in modern web development. The website features: This portfolio serves as a professional showcase for **Killian' DAL-CIN**, a Full Stack Developer specializing in modern web development. The website features:
- **Professional Presentation**: Clean, modern design highlighting skills and experience - **Professional Presentation**: Clean, modern design highlighting skills and experience
- **Project Showcase**: Interactive gallery of completed projects with detailed case studies - **Project Showcase**: Interactive gallery of completed projects with detailed case studies
@@ -220,7 +220,7 @@ This project is personal portfolio software. Please respect the intellectual pro
## 📧 Contact ## 📧 Contact
**Killian Dal Cin** **Killian' DAL-CIN**
- Email: contact@killiandalcin.fr - Email: contact@killiandalcin.fr
- LinkedIn: [killian-dalcin](https://linkedin.com/in/killian-dal-cin) - LinkedIn: [killian-dalcin](https://linkedin.com/in/killian-dal-cin)
+42 -15
View File
@@ -14,10 +14,10 @@ const schema = z.object({
type Schema = z.output<typeof schema> type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ const state = reactive({
name: undefined, name: '',
email: undefined, email: '',
message: undefined, message: '',
}) })
async function onSubmit(event: FormSubmitEvent<Schema>) { async function onSubmit(event: FormSubmitEvent<Schema>) {
@@ -29,10 +29,9 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
color: 'success', color: 'success',
icon: 'i-lucide-check', icon: 'i-lucide-check',
}) })
// Reset form state.name = ''
state.name = undefined state.email = ''
state.email = undefined state.message = ''
state.message = undefined
} catch { } catch {
toast.add({ toast.add({
title: t('contact.form.error'), title: t('contact.form.error'),
@@ -46,21 +45,49 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
</script> </script>
<template> <template>
<UForm :schema="schema" :state="state" class="flex flex-col gap-4" @submit="onSubmit"> <UForm :schema="schema" :state="state" class="space-y-5" @submit="onSubmit">
<UFormField :label="t('contact.form.name')" name="name"> <UFormField :label="t('contact.form.name')" name="name">
<UInput v-model="state.name" :placeholder="t('contact.form.name')" class="w-full" /> <UInput
v-model="state.name"
:placeholder="t('contact.form.name')"
icon="i-lucide-user"
size="lg"
class="w-full"
/>
</UFormField> </UFormField>
<UFormField :label="t('contact.form.email')" name="email"> <UFormField :label="t('contact.form.email')" name="email">
<UInput v-model="state.email" type="email" :placeholder="t('contact.form.email')" class="w-full" /> <UInput
v-model="state.email"
type="email"
:placeholder="t('contact.form.email')"
icon="i-lucide-mail"
size="lg"
class="w-full"
/>
</UFormField> </UFormField>
<UFormField :label="t('contact.form.message')" name="message"> <UFormField :label="t('contact.form.message')" name="message">
<UTextarea v-model="state.message" :rows="5" :placeholder="t('contact.form.message')" class="w-full" /> <UTextarea
v-model="state.message"
:rows="6"
:placeholder="t('contact.form.message')"
size="lg"
class="w-full"
/>
</UFormField> </UFormField>
<UButton type="submit" :loading="loading" size="lg" class="self-start"> <button
{{ t('contact.form.submit') }} type="submit"
</UButton> :disabled="loading"
class="inline-flex items-center justify-center gap-2 w-full px-6 py-3.5 rounded-xl bg-brand-500 hover:bg-brand-600 disabled:opacity-60 disabled:cursor-not-allowed text-white font-semibold text-sm transition-all duration-200 shadow-lg shadow-brand-500/25 hover:shadow-brand-500/40"
>
<UIcon v-if="loading" name="i-lucide-loader-2" class="w-4 h-4 animate-spin" />
<template v-if="loading">{{ t('contact.form.sending') }}</template>
<template v-else>
{{ t('contact.form.submit') }}
<UIcon name="i-lucide-send" class="w-4 h-4" />
</template>
</button>
</UForm> </UForm>
</template> </template>
+22 -20
View File
@@ -17,7 +17,7 @@ const translatedCategory = computed(() => {
<template> <template>
<article <article
class="group relative rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden transition-all duration-300 hover:border-brand-500/30 hover:shadow-xl hover:shadow-brand-500/5 hover:-translate-y-1" class="group relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm overflow-hidden transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1.5"
itemscope itemscope
itemtype="https://schema.org/CreativeWork" itemtype="https://schema.org/CreativeWork"
> >
@@ -30,21 +30,26 @@ const translatedCategory = computed(() => {
format="webp" format="webp"
width="400" width="400"
height="300" height="300"
class="w-full h-48 object-cover transition-transform duration-500 group-hover:scale-105" class="w-full h-52 object-cover transition-transform duration-500 group-hover:scale-105"
itemprop="image" itemprop="image"
/> />
<!-- Overlay on hover --> <!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end justify-center pb-5">
<span class="text-white text-sm font-semibold flex items-center gap-1.5 translate-y-2 group-hover:translate-y-0 transition-transform duration-300">
{{ t('projects.buttons.viewProject') }}
<UIcon name="i-lucide-arrow-right" class="w-4 h-4" />
</span>
</div>
</NuxtLink> </NuxtLink>
<!-- Content --> <!-- Content -->
<div class="p-5 flex flex-col gap-3"> <div class="p-5 sm:p-6 flex flex-col gap-3">
<!-- Category & Date --> <!-- Category & Date -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<UBadge v-if="project.category" color="primary" variant="subtle" itemprop="genre"> <UBadge v-if="project.category" color="primary" variant="subtle" itemprop="genre">
{{ translatedCategory }} {{ translatedCategory }}
</UBadge> </UBadge>
<time v-if="project.date" class="text-xs text-gray-400 dark:text-gray-500 font-medium" :datetime="project.date" itemprop="dateCreated"> <time v-if="project.date" class="text-xs text-gray-400 dark:text-gray-500 font-mono" :datetime="project.date" itemprop="dateCreated">
{{ project.date }} {{ project.date }}
</time> </time>
</div> </div>
@@ -60,29 +65,26 @@ const translatedCategory = computed(() => {
</p> </p>
<!-- Technologies --> <!-- Technologies -->
<div v-if="project.technologies?.length" class="flex flex-wrap gap-1.5 pt-1" itemprop="keywords"> <div v-if="project.technologies?.length" class="flex flex-wrap gap-1.5 pt-2" itemprop="keywords">
<span <span
v-for="tech in project.technologies.slice(0, 3)" v-for="tech in project.technologies.slice(0, 3)"
:key="tech" :key="tech"
class="text-xs px-2 py-0.5 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 font-medium" class="text-[11px] px-2.5 py-1 rounded-full bg-gray-100 dark:bg-gray-800/80 text-gray-600 dark:text-gray-400 font-medium border border-gray-200/50 dark:border-gray-700/30"
> >
{{ tech }} {{ tech }}
</span> </span>
<span v-if="project.technologies.length > 3" class="text-xs px-2 py-0.5 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-500 font-medium"> <span v-if="project.technologies.length > 3" class="text-[11px] px-2.5 py-1 rounded-full bg-gray-100 dark:bg-gray-800/80 text-gray-500 dark:text-gray-500 font-medium border border-gray-200/50 dark:border-gray-700/30">
+{{ project.technologies.length - 3 }} +{{ project.technologies.length - 3 }}
</span> </span>
</div> </div>
<!-- Action link -->
<NuxtLink
:to="`/project/${project.id}`"
class="inline-flex items-center gap-1.5 text-sm font-semibold text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300 transition-colors mt-1 group/link"
:aria-label="`${t('projects.buttons.viewProject')} - ${project.title}`"
itemprop="url"
>
{{ t('projects.buttons.viewProject') }}
<UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform group-hover/link:translate-x-0.5" />
</NuxtLink>
</div> </div>
<!-- Hidden SEO link -->
<NuxtLink
:to="`/project/${project.id}`"
class="absolute inset-0 z-10"
:aria-label="`${t('projects.buttons.viewProject')} - ${project.title}`"
itemprop="url"
/>
</article> </article>
</template> </template>
+1 -1
View File
@@ -39,7 +39,7 @@ defineExpose({ openGallery })
<template> <template>
<UModal v-model:open="isOpen" fullscreen> <UModal v-model:open="isOpen" fullscreen>
<template #content> <template #content>
<div class="flex flex-col items-center justify-center h-full p-4 gap-4"> <div class="flex flex-col items-center justify-center h-full p-4 gap-4" @click.self="isOpen = false">
<div class="flex items-center justify-between w-full max-w-4xl"> <div class="flex items-center justify-between w-full max-w-4xl">
<h3 class="text-lg font-semibold">{{ projectTitle }}</h3> <h3 class="text-lg font-semibold">{{ projectTitle }}</h3>
<UButton <UButton
+42 -39
View File
@@ -18,70 +18,73 @@ const quickLinks = computed(() => [
</script> </script>
<template> <template>
<footer class="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50"> <footer class="border-t border-gray-200/80 dark:border-gray-800/50 bg-gray-50/80 dark:bg-gray-950/80">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-20">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-8"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 lg:gap-8">
<!-- Branding & Tagline --> <!-- Brand column -->
<div class="space-y-4"> <div class="sm:col-span-2 lg:col-span-1 space-y-5">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2.5"> <NuxtLink :to="localePath('/')" class="flex items-center gap-2.5 group">
<NuxtImg <NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="36" height="36" loading="lazy"
src="/images/logo.webp" class="rounded-lg transition-transform duration-300 group-hover:scale-110" />
alt="Killian Dalcin" <span class="text-lg font-bold text-gray-900 dark:text-white">Killian' DAL-CIN</span>
width="36"
height="36"
loading="lazy"
class="rounded-lg"
/>
<span class="text-lg font-bold text-gray-900 dark:text-white">Killian Dalcin</span>
</NuxtLink> </NuxtLink>
<p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed max-w-xs"> <p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed max-w-xs">
Full Stack Developer &amp; Hytale Plugin Developer. Building modern web experiences and game plugins. Full Stack Developer &amp; Hytale Plugin Developer. Building modern web experiences and game plugins.
</p> </p>
</div> </div>
<!-- Quick Links --> <!-- Navigation links -->
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4"> <h3 class="font-mono text-xs text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-5">
Navigation Navigation
</h3> </h3>
<nav class="flex flex-col gap-2.5"> <nav class="flex flex-col gap-3">
<NuxtLink <NuxtLink v-for="link in quickLinks" :key="link.key" :to="localePath(link.path)"
v-for="link in quickLinks" class="text-sm text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors duration-200">
:key="link.key"
:to="localePath(link.path)"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors"
>
{{ t(`nav.${link.key}`) }} {{ t(`nav.${link.key}`) }}
</NuxtLink> </NuxtLink>
</nav> </nav>
</div> </div>
<!-- Social Links --> <!-- Services links -->
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4"> <h3 class="font-mono text-xs text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-5">
Social Services
</h3> </h3>
<div class="flex items-center gap-3"> <nav class="flex flex-col gap-3">
<a <span class="text-sm text-gray-600 dark:text-gray-400">Web Development</span>
v-for="link in socialLinks" <span class="text-sm text-gray-600 dark:text-gray-400">Hytale Plugins</span>
:key="link.name" <span class="text-sm text-gray-600 dark:text-gray-400">Consulting</span>
:href="link.url" <span class="text-sm text-gray-600 dark:text-gray-400">Maintenance</span>
target="_blank" </nav>
rel="noopener noreferrer" </div>
<!-- Connect -->
<div>
<h3 class="font-mono text-xs text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-5">
Connect
</h3>
<div class="flex items-center gap-2">
<a v-for="link in socialLinks" :key="link.name" :href="link.url" target="_blank" rel="noopener noreferrer"
:aria-label="t(link.ariaKey)" :aria-label="t(link.ariaKey)"
class="w-10 h-10 inline-flex items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-brand-500/10 dark:hover:bg-brand-500/10 transition-all duration-200 group" class="w-10 h-10 inline-flex items-center justify-center rounded-xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 hover:border-brand-500/40 hover:bg-brand-500/10 dark:hover:bg-brand-500/10 transition-all duration-300 group">
> <UIcon :name="link.icon"
<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 group-hover:text-brand-500 dark:group-hover:text-brand-400 transition-colors" /> class="w-4.5 h-4.5 text-gray-500 dark:text-gray-400 group-hover:text-brand-500 dark:group-hover:text-brand-400 transition-colors" />
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Bottom bar --> <!-- Bottom bar -->
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-800"> <div
<p class="text-sm text-gray-400 dark:text-gray-500 text-center"> class="mt-14 pt-8 border-t border-gray-200/60 dark:border-gray-800/40 flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-sm text-gray-400 dark:text-gray-500 font-mono">
{{ t('footer.copyright') }} {{ t('footer.copyright') }}
</p> </p>
<div class="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-600">
<span class="w-1.5 h-1.5 rounded-full bg-brand-500 animate-pulse" />
<span>Built with Nuxt</span>
</div>
</div> </div>
</div> </div>
</footer> </footer>
+21 -73
View File
@@ -27,40 +27,26 @@ function isActive(path: string): boolean {
</script> </script>
<template> <template>
<header class="sticky top-0 z-50 backdrop-blur-xl bg-white/80 dark:bg-gray-950/80 border-b border-gray-200/50 dark:border-gray-800/50"> <header
class="sticky top-0 z-50 backdrop-blur-xl bg-white/80 dark:bg-gray-950/80 border-b border-gray-200/50 dark:border-gray-800/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16"> <div class="flex items-center justify-between h-16">
<!-- Logo --> <!-- Logo -->
<NuxtLink <NuxtLink :to="localePath('/')" :aria-label="t('a11y.logoLabel')" class="flex items-center gap-2.5 shrink-0">
:to="localePath('/')" <NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="36" height="36" loading="eager"
:aria-label="t('a11y.logoLabel')" class="rounded-lg" />
class="flex items-center gap-2.5 shrink-0" <span class="text-base font-semibold tracking-tight text-gray-900 dark:text-white">Killian'</span>
>
<NuxtImg
src="/images/logo.webp"
alt="Killian Dalcin"
width="36"
height="36"
loading="eager"
class="rounded-lg"
/>
<span class="text-base font-semibold tracking-tight text-gray-900 dark:text-white">Killian</span>
</NuxtLink> </NuxtLink>
<!-- Desktop nav --> <!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-1" aria-label="Main navigation"> <nav class="hidden md:flex items-center gap-1" aria-label="Main navigation">
<NuxtLink <NuxtLink v-for="link in navLinks" :key="link.key" :to="localePath(link.path)"
v-for="link in navLinks"
:key="link.key"
:to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined" :aria-current="isActive(link.path) ? 'page' : undefined"
class="px-3 py-2 text-sm font-medium rounded-lg transition-colors" class="px-3 py-2 text-sm font-medium rounded-lg transition-colors" :class="[
:class="[
isActive(link.path) isActive(link.path)
? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40' ? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60'
]" ]">
>
{{ t(`nav.${link.key}`) }} {{ t(`nav.${link.key}`) }}
</NuxtLink> </NuxtLink>
</nav> </nav>
@@ -68,36 +54,19 @@ function isActive(path: string): boolean {
<!-- Right actions --> <!-- Right actions -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<!-- Language toggle --> <!-- Language toggle -->
<UButton <UButton variant="ghost" color="neutral" size="sm" :aria-label="t('a11y.langToggle')" @click="toggleLocale">
variant="ghost"
color="neutral"
size="sm"
:aria-label="t('a11y.langToggle')"
@click="toggleLocale"
>
{{ locale === 'fr' ? 'EN' : 'FR' }} {{ locale === 'fr' ? 'EN' : 'FR' }}
</UButton> </UButton>
<!-- Theme toggle --> <!-- Theme toggle -->
<UButton <UButton variant="ghost" color="neutral" size="sm"
variant="ghost"
color="neutral"
size="sm"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'" :icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')" :aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"
@click="toggleTheme" @click="toggleTheme" />
/>
<!-- Mobile hamburger --> <!-- Mobile hamburger -->
<UButton <UButton variant="ghost" color="neutral" size="sm" icon="i-lucide-menu" class="md:hidden"
variant="ghost" :aria-label="t('a11y.openMenu')" @click="mobileOpen = true" />
color="neutral"
size="sm"
icon="i-lucide-menu"
class="md:hidden"
:aria-label="t('a11y.openMenu')"
@click="mobileOpen = true"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -106,32 +75,20 @@ function isActive(path: string): boolean {
<USlideover v-model:open="mobileOpen" side="left" class="md:hidden"> <USlideover v-model:open="mobileOpen" side="left" class="md:hidden">
<template #header> <template #header>
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<NuxtImg <NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="32" height="32" class="rounded-lg" />
src="/images/logo.webp"
alt="Killian Dalcin"
width="32"
height="32"
class="rounded-lg"
/>
<span class="text-base font-semibold text-gray-900 dark:text-white">Killian</span> <span class="text-base font-semibold text-gray-900 dark:text-white">Killian</span>
</div> </div>
</template> </template>
<template #body> <template #body>
<nav class="flex flex-col gap-1" aria-label="Mobile navigation"> <nav class="flex flex-col gap-1" aria-label="Mobile navigation">
<NuxtLink <NuxtLink v-for="link in navLinks" :key="link.key" :to="localePath(link.path)"
v-for="link in navLinks"
:key="link.key"
:to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined" :aria-current="isActive(link.path) ? 'page' : undefined"
class="px-4 py-3 text-base font-medium rounded-lg transition-colors" class="px-4 py-3 text-base font-medium rounded-lg transition-colors" :class="[
:class="[
isActive(link.path) isActive(link.path)
? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40' ? 'text-brand-600 dark:text-brand-400 bg-brand-50 dark:bg-brand-950/40'
: 'text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60' : 'text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800/60'
]" ]" @click="mobileOpen = false">
@click="mobileOpen = false"
>
{{ t(`nav.${link.key}`) }} {{ t(`nav.${link.key}`) }}
</NuxtLink> </NuxtLink>
</nav> </nav>
@@ -139,21 +96,12 @@ function isActive(path: string): boolean {
<template #footer> <template #footer>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UButton <UButton variant="ghost" color="neutral" :aria-label="t('a11y.langToggle')" @click="toggleLocale">
variant="ghost"
color="neutral"
:aria-label="t('a11y.langToggle')"
@click="toggleLocale"
>
{{ locale === 'fr' ? 'EN' : 'FR' }} {{ locale === 'fr' ? 'EN' : 'FR' }}
</UButton> </UButton>
<UButton <UButton variant="ghost" color="neutral" :icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
variant="ghost"
color="neutral"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')" :aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"
@click="toggleTheme" @click="toggleTheme" />
/>
</div> </div>
</template> </template>
</USlideover> </USlideover>
+49 -13
View File
@@ -1,26 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath() const localePath = useLocalePath()
interface Props {
title?: string
subtitle?: string
primaryText?: string
primaryTo?: string
secondaryText?: string
secondaryTo?: string
external?: boolean
}
const props = withDefaults(defineProps<Props>(), {
title: '',
subtitle: '',
primaryText: '',
primaryTo: '/contact',
secondaryText: '',
secondaryTo: '/about',
external: false,
})
const resolvedTitle = computed(() => props.title || t('home.cta2.title'))
const resolvedSubtitle = computed(() => props.subtitle || t('home.cta2.subtitle'))
const resolvedPrimaryText = computed(() => props.primaryText || t('home.cta2.startProject'))
const resolvedSecondaryText = computed(() => props.secondaryText || t('home.cta2.learnMore'))
</script> </script>
<template> <template>
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-5xl mx-auto"> <div class="max-w-5xl mx-auto">
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-brand-600 via-brand-500 to-emerald-500 px-8 py-16 sm:px-16 sm:py-20 text-center"> <div class="relative overflow-hidden rounded-3xl px-8 py-20 sm:px-16 sm:py-24 text-center border border-gray-200/60 dark:border-gray-800/40 bg-gray-50 dark:bg-gray-900">
<!-- Decorative shapes --> <!-- Subtle dot pattern -->
<div class="absolute top-0 left-0 w-72 h-72 bg-white/10 rounded-full -translate-x-1/2 -translate-y-1/2 blur-2xl" aria-hidden="true" /> <div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.06]" aria-hidden="true"
<div class="absolute bottom-0 right-0 w-96 h-96 bg-black/10 rounded-full translate-x-1/3 translate-y-1/3 blur-2xl" aria-hidden="true" /> style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 24px 24px;" />
<!-- Brand glow -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-brand-500/8 dark:bg-brand-500/15 rounded-full blur-3xl pointer-events-none" aria-hidden="true" />
<div class="relative z-10"> <div class="relative z-10">
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('home.cta2.title') }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-5 tracking-tight">{{ resolvedTitle }}</h2>
<p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('home.cta2.subtitle') }}</p> <p class="text-lg sm:text-xl text-gray-500 dark:text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">{{ resolvedSubtitle }}</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton :to="localePath('/contact')" size="xl" color="white" class="font-semibold"> <NuxtLink
{{ t('home.cta2.startProject') }} :to="external ? props.primaryTo : localePath(props.primaryTo)"
</UButton> :target="external ? '_blank' : undefined"
<UButton :to="localePath('/about')" size="xl" variant="outline" color="white" class="font-semibold"> :rel="external ? 'noopener noreferrer' : undefined"
{{ t('home.cta2.learnMore') }} class="inline-flex items-center justify-center gap-2 px-7 py-3.5 rounded-xl bg-brand-500 hover:bg-brand-600 text-white font-semibold text-sm transition-all duration-200 shadow-lg shadow-brand-500/25 hover:shadow-brand-500/40"
</UButton> >
{{ resolvedPrimaryText }}
<UIcon :name="external ? 'i-lucide-external-link' : 'i-lucide-arrow-right'" class="w-4 h-4" />
</NuxtLink>
<NuxtLink
:to="localePath(props.secondaryTo)"
class="inline-flex items-center justify-center gap-2 px-7 py-3.5 rounded-xl border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:border-brand-500/50 hover:text-brand-500 font-semibold text-sm transition-all duration-200"
>
{{ resolvedSecondaryText }}
</NuxtLink>
</div> </div>
</div> </div>
</div> </div>
+6 -6
View File
@@ -20,15 +20,15 @@ const items = computed(() =>
</script> </script>
<template> <template>
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<div class="text-center mb-14"> <div class="text-center mb-16">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">FAQ</span> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// faq</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ title }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ title }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3">{{ subtitle }}</p> <p class="text-lg text-gray-500 dark:text-gray-400 mt-4 leading-relaxed">{{ subtitle }}</p>
</div> </div>
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-2"> <div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-3 sm:p-4 shadow-sm">
<UAccordion :items="items" type="single" collapsible /> <UAccordion :items="items" type="single" collapsible />
</div> </div>
</div> </div>
@@ -4,27 +4,31 @@ const { featuredProjects } = useProjects()
</script> </script>
<template> <template>
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<!-- Section header --> <!-- Section header -->
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-4 mb-14"> <div class="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-16">
<div> <div>
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Portfolio</span> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// portfolio</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('home.featuredProjects.title') }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('home.featuredProjects.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl">{{ t('home.featuredProjects.subtitle') }}</p> <p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl leading-relaxed">{{ t('home.featuredProjects.subtitle') }}</p>
</div> </div>
<UButton to="/projects" variant="ghost" trailing-icon="i-lucide-arrow-right" class="shrink-0 self-start md:self-auto"> <UButton to="/projects" variant="ghost" trailing-icon="i-lucide-arrow-right" class="shrink-0 self-start md:self-auto group">
{{ t('home.cta.viewProjects') }} {{ t('home.cta.viewProjects') }}
</UButton> </UButton>
</div> </div>
<!-- Projects grid --> <!-- Bento grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 lg:gap-6 auto-rows-fr">
<ProjectCard <div
v-for="project in featuredProjects" v-for="(project, index) in featuredProjects"
:key="project.id" :key="project.id"
:project="project" :class="[
/> index === 0 ? 'md:col-span-2 md:row-span-1' : '',
]"
>
<ProjectCard :project="project" :class="{ 'h-full': true }" />
</div>
</div> </div>
</div> </div>
</section> </section>
+46 -19
View File
@@ -7,18 +7,22 @@ const localePath = useLocalePath()
<section class="relative min-h-[80vh] flex items-center overflow-hidden bg-white dark:bg-gray-950"> <section class="relative min-h-[80vh] flex items-center overflow-hidden bg-white dark:bg-gray-950">
<!-- Dot grid background pattern --> <!-- Dot grid background pattern -->
<div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.06]" aria-hidden="true"> <div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.06]" aria-hidden="true">
<div class="absolute inset-0" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 32px 32px;" /> <div class="absolute inset-0"
style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 32px 32px;" />
</div> </div>
<!-- Gradient glow --> <!-- Gradient glow -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[600px] bg-brand-500/5 dark:bg-brand-500/10 rounded-full blur-3xl pointer-events-none" aria-hidden="true" /> <div
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[600px] bg-brand-500/5 dark:bg-brand-500/10 rounded-full blur-3xl pointer-events-none"
aria-hidden="true" />
<div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full py-16 md:py-24"> <div class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full py-16 md:py-24">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<!-- Left: Content --> <!-- Left: Content -->
<div class="space-y-8"> <div class="space-y-8">
<!-- Status badge --> <!-- Status badge -->
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-brand-500/10 dark:bg-brand-500/15 border border-brand-500/20"> <div
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-brand-500/10 dark:bg-brand-500/15 border border-brand-500/20">
<span class="relative flex h-2 w-2"> <span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-400 opacity-75" /> <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-400 opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-brand-500" /> <span class="relative inline-flex rounded-full h-2 w-2 bg-brand-500" />
@@ -28,8 +32,11 @@ const localePath = useLocalePath()
<div class="space-y-4"> <div class="space-y-4">
<h1 class="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1]"> <h1 class="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1]">
<span class="text-gray-900 dark:text-white">{{ t('home.title').split(' ').slice(0, -2).join(' ') }} </span> <span class="text-gray-900 dark:text-white">{{ t('home.title').split(' ').slice(0, -2).join(' ') }}
<span class="bg-gradient-to-r from-brand-500 via-brand-400 to-emerald-400 bg-clip-text text-transparent">{{ t('home.title').split(' ').slice(-2).join(' ') }}</span> </span>
<span
class="bg-gradient-to-r from-brand-500 via-brand-400 to-emerald-400 bg-clip-text text-transparent">{{
t('home.title').split(' ').slice(-2).join(' ') }}</span>
</h1> </h1>
<p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-xl leading-relaxed"> <p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-xl leading-relaxed">
{{ t('home.subtitle') }} {{ t('home.subtitle') }}
@@ -38,15 +45,27 @@ const localePath = useLocalePath()
<!-- CTA Buttons --> <!-- CTA Buttons -->
<div class="flex flex-col sm:flex-row gap-3"> <div class="flex flex-col sm:flex-row gap-3">
<UButton :to="localePath('/projects')" size="xl" icon="i-lucide-arrow-right" trailing class="font-semibold"> <NuxtLink
:to="localePath('/projects')"
class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-brand-500 hover:bg-brand-600 text-white font-semibold text-sm transition-all duration-200 shadow-lg shadow-brand-500/25 hover:shadow-brand-500/40"
>
{{ t('home.cta.viewProjects') }} {{ t('home.cta.viewProjects') }}
</UButton> <UIcon name="i-lucide-arrow-right" class="w-4 h-4" />
<UButton :to="localePath('/fiverr')" size="xl" variant="outline" icon="i-lucide-dollar-sign" trailing class="font-semibold"> </NuxtLink>
<NuxtLink
:to="localePath('/fiverr')"
class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:border-brand-500/50 hover:text-brand-500 font-semibold text-sm transition-all duration-200"
>
{{ t('nav.fiverr') }} {{ t('nav.fiverr') }}
</UButton> <UIcon name="i-lucide-external-link" class="w-4 h-4" />
<UButton :to="localePath('/contact')" size="xl" variant="ghost" icon="i-lucide-message-circle" trailing class="font-semibold"> </NuxtLink>
<NuxtLink
:to="localePath('/contact')"
class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl text-gray-600 dark:text-gray-400 hover:text-brand-500 font-semibold text-sm transition-all duration-200"
>
{{ t('home.cta.contactMe') }} {{ t('home.cta.contactMe') }}
</UButton> <UIcon name="i-lucide-message-circle" class="w-4 h-4" />
</NuxtLink>
</div> </div>
</div> </div>
@@ -54,9 +73,11 @@ const localePath = useLocalePath()
<div class="hidden lg:block" aria-hidden="true"> <div class="hidden lg:block" aria-hidden="true">
<div class="relative"> <div class="relative">
<!-- Terminal window --> <!-- Terminal window -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 shadow-2xl shadow-brand-500/5 overflow-hidden"> <div
class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 shadow-2xl shadow-brand-500/5 overflow-hidden">
<!-- Title bar --> <!-- Title bar -->
<div class="flex items-center gap-2 px-4 py-3 bg-gray-100 dark:bg-gray-800/80 border-b border-gray-200 dark:border-gray-700/50"> <div
class="flex items-center gap-2 px-4 py-3 bg-gray-100 dark:bg-gray-800/80 border-b border-gray-200 dark:border-gray-700/50">
<div class="flex gap-1.5"> <div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-400/80" /> <div class="w-3 h-3 rounded-full bg-red-400/80" />
<div class="w-3 h-3 rounded-full bg-yellow-400/80" /> <div class="w-3 h-3 rounded-full bg-yellow-400/80" />
@@ -75,11 +96,13 @@ const localePath = useLocalePath()
</div> </div>
<div class="pl-6"> <div class="pl-6">
<span class="text-purple-500 dark:text-purple-400">name</span><span class="text-gray-500">: </span> <span class="text-purple-500 dark:text-purple-400">name</span><span class="text-gray-500">: </span>
<span class="text-amber-600 dark:text-amber-400">'Killian Dalcin'</span><span class="text-gray-500">,</span> <span class="text-amber-600 dark:text-amber-400">'Killian\' DAL-CIN'</span><span
class="text-gray-500">,</span>
</div> </div>
<div class="pl-6"> <div class="pl-6">
<span class="text-purple-500 dark:text-purple-400">role</span><span class="text-gray-500">: </span> <span class="text-purple-500 dark:text-purple-400">role</span><span class="text-gray-500">: </span>
<span class="text-amber-600 dark:text-amber-400">'Full Stack Dev'</span><span class="text-gray-500">,</span> <span class="text-amber-600 dark:text-amber-400">'Full Stack Dev'</span><span
class="text-gray-500">,</span>
</div> </div>
<div class="pl-6"> <div class="pl-6">
<span class="text-purple-500 dark:text-purple-400">skills</span><span class="text-gray-500">: [</span> <span class="text-purple-500 dark:text-purple-400">skills</span><span class="text-gray-500">: [</span>
@@ -91,7 +114,8 @@ const localePath = useLocalePath()
</div> </div>
<div class="pl-10"> <div class="pl-10">
<span class="text-amber-600 dark:text-amber-400">'Java'</span><span class="text-gray-500">, </span> <span class="text-amber-600 dark:text-amber-400">'Java'</span><span class="text-gray-500">, </span>
<span class="text-amber-600 dark:text-amber-400">'Hytale Plugins'</span><span class="text-gray-500">,</span> <span class="text-amber-600 dark:text-amber-400">'Hytale Plugins'</span><span
class="text-gray-500">,</span>
</div> </div>
<div class="pl-10"> <div class="pl-10">
<span class="text-amber-600 dark:text-amber-400">'Docker'</span><span class="text-gray-500">, </span> <span class="text-amber-600 dark:text-amber-400">'Docker'</span><span class="text-gray-500">, </span>
@@ -101,7 +125,8 @@ const localePath = useLocalePath()
<span class="text-gray-500">],</span> <span class="text-gray-500">],</span>
</div> </div>
<div class="pl-6"> <div class="pl-6">
<span class="text-purple-500 dark:text-purple-400">available</span><span class="text-gray-500">: </span> <span class="text-purple-500 dark:text-purple-400">available</span><span class="text-gray-500">:
</span>
<span class="text-brand-500">true</span> <span class="text-brand-500">true</span>
</div> </div>
<div> <div>
@@ -116,11 +141,13 @@ const localePath = useLocalePath()
</div> </div>
<!-- Floating decoration cards --> <!-- Floating decoration cards -->
<div class="absolute -top-4 -right-4 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg text-xs font-medium flex items-center gap-2"> <div
class="absolute -top-4 -right-4 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg text-xs font-medium flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-brand-500" /> <span class="w-2 h-2 rounded-full bg-brand-500" />
<span class="text-gray-700 dark:text-gray-300">50+ projects</span> <span class="text-gray-700 dark:text-gray-300">50+ projects</span>
</div> </div>
<div class="absolute -bottom-3 -left-3 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg text-xs font-medium flex items-center gap-2"> <div
class="absolute -bottom-3 -left-3 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg text-xs font-medium flex items-center gap-2">
<UIcon name="i-lucide-star" class="text-yellow-400 w-3.5 h-3.5" /> <UIcon name="i-lucide-star" class="text-yellow-400 w-3.5 h-3.5" />
<span class="text-gray-700 dark:text-gray-300">5.0 rating</span> <span class="text-gray-700 dark:text-gray-300">5.0 rating</span>
</div> </div>
+23 -14
View File
@@ -26,27 +26,36 @@ const services = computed(() => [
</script> </script>
<template> <template>
<section class="py-20 md:py-28 px-4 bg-gray-50 dark:bg-gray-900/50 rounded-3xl mx-2 md:mx-0"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
<div class="max-w-7xl mx-auto"> <!-- Subtle background gradient -->
<div class="text-center mb-14"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Services</span> <div class="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl -translate-y-1/2 translate-x-1/4 pointer-events-none" aria-hidden="true" />
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('home.services.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto">{{ t('home.services.subtitle') }}</p> <div class="relative z-10 max-w-7xl mx-auto">
<div class="text-center mb-16">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// services</span>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('home.services.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">{{ t('home.services.subtitle') }}</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5 lg:gap-6">
<div <div
v-for="(service, index) in services" v-for="(service, index) in services"
:key="index" :key="index"
class="group relative rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8 transition-all duration-300 hover:border-brand-500/30 hover:shadow-lg hover:shadow-brand-500/5 hover:-translate-y-0.5" class="group relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8 transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1"
> >
<!-- Icon --> <!-- Hover glow effect -->
<div class="w-12 h-12 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mb-5 transition-colors group-hover:bg-brand-500/20"> <div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-brand-500/0 to-emerald-500/0 group-hover:from-brand-500/5 group-hover:to-emerald-500/5 transition-all duration-500 pointer-events-none" aria-hidden="true" />
<UIcon :name="service.icon" class="text-brand-600 dark:text-brand-400 text-xl" />
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">{{ service.title }}</h3> <div class="relative z-10">
<p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ service.description }}</p> <!-- Icon -->
<div class="w-12 h-12 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mb-6 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon :name="service.icon" class="text-brand-600 dark:text-brand-400 text-xl" />
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ service.title }}</h3>
<p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ service.description }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
+27 -24
View File
@@ -5,40 +5,43 @@ const { t } = useI18n()
</script> </script>
<template> <template>
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-14"> <div class="text-center mb-16">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">{{ t('testimonials.title') }}</span> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// testimonials</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('testimonials.title') }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('testimonials.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto">{{ t('testimonials.subtitle') }}</p> <p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">{{ t('testimonials.subtitle') }}</p>
<!-- Stats row --> <!-- Stats row -->
<div class="flex justify-center gap-10 mt-10"> <div class="flex justify-center gap-8 sm:gap-12 mt-12">
<div class="text-center"> <div class="text-center group">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.totalReviews }}</p> <p class="text-4xl sm:text-5xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ testimonialsStats.totalReviews }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.clients') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('testimonials.stats.clients') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center"> <div class="text-center group">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.averageRating }}/5</p> <p class="text-4xl sm:text-5xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ testimonialsStats.averageRating }}/5</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.rating') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('testimonials.stats.rating') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center"> <div class="text-center group">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.projectsCompleted }}</p> <p class="text-4xl sm:text-5xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ testimonialsStats.projectsCompleted }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.projects') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('testimonials.stats.projects') }}</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Horizontal scrolling testimonials --> <!-- Horizontal scrolling testimonials -->
<div class="flex gap-6 overflow-x-auto pb-4 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide"> <div class="flex gap-5 overflow-x-auto overflow-y-visible pb-8 -mx-4 px-4 pt-2 snap-x snap-mandatory scrollbar-hide">
<div <div
v-for="(testimonial, index) in testimonials" v-for="(testimonial, index) in testimonials"
:key="index" :key="index"
class="flex-none w-[340px] sm:w-[380px] snap-start" class="flex-none w-[340px] sm:w-[400px] snap-start"
> >
<div class="h-full rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 flex flex-col gap-4 transition-all duration-300 hover:border-brand-500/30 hover:shadow-lg hover:shadow-brand-500/5"> <div class="h-full relative rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 flex flex-col gap-5 transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1">
<!-- Decorative quote mark -->
<div class="absolute top-5 right-6 text-6xl font-serif text-brand-500/10 dark:text-brand-400/10 leading-none select-none pointer-events-none" aria-hidden="true">"</div>
<!-- Rating stars --> <!-- Rating stars -->
<div class="flex gap-1"> <div class="flex gap-1">
<UIcon <UIcon
@@ -51,23 +54,23 @@ const { t } = useI18n()
</div> </div>
<!-- Quote --> <!-- Quote -->
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed flex-1 italic"> <p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed flex-1 relative z-10">
"{{ testimonial.content }}" "{{ testimonial.content }}"
</p> </p>
<!-- Author --> <!-- Author -->
<div class="flex items-center gap-3 pt-2 border-t border-gray-100 dark:border-gray-800"> <div class="flex items-center gap-3 pt-4 border-t border-gray-100 dark:border-gray-800/60">
<NuxtImg <NuxtImg
:src="testimonial.avatar" :src="testimonial.avatar"
:alt="testimonial.name" :alt="testimonial.name"
width="40" width="40"
height="40" height="40"
class="rounded-full ring-2 ring-gray-100 dark:ring-gray-800" class="rounded-full ring-2 ring-brand-500/20 dark:ring-brand-400/20"
loading="lazy" loading="lazy"
/> />
<div> <div>
<p class="font-semibold text-sm text-gray-900 dark:text-white">{{ testimonial.name }}</p> <p class="font-semibold text-sm text-gray-900 dark:text-white">{{ testimonial.name }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ testimonial.project_type }} - {{ testimonial.platform }}</p> <p class="text-xs text-gray-500 dark:text-gray-400 font-mono">{{ testimonial.project_type }} - {{ testimonial.platform }}</p>
</div> </div>
</div> </div>
</div> </div>
+2 -3
View File
@@ -4,7 +4,7 @@ export type { SiteConfig, ContactInfo, SocialLink, FiverrService, FiverrConfig }
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
name: 'Killian', name: 'Killian',
title: 'Killian - Full Stack Developer | Vue.js, React, Node.js Expert', title: "Killian' DAL-CIN - Full Stack Developer | Vue.js, React, Node.js Expert",
description: description:
'Professional Full Stack Developer specializing in modern web development with Vue.js, React, Node.js. Expert in Discord bots, web applications, and custom software solutions.', 'Professional Full Stack Developer specializing in modern web development with Vue.js, React, Node.js. Expert in Discord bots, web applications, and custom software solutions.',
author: 'Killian', author: 'Killian',
@@ -12,7 +12,6 @@ export const siteConfig: SiteConfig = {
contact: { contact: {
email: 'contact@killiandalcin.fr', email: 'contact@killiandalcin.fr',
phone: '+33 6 49 19 38 16',
location: 'France', location: 'France',
}, },
@@ -92,7 +91,7 @@ export const siteConfig: SiteConfig = {
}, },
organization: { organization: {
'@type': 'ProfessionalService', '@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack', name: "Killian' DAL-CIN - Developpeur Full Stack",
logo: 'https://killiandalcin.fr/logo.webp', logo: 'https://killiandalcin.fr/logo.webp',
priceRange: '$$$', priceRange: '$$$',
aggregateRating: { aggregateRating: {
+13 -10
View File
@@ -10,29 +10,32 @@ function handleError() {
</script> </script>
<template> <template>
<div class="min-h-screen flex flex-col items-center justify-center gap-8 px-4 bg-white dark:bg-gray-950"> <div class="min-h-screen flex flex-col items-center justify-center gap-8 px-4 bg-white dark:bg-gray-950 relative overflow-hidden">
<!-- Decorative background --> <!-- Decorative background -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true"> <div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-brand-500/5 dark:bg-brand-500/10 rounded-full blur-3xl" /> <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-brand-500/5 dark:bg-brand-500/10 rounded-full blur-3xl" />
<!-- Dot grid -->
<div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.05]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 32px 32px;" />
</div> </div>
<div class="relative z-10 text-center space-y-6 max-w-lg"> <div class="relative z-10 text-center space-y-8 max-w-lg">
<!-- Error code with glitch-like styling --> <!-- Error code -->
<div class="relative"> <div class="relative">
<h1 class="text-[10rem] sm:text-[12rem] font-black leading-none tracking-tighter bg-gradient-to-b from-brand-500 to-brand-600 bg-clip-text text-transparent select-none"> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// error</span>
<h1 class="text-[10rem] sm:text-[12rem] font-black leading-none tracking-tighter bg-gradient-to-b from-brand-400 via-brand-500 to-brand-700 bg-clip-text text-transparent select-none mt-2">
{{ error.statusCode }} {{ error.statusCode }}
</h1> </h1>
<!-- Shadow text behind --> <!-- Shadow glow behind -->
<span class="absolute inset-0 text-[10rem] sm:text-[12rem] font-black leading-none tracking-tighter text-brand-500/10 blur-sm select-none" aria-hidden="true"> <span class="absolute inset-0 top-8 text-[10rem] sm:text-[12rem] font-black leading-none tracking-tighter text-brand-500/8 blur-md select-none" aria-hidden="true">
{{ error.statusCode }} {{ error.statusCode }}
</span> </span>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<p class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white"> <p class="text-xl sm:text-2xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
{{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }} {{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }}
</p> </p>
<p class="text-gray-500 dark:text-gray-400"> <p class="text-gray-500 dark:text-gray-400 leading-relaxed">
{{ error.statusCode === 404 {{ error.statusCode === 404
? 'The page you are looking for does not exist or has been moved.' ? 'The page you are looking for does not exist or has been moved.'
: 'Something unexpected happened. Please try again.' : 'Something unexpected happened. Please try again.'
@@ -40,7 +43,7 @@ function handleError() {
</p> </p>
</div> </div>
<UButton size="xl" icon="i-lucide-home" class="font-semibold" @click="handleError"> <UButton size="xl" icon="i-lucide-home" class="font-semibold" trailing-icon="i-lucide-arrow-right" @click="handleError">
{{ t('error.backHome') }} {{ t('error.backHome') }}
</UButton> </UButton>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-screen flex flex-col bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100"> <div class="min-h-screen flex flex-col bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 antialiased">
<AppHeader /> <AppHeader />
<main class="flex-1"> <main class="flex-1">
<slot /> <slot />
+47 -51
View File
@@ -65,42 +65,45 @@ const approachCards = computed(() => [
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="pt-16 pb-20 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative pt-20 pb-20 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-4xl mx-auto text-center"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">About</span> <div class="absolute top-0 right-0 w-[600px] h-[600px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl -translate-y-1/2 translate-x-1/4 pointer-events-none" aria-hidden="true" />
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<div class="relative z-10 max-w-4xl mx-auto text-center">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// about</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mt-3 mb-6 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">
{{ t('about.title') }} {{ t('about.title') }}
</h1> </h1>
<p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
{{ t('about.subtitle') }} {{ t('about.subtitle') }}
</p> </p>
<div class="space-y-4 text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto leading-relaxed"> <div class="max-w-3xl mx-auto space-y-6">
<p>{{ t('about.intro.content') }}</p> <p class="text-lg text-gray-600 dark:text-gray-400 leading-relaxed rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-6 sm:p-8 text-left">{{ t('about.intro.content') }}</p>
<p>{{ t('about.experience.content') }}</p> <p class="text-lg text-gray-600 dark:text-gray-400 leading-relaxed rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-6 sm:p-8 text-left">{{ t('about.experience.content') }}</p>
</div> </div>
</div> </div>
</section> </section>
<!-- Skills Section --> <!-- Skills Section -->
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-14"> <div class="text-center mb-16">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Tech Stack</span> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// tech-stack</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('about.skills.title') }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('about.skills.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto"> <p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">
{{ t('about.subtitle') }} {{ t('about.subtitle') }}
</p> </p>
</div> </div>
<!-- Tech Categories Grid --> <!-- Tech Categories Bento Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-5">
<div <div
v-for="category in techCategories" v-for="category in techCategories"
:key="category.key" :key="category.key"
class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8" class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8 transition-all duration-300 hover:border-brand-500/30 hover:shadow-lg hover:shadow-brand-500/5"
> >
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center"> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon :name="category.icon" class="text-xl text-brand-600 dark:text-brand-400" /> <UIcon :name="category.icon" class="text-xl text-brand-600 dark:text-brand-400" />
</div> </div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ category.title }}</h3> <h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ category.title }}</h3>
@@ -117,9 +120,9 @@ const approachCards = computed(() => [
</div> </div>
<!-- Operating Systems --> <!-- Operating Systems -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8"> <div class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8 transition-all duration-300 hover:border-brand-500/30 hover:shadow-lg hover:shadow-brand-500/5">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center"> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon name="i-lucide-monitor" class="text-xl text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-monitor" class="text-xl text-brand-600 dark:text-brand-400" />
</div> </div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('about.skills.systems') }}</h3> <h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ t('about.skills.systems') }}</h3>
@@ -137,28 +140,34 @@ const approachCards = computed(() => [
</section> </section>
<!-- Approach Section --> <!-- Approach Section -->
<section class="py-20 md:py-28 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative py-24 md:py-32 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-7xl mx-auto"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<div class="text-center mb-14"> <div class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl translate-y-1/2 -translate-x-1/4 pointer-events-none" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Methodology</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('about.approach.title') }}</h2> <div class="relative z-10 max-w-7xl mx-auto">
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto"> <div class="text-center mb-16">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// methodology</span>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('about.approach.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">
{{ t('about.approach.subtitle') }} {{ t('about.approach.subtitle') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div <div
v-for="(card, index) in approachCards" v-for="(card, index) in approachCards"
:key="index" :key="index"
class="group rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8 transition-all duration-300 hover:border-brand-500/30 hover:shadow-lg hover:shadow-brand-500/5" class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8 transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1"
> >
<div class="flex items-start gap-4"> <!-- Hover glow -->
<div class="w-12 h-12 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 transition-colors group-hover:bg-brand-500/20"> <div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-brand-500/0 to-emerald-500/0 group-hover:from-brand-500/5 group-hover:to-emerald-500/5 transition-all duration-500 pointer-events-none" aria-hidden="true" />
<div class="relative z-10 flex items-start gap-4">
<div class="w-12 h-12 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon :name="card.icon" class="text-xl text-brand-600 dark:text-brand-400" /> <UIcon :name="card.icon" class="text-xl text-brand-600 dark:text-brand-400" />
</div> </div>
<div> <div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ card.title }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ card.title }}</h3>
<p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ card.description }}</p> <p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ card.description }}</p>
</div> </div>
</div> </div>
@@ -168,26 +177,13 @@ const approachCards = computed(() => [
</section> </section>
<!-- CTA Section --> <!-- CTA Section -->
<section class="py-20 md:py-28 px-4"> <CTASection
<div class="max-w-5xl mx-auto"> :title="t('about.cta.title')"
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-brand-600 via-brand-500 to-emerald-500 px-8 py-16 sm:px-16 sm:py-20 text-center"> :subtitle="t('about.cta.description')"
<div class="absolute top-0 left-0 w-72 h-72 bg-white/10 rounded-full -translate-x-1/2 -translate-y-1/2 blur-2xl" aria-hidden="true" /> :primary-text="t('about.cta.button')"
<div class="absolute bottom-0 right-0 w-96 h-96 bg-black/10 rounded-full translate-x-1/3 translate-y-1/3 blur-2xl" aria-hidden="true" /> primary-to="/contact"
:secondary-text="t('home.cta.viewProjects')"
<div class="relative z-10"> secondary-to="/projects"
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('about.cta.title') }}</h2> />
<p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('about.cta.description') }}</p>
<div class="flex flex-wrap justify-center gap-4">
<UButton :to="localePath('/contact')" size="xl" color="white" class="font-semibold">
{{ t('about.cta.button') }}
</UButton>
<UButton :to="localePath('/projects')" size="xl" variant="outline" color="white" class="font-semibold">
{{ t('home.cta.viewProjects') }}
</UButton>
</div>
</div>
</div>
</div>
</section>
</div> </div>
</template> </template>
+62 -57
View File
@@ -18,43 +18,49 @@ useSeoMeta({
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-4xl mx-auto text-center"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Contact</span> <div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl pointer-events-none" aria-hidden="true" />
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<div class="relative z-10 max-w-4xl mx-auto text-center">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// contact</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mt-3 mb-6 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">
{{ t('contact.title') }} {{ t('contact.title') }}
</h1> </h1>
<p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
{{ t('contact.subtitle') }} {{ t('contact.subtitle') }}
</p> </p>
<!-- Stats --> <!-- Stats -->
<div class="flex flex-wrap justify-center gap-10"> <div class="flex flex-wrap justify-center gap-8 sm:gap-12">
<div class="text-center"> <div class="text-center">
<div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">24-48h</div> <div class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">24-48h</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.responseTime') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('contact.stats.responseTime') }}</div>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800 hidden sm:block" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent hidden sm:block" />
<div class="text-center"> <div class="text-center">
<div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">100%</div> <div class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">100%</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.satisfaction') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('contact.stats.satisfaction') }}</div>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800 hidden sm:block" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent hidden sm:block" />
<div class="text-center"> <div class="text-center">
<div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">Remote</div> <div class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">Remote</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.collaboration') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('contact.stats.collaboration') }}</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Two Column Layout --> <!-- Two Column Layout -->
<section class="py-16 md:py-20 px-4"> <section class="py-16 md:py-24 px-4 sm:px-6 lg:px-8">
<div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-5 gap-10 lg:gap-16"> <div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-12">
<!-- Left: Contact Form (wider) --> <!-- Left: Contact Form (wider) -->
<div class="lg:col-span-3"> <div class="lg:col-span-3">
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8"> <div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.form.title') }}</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-3">
<div class="w-1 h-6 rounded-full bg-brand-500" />
{{ t('contact.form.title') }}
</h2>
<ContactForm /> <ContactForm />
</div> </div>
</div> </div>
@@ -62,53 +68,50 @@ useSeoMeta({
<!-- Right: Contact Info + Social --> <!-- Right: Contact Info + Social -->
<div class="lg:col-span-2 flex flex-col gap-6"> <div class="lg:col-span-2 flex flex-col gap-6">
<!-- Contact Info --> <!-- Contact Info -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8"> <div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.quickContact') }}</h2> <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-3">
<div class="flex flex-col gap-5"> <div class="w-1 h-5 rounded-full bg-brand-500" />
{{ t('contact.quickContact') }}
</h2>
<div class="flex flex-col gap-4">
<a <a
:href="`mailto:${siteConfig.contact.email}`" :href="`mailto:${siteConfig.contact.email}`"
class="flex items-center gap-4 group" class="flex items-center gap-4 p-3 rounded-xl border border-transparent hover:border-brand-500/20 hover:bg-brand-500/5 transition-all duration-200 group"
> >
<div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 transition-colors group-hover:bg-brand-500/20"> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon name="i-lucide-mail" class="text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-mail" class="text-brand-600 dark:text-brand-400" />
</div> </div>
<span class="text-gray-600 dark:text-gray-300 group-hover:text-brand-500 transition-colors">{{ siteConfig.contact.email }}</span> <span class="text-gray-600 dark:text-gray-300 group-hover:text-brand-500 transition-colors font-medium">{{ siteConfig.contact.email }}</span>
</a> </a>
<a <div class="flex items-center gap-4 p-3">
:href="`tel:${siteConfig.contact.phone.replace(/\s/g, '')}`"
class="flex items-center gap-4 group"
>
<div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 transition-colors group-hover:bg-brand-500/20">
<UIcon name="i-lucide-phone" class="text-brand-600 dark:text-brand-400" />
</div>
<span class="text-gray-600 dark:text-gray-300 group-hover:text-brand-500 transition-colors">{{ siteConfig.contact.phone }}</span>
</a>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0"> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0">
<UIcon name="i-lucide-map-pin" class="text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-map-pin" class="text-brand-600 dark:text-brand-400" />
</div> </div>
<span class="text-gray-600 dark:text-gray-300">{{ siteConfig.contact.location }}</span> <span class="text-gray-600 dark:text-gray-300 font-medium">{{ siteConfig.contact.location }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Social Links --> <!-- Social Links -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8"> <div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.findMeOn') }}</h2> <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-3">
<div class="flex flex-col gap-3"> <div class="w-1 h-5 rounded-full bg-brand-500" />
{{ t('contact.findMeOn') }}
</h2>
<div class="flex flex-col gap-2">
<a <a
v-for="social in siteConfig.social.filter(s => s.icon !== 'i-lucide-mail')" v-for="social in siteConfig.social.filter(s => s.icon !== 'i-lucide-mail')"
:key="social.name" :key="social.name"
:href="social.url" :href="social.url"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="flex items-center gap-4 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors group" class="flex items-center gap-4 p-3 rounded-xl border border-transparent hover:border-brand-500/20 hover:bg-brand-500/5 transition-all duration-200 group"
> >
<div class="w-10 h-10 rounded-xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center shrink-0 group-hover:bg-brand-500/10 transition-colors"> <div class="w-10 h-10 rounded-xl bg-gray-100 dark:bg-gray-800/80 border border-gray-200/50 dark:border-gray-700/30 flex items-center justify-center shrink-0 group-hover:bg-brand-500/10 group-hover:border-brand-500/20 transition-all duration-300">
<UIcon :name="social.icon" class="text-gray-500 dark:text-gray-400 group-hover:text-brand-500 transition-colors" /> <UIcon :name="social.icon" class="text-gray-500 dark:text-gray-400 group-hover:text-brand-500 transition-colors" />
</div> </div>
<span class="text-gray-700 dark:text-gray-300 font-medium group-hover:text-brand-500 transition-colors">{{ social.name }}</span> <span class="text-gray-700 dark:text-gray-300 font-medium group-hover:text-brand-500 transition-colors">{{ social.name }}</span>
<UIcon name="i-lucide-external-link" class="text-xs text-gray-400 dark:text-gray-600 ml-auto" /> <UIcon name="i-lucide-external-link" class="text-xs text-gray-400 dark:text-gray-600 ml-auto group-hover:text-brand-400 transition-colors" />
</a> </a>
</div> </div>
</div> </div>
@@ -117,38 +120,40 @@ useSeoMeta({
</section> </section>
<!-- FAQ Info Cards --> <!-- FAQ Info Cards -->
<section class="py-16 md:py-20 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative py-20 md:py-28 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-6xl mx-auto"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<div class="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Info</span> <div class="relative z-10 max-w-6xl mx-auto">
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('contact.faq.title') }}</h2> <div class="text-center mb-16">
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto"> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// info</span>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('contact.faq.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">
{{ t('contact.faq.subtitle') }} {{ t('contact.faq.subtitle') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-8 text-center group hover:border-brand-500/30 transition-all duration-300"> <div class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-8 text-center transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1">
<div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 group-hover:bg-brand-500/20 transition-colors"> <div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon name="i-lucide-clock" class="text-2xl text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-clock" class="text-2xl text-brand-600 dark:text-brand-400" />
</div> </div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ t('contact.faq.responseTime.title') }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-3 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ t('contact.faq.responseTime.title') }}</h3>
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.responseTime.description') }}</p> <p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.responseTime.description') }}</p>
</div> </div>
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-8 text-center group hover:border-brand-500/30 transition-all duration-300"> <div class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-8 text-center transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1">
<div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 group-hover:bg-brand-500/20 transition-colors"> <div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon name="i-lucide-building" class="text-2xl text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-building" class="text-2xl text-brand-600 dark:text-brand-400" />
</div> </div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ t('contact.faq.projectTypes.title') }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-3 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ t('contact.faq.projectTypes.title') }}</h3>
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.projectTypes.description') }}</p> <p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.projectTypes.description') }}</p>
</div> </div>
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-8 text-center group hover:border-brand-500/30 transition-all duration-300"> <div class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm p-8 text-center transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1">
<div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 group-hover:bg-brand-500/20 transition-colors"> <div class="w-14 h-14 rounded-2xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center mx-auto mb-5 transition-all duration-300 group-hover:bg-brand-500/20 group-hover:scale-110">
<UIcon name="i-lucide-users" class="text-2xl text-brand-600 dark:text-brand-400" /> <UIcon name="i-lucide-users" class="text-2xl text-brand-600 dark:text-brand-400" />
</div> </div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ t('contact.faq.collaboration.title') }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-3 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ t('contact.faq.collaboration.title') }}</h3>
<p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.collaboration.description') }}</p> <p class="text-gray-500 dark:text-gray-400 text-sm leading-relaxed">{{ t('contact.faq.collaboration.description') }}</p>
</div> </div>
</div> </div>
+35 -47
View File
@@ -33,21 +33,24 @@ const heroStats = computed(() => [
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="pt-16 pb-16 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-4xl mx-auto text-center"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Fiverr</span> <div class="absolute top-0 right-0 w-[600px] h-[600px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl -translate-y-1/2 translate-x-1/4 pointer-events-none" aria-hidden="true" />
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<div class="relative z-10 max-w-4xl mx-auto text-center">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// fiverr</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mt-3 mb-6 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">
{{ t('fiverr.title') }} {{ t('fiverr.title') }}
</h1> </h1>
<p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-12 max-w-2xl mx-auto leading-relaxed">
{{ t('fiverr.subtitle') }} {{ t('fiverr.subtitle') }}
</p> </p>
<!-- Stats --> <!-- Stats -->
<div class="flex flex-wrap justify-center gap-10 mb-10"> <div class="flex flex-wrap justify-center gap-8 sm:gap-12 mb-12">
<div v-for="stat in heroStats" :key="stat.label" class="text-center"> <div v-for="stat in heroStats" :key="stat.label" class="text-center">
<div class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ stat.number }}</div> <div class="text-4xl sm:text-5xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ stat.number }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ stat.label }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ stat.label }}</div>
</div> </div>
</div> </div>
@@ -65,19 +68,19 @@ const heroStats = computed(() => [
</section> </section>
<!-- Services Section --> <!-- Services Section -->
<section class="py-20 md:py-28 px-4"> <section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-14"> <div class="text-center mb-16">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Services</span> <span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// services</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('fiverr.services.title') }}</h2> <h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold mt-3 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('fiverr.services.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3">{{ t('fiverr.services.subtitle') }}</p> <p class="text-lg text-gray-500 dark:text-gray-400 mt-4 leading-relaxed">{{ t('fiverr.services.subtitle') }}</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5 lg:gap-6">
<div <div
v-for="service in services" v-for="service in services"
:key="service.id" :key="service.id"
class="group rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden transition-all duration-300 hover:border-brand-500/30 hover:shadow-xl hover:shadow-brand-500/5 hover:-translate-y-1" class="group rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/80 dark:bg-gray-900/60 backdrop-blur-sm overflow-hidden transition-all duration-300 hover:border-brand-500/40 hover:shadow-xl hover:shadow-brand-500/10 hover:-translate-y-1.5"
> >
<!-- Service Image --> <!-- Service Image -->
<div class="relative overflow-hidden"> <div class="relative overflow-hidden">
@@ -87,10 +90,10 @@ const heroStats = computed(() => [
class="w-full h-52 object-cover transition-transform duration-500 group-hover:scale-105" class="w-full h-52 object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy" loading="lazy"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" /> <div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
<!-- Price badge overlay --> <!-- Price badge overlay -->
<div class="absolute bottom-3 left-3"> <div class="absolute bottom-3 left-3">
<span class="px-3 py-1.5 rounded-lg bg-brand-500 text-white text-sm font-bold shadow-lg"> <span class="px-3 py-1.5 rounded-lg bg-brand-500 text-white text-sm font-bold shadow-lg backdrop-blur-sm">
{{ t('fiverr.pricing.startingAt') }} {{ service.price }} {{ t('fiverr.pricing.startingAt') }} {{ service.price }}
</span> </span>
</div> </div>
@@ -98,8 +101,8 @@ const heroStats = computed(() => [
<div class="absolute top-3 right-3"> <div class="absolute top-3 right-3">
<span <span
:class="service.url !== '#' :class="service.url !== '#'
? 'bg-green-500/90 text-white' ? 'bg-green-500/90 text-white backdrop-blur-sm'
: 'bg-yellow-500/90 text-white'" : 'bg-yellow-500/90 text-white backdrop-blur-sm'"
class="px-2.5 py-1 rounded-lg text-xs font-semibold shadow-lg" class="px-2.5 py-1 rounded-lg text-xs font-semibold shadow-lg"
> >
{{ service.url !== '#' ? t('fiverr.services.available') : t('fiverr.services.comingSoon') }} {{ service.url !== '#' ? t('fiverr.services.available') : t('fiverr.services.comingSoon') }}
@@ -108,11 +111,11 @@ const heroStats = computed(() => [
</div> </div>
<!-- Content --> <!-- Content -->
<div class="p-6"> <div class="p-6 sm:p-7">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-brand-500 transition-colors"> <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">
{{ t(`fiverr.serviceData.${service.id}.title`) }} {{ t(`fiverr.serviceData.${service.id}.title`) }}
</h3> </h3>
<p class="text-gray-500 dark:text-gray-400 mb-5 leading-relaxed"> <p class="text-gray-500 dark:text-gray-400 mb-6 leading-relaxed">
{{ t(`fiverr.serviceData.${service.id}.description`) }} {{ t(`fiverr.serviceData.${service.id}.description`) }}
</p> </p>
@@ -142,7 +145,7 @@ const heroStats = computed(() => [
</section> </section>
<!-- FAQ Section --> <!-- FAQ Section -->
<div class="bg-gray-50 dark:bg-gray-900/30"> <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
<FAQSection <FAQSection
:faqs="homeFAQs" :faqs="homeFAQs"
:title="t('fiverr.faq.title')" :title="t('fiverr.faq.title')"
@@ -151,29 +154,14 @@ const heroStats = computed(() => [
</div> </div>
<!-- CTA Section --> <!-- CTA Section -->
<section class="py-20 md:py-28 px-4"> <CTASection
<div class="max-w-5xl mx-auto"> :title="t('fiverr.cta.title')"
<div class="relative overflow-hidden rounded-3xl bg-gradient-to-br from-brand-600 via-brand-500 to-emerald-500 px-8 py-16 sm:px-16 sm:py-20 text-center"> :subtitle="t('fiverr.cta.subtitle')"
<div class="absolute top-0 left-0 w-72 h-72 bg-white/10 rounded-full -translate-x-1/2 -translate-y-1/2 blur-2xl" aria-hidden="true" /> :primary-text="t('fiverr.cta.button')"
<div class="absolute bottom-0 right-0 w-96 h-96 bg-black/10 rounded-full translate-x-1/3 translate-y-1/3 blur-2xl" aria-hidden="true" /> :primary-to="siteConfig.fiverr.profileUrl"
:secondary-text="t('fiverr.profileCta')"
<div class="relative z-10"> secondary-to="/contact"
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('fiverr.cta.title') }}</h2> external
<p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('fiverr.cta.subtitle') }}</p> />
<UButton
:to="siteConfig.fiverr.profileUrl"
target="_blank"
external
size="xl"
color="white"
trailing-icon="i-lucide-external-link"
class="font-semibold"
>
{{ t('fiverr.cta.button') }}
</UButton>
</div>
</div>
</div>
</section>
</div> </div>
</template> </template>
+9 -13
View File
@@ -23,7 +23,7 @@ useHead({
'@graph': [ '@graph': [
{ {
'@type': 'Person', '@type': 'Person',
name: 'Killian Dalcin', name: "Killian' DAL-CIN",
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
jobTitle: 'Developpeur Full Stack Freelance', jobTitle: 'Developpeur Full Stack Freelance',
email: 'contact@killiandalcin.fr', email: 'contact@killiandalcin.fr',
@@ -35,14 +35,14 @@ useHead({
}, },
{ {
'@type': 'ProfessionalService', '@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack', name: "Killian' DAL-CIN - Developpeur Full Stack",
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
logo: 'https://killiandalcin.fr/images/logo.webp', logo: 'https://killiandalcin.fr/images/logo.webp',
priceRange: '$$$', priceRange: '$$$',
areaServed: 'Worldwide', areaServed: 'Worldwide',
}, },
], ],
}), }),
}, },
], ],
}) })
@@ -54,7 +54,7 @@ useHead({
<HeroSection /> <HeroSection />
<!-- Featured Projects Section --> <!-- Featured Projects Section -->
<div class="bg-gray-50 dark:bg-gray-900/30"> <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
<FeaturedProjectsSection /> <FeaturedProjectsSection />
</div> </div>
@@ -62,16 +62,12 @@ useHead({
<ServicesSection /> <ServicesSection />
<!-- Testimonials Section --> <!-- Testimonials Section -->
<div class="bg-gray-50 dark:bg-gray-900/30"> <div class="relative bg-gray-50/50 dark:bg-gray-900/20">
<TestimonialsSection /> <TestimonialsSection />
</div> </div>
<!-- FAQ Section --> <!-- FAQ Section -->
<FAQSection <FAQSection :faqs="homeFAQs" :title="t('faq.title')" :subtitle="t('faq.subtitle')" />
:faqs="homeFAQs"
:title="t('faq.title')"
:subtitle="t('faq.subtitle')"
/>
<!-- CTA Section --> <!-- CTA Section -->
<CTASection /> <CTASection />
+117 -99
View File
@@ -30,107 +30,113 @@ useSeoMeta({
<template> <template>
<div v-if="project"> <div v-if="project">
<!-- Back navigation --> <!-- Full-width hero image -->
<div class="bg-gray-50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-800"> <section class="relative overflow-hidden">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <!-- Hero image with overlay -->
<UButton <div class="relative h-[40vh] sm:h-[50vh] lg:h-[60vh]">
variant="ghost" <NuxtImg
icon="i-lucide-arrow-left" v-if="project.image"
to="/projects" :src="project.image"
size="sm" :alt="project.title"
class="text-gray-500 hover:text-gray-900 dark:hover:text-white" class="w-full h-full object-cover"
> format="webp"
{{ t('projects.projectDetail.backToProjects') }} loading="eager"
</UButton> />
</div> <!-- Gradient overlay -->
</div> <div class="absolute inset-0 bg-gradient-to-t from-white via-white/40 to-transparent dark:from-gray-950 dark:via-gray-950/40 dark:to-transparent" />
<div class="absolute inset-0 bg-gradient-to-r from-white/60 to-transparent dark:from-gray-950/60 dark:to-transparent" />
<!-- Hero section --> <!-- Back button (floating) -->
<section class="bg-gray-50 dark:bg-gray-900/30 pb-16 pt-8"> <div class="absolute top-6 left-4 sm:left-6 lg:left-8 z-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <UButton
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16 items-center"> variant="solid"
<!-- Project Image --> color="neutral"
<div class="rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-800 shadow-lg"> icon="i-lucide-arrow-left"
<NuxtImg to="/projects"
v-if="project.image" size="sm"
:src="project.image" class="shadow-lg backdrop-blur-sm"
:alt="project.title" >
class="w-full h-auto object-cover" {{ t('projects.projectDetail.backToProjects') }}
format="webp" </UButton>
loading="lazy" </div>
/>
</div>
<!-- Project Info --> <!-- Title overlay at bottom -->
<div class="flex flex-col justify-center space-y-6"> <div class="absolute bottom-0 left-0 right-0 z-10 px-4 sm:px-6 lg:px-8 pb-10">
<div class="flex items-center gap-3"> <div class="max-w-7xl mx-auto">
<div class="flex items-center gap-3 mb-4">
<UBadge v-if="project.category" variant="subtle" size="md">{{ project.category }}</UBadge> <UBadge v-if="project.category" variant="subtle" size="md">{{ project.category }}</UBadge>
<span v-if="project.date" class="text-sm text-gray-400 dark:text-gray-500 font-medium">{{ project.date }}</span> <span v-if="project.date" class="text-sm text-gray-500 dark:text-gray-400 font-mono">{{ project.date }}</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white">{{ project.title }}</h1>
<p class="text-lg text-gray-500 dark:text-gray-400 leading-relaxed">{{ project.description }}</p>
<!-- CTA Buttons -->
<div class="flex flex-wrap gap-3 pt-2">
<UButton
v-if="project.demoUrl"
:to="project.demoUrl"
target="_blank"
icon="i-lucide-external-link"
size="lg"
class="font-semibold"
>
{{ t('projects.projectDetail.viewDemo') }}
</UButton>
<UButton
v-if="project.githubUrl"
:to="project.githubUrl"
target="_blank"
variant="soft"
icon="i-lucide-github"
size="lg"
>
{{ t('projects.projectDetail.sourceCode') }}
</UButton>
<UButton
v-for="button in project.buttons"
:key="button.title"
:to="button.link"
target="_blank"
variant="outline"
icon="i-lucide-external-link"
size="lg"
>
{{ button.title }}
</UButton>
</div> </div>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white max-w-3xl tracking-tight">{{ project.title }}</h1>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Content --> <!-- Content area -->
<section class="py-16 px-4"> <section class="py-12 md:py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-10 lg:gap-16"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-10 lg:gap-16">
<!-- Main Content --> <!-- Main Content -->
<div class="lg:col-span-2 space-y-16"> <div class="lg:col-span-2 space-y-14">
<!-- About --> <!-- Description -->
<div> <div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.aboutProject') }}</h2> <p class="text-lg sm:text-xl text-gray-600 dark:text-gray-300 leading-relaxed mb-8">{{ project.description }}</p>
<!-- CTA Buttons -->
<div class="flex flex-wrap gap-3">
<UButton
v-if="project.demoUrl"
:to="project.demoUrl"
target="_blank"
icon="i-lucide-external-link"
size="lg"
class="font-semibold"
>
{{ t('projects.projectDetail.viewDemo') }}
</UButton>
<UButton
v-if="project.githubUrl"
:to="project.githubUrl"
target="_blank"
variant="soft"
icon="i-lucide-github"
size="lg"
>
{{ t('projects.projectDetail.sourceCode') }}
</UButton>
<UButton
v-for="button in project.buttons"
:key="button.title"
:to="button.link"
target="_blank"
variant="outline"
icon="i-lucide-external-link"
size="lg"
>
{{ button.title }}
</UButton>
</div>
</div>
<!-- About -->
<div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-3">
<div class="w-1 h-6 rounded-full bg-brand-500" />
{{ t('projects.projectDetail.aboutProject') }}
</h2>
<p class="text-gray-500 dark:text-gray-400 leading-relaxed text-lg"> <p class="text-gray-500 dark:text-gray-400 leading-relaxed text-lg">
{{ project.longDescription || project.description }} {{ project.longDescription || project.description }}
</p> </p>
<!-- Features --> <!-- Features -->
<div v-if="project.features" class="mt-8"> <div v-if="project.features" class="mt-8">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">{{ t('projects.projectDetail.keyFeatures') }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.keyFeatures') }}</h3>
<ul class="space-y-3"> <ul class="space-y-3">
<li v-for="feature in project.features" :key="feature" class="flex items-start gap-3"> <li v-for="feature in project.features" :key="feature" class="flex items-start gap-3 group">
<div class="w-6 h-6 rounded-full bg-brand-500/10 flex items-center justify-center shrink-0 mt-0.5"> <div class="w-6 h-6 rounded-lg bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0 mt-0.5 group-hover:bg-brand-500/20 transition-colors">
<UIcon name="i-lucide-check" class="text-brand-500 w-3.5 h-3.5" /> <UIcon name="i-lucide-check" class="text-brand-500 w-3.5 h-3.5" />
</div> </div>
<span class="text-gray-600 dark:text-gray-300">{{ feature }}</span> <span class="text-gray-600 dark:text-gray-300">{{ feature }}</span>
@@ -140,27 +146,33 @@ useSeoMeta({
</div> </div>
<!-- Technologies --> <!-- Technologies -->
<div v-if="project.technologies.length"> <div v-if="project.technologies.length" class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.technologiesUsed') }}</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-3">
<div class="w-1 h-6 rounded-full bg-brand-500" />
{{ t('projects.projectDetail.technologiesUsed') }}
</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<TechBadge v-for="tech in project.technologies" :key="tech" :tech="tech" /> <TechBadge v-for="tech in project.technologies" :key="tech" :tech="tech" />
</div> </div>
</div> </div>
<!-- Gallery Thumbnails --> <!-- Gallery Thumbnails -->
<div v-if="project.gallery?.length"> <div v-if="project.gallery?.length" class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-7 sm:p-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.gallery') }}</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-3">
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div class="w-1 h-6 rounded-full bg-brand-500" />
{{ t('projects.projectDetail.gallery') }}
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<button <button
v-for="(image, index) in project.gallery" v-for="(image, index) in project.gallery"
:key="index" :key="index"
class="relative rounded-xl overflow-hidden group cursor-pointer border border-gray-200 dark:border-gray-800" class="relative rounded-xl overflow-hidden group cursor-pointer border border-gray-200/80 dark:border-gray-800/50 aspect-video"
@click="galleryRef?.openGallery(index)" @click="galleryRef?.openGallery(index)"
> >
<NuxtImg <NuxtImg
:src="image" :src="image"
:alt="`${project.title} - Image ${index + 1}`" :alt="`${project.title} - Image ${index + 1}`"
class="w-full h-32 object-cover transition-transform duration-300 group-hover:scale-105" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy" loading="lazy"
format="webp" format="webp"
/> />
@@ -173,17 +185,20 @@ useSeoMeta({
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->
<aside class="space-y-6"> <aside class="sticky top-24 space-y-6">
<!-- Project Info Card --> <!-- Project Info Card -->
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 p-6 sticky top-24"> <div class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-6">
<h3 class="font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.projectInfo') }}</h3> <h3 class="font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2">
<UIcon name="i-lucide-info" class="text-brand-500 w-4 h-4" />
{{ t('projects.projectDetail.projectInfo') }}
</h3>
<div class="space-y-4 text-sm"> <div class="space-y-4 text-sm">
<div v-if="project.date" class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-800"> <div v-if="project.date" class="flex justify-between items-center py-3 border-b border-gray-200/60 dark:border-gray-800/40">
<span class="text-gray-500 dark:text-gray-400">{{ t('projects.projectDetail.date') }}</span> <span class="text-gray-500 dark:text-gray-400">{{ t('projects.projectDetail.date') }}</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ project.date }}</span> <span class="font-semibold text-gray-900 dark:text-white font-mono text-xs">{{ project.date }}</span>
</div> </div>
<div v-if="project.category" class="flex justify-between items-center py-2"> <div v-if="project.category" class="flex justify-between items-center py-3">
<span class="text-gray-500 dark:text-gray-400">{{ t('projects.projectDetail.category') }}</span> <span class="text-gray-500 dark:text-gray-400">{{ t('projects.projectDetail.category') }}</span>
<UBadge variant="subtle" size="xs">{{ project.category }}</UBadge> <UBadge variant="subtle" size="xs">{{ project.category }}</UBadge>
</div> </div>
@@ -191,14 +206,17 @@ useSeoMeta({
</div> </div>
<!-- Related Projects --> <!-- Related Projects -->
<div v-if="relatedProjects.length > 0" class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 p-6"> <div v-if="relatedProjects.length > 0" class="rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm p-6">
<h3 class="font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.relatedProjects') }}</h3> <h3 class="font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2">
<div class="space-y-4"> <UIcon name="i-lucide-layers" class="text-brand-500 w-4 h-4" />
{{ t('projects.projectDetail.relatedProjects') }}
</h3>
<div class="space-y-3">
<NuxtLink <NuxtLink
v-for="related in relatedProjects" v-for="related in relatedProjects"
:key="related.id" :key="related.id"
:to="`/project/${related.id}`" :to="`/project/${related.id}`"
class="flex gap-3 p-3 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors group" class="flex gap-3 p-3 rounded-xl border border-transparent hover:border-brand-500/20 hover:bg-brand-500/5 transition-all duration-200 group"
> >
<NuxtImg <NuxtImg
v-if="related.image" v-if="related.image"
@@ -211,7 +229,7 @@ useSeoMeta({
/> />
<div class="min-w-0"> <div class="min-w-0">
<p class="font-semibold text-sm text-gray-900 dark:text-white truncate group-hover:text-brand-500 transition-colors">{{ related.title }}</p> <p class="font-semibold text-sm text-gray-900 dark:text-white truncate group-hover:text-brand-500 transition-colors">{{ related.title }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">{{ related.description }}</p> <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mt-1">{{ related.description }}</p>
</div> </div>
</NuxtLink> </NuxtLink>
</div> </div>
+25 -22
View File
@@ -53,37 +53,40 @@ function resetFilters() {
<template> <template>
<div> <div>
<!-- Hero --> <!-- Hero -->
<section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30"> <section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="max-w-7xl mx-auto text-center"> <div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Portfolio</span> <div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-brand-500/5 dark:bg-brand-500/8 rounded-full blur-3xl pointer-events-none" aria-hidden="true" />
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-4 text-gray-900 dark:text-white">{{ t('projects.title') }}</h1>
<p class="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">{{ t('projects.subtitle') }}</p> <div class="relative z-10 max-w-7xl mx-auto text-center">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// portfolio</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mt-3 mb-5 bg-gradient-to-r from-gray-900 via-gray-800 to-gray-600 dark:from-white dark:via-gray-200 dark:to-gray-500 bg-clip-text text-transparent">{{ t('projects.title') }}</h1>
<p class="text-lg sm:text-xl text-gray-500 dark:text-gray-400 max-w-2xl mx-auto leading-relaxed">{{ t('projects.subtitle') }}</p>
<!-- Stats --> <!-- Stats -->
<div class="flex justify-center gap-10 mt-10"> <div class="flex justify-center gap-8 sm:gap-12 mt-12">
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ totalProjects }}</p> <p class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ totalProjects }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('nav.projects') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('nav.projects') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ featuredCount }}</p> <p class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ featuredCount }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('home.featuredProjects.title') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('home.featuredProjects.title') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" /> <div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ categories.length - 1 }}</p> <p class="text-3xl sm:text-4xl font-black bg-gradient-to-b from-brand-400 to-brand-600 bg-clip-text text-transparent">{{ categories.length - 1 }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('projects.categories.all') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 font-medium">{{ t('projects.categories.all') }}</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Filters & Grid --> <!-- Filters & Grid -->
<section class="py-12 px-4"> <section class="py-16 md:py-20 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<!-- Filter bar --> <!-- Filter bar -->
<div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center mb-10 p-4 rounded-2xl bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800"> <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center mb-12 p-4 sm:p-5 rounded-2xl border border-gray-200/80 dark:border-gray-800/50 bg-white/60 dark:bg-gray-900/40 backdrop-blur-sm">
<UInput <UInput
v-model="searchQuery" v-model="searchQuery"
icon="i-lucide-search" icon="i-lucide-search"
@@ -108,18 +111,18 @@ function resetFilters() {
</div> </div>
<!-- Projects Grid --> <!-- Projects Grid -->
<div v-if="filteredProjects.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8"> <div v-if="filteredProjects.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 lg:gap-6">
<ProjectCard v-for="project in filteredProjects" :key="project.id" :project="project" /> <ProjectCard v-for="project in filteredProjects" :key="project.id" :project="project" />
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div v-else class="text-center py-24"> <div v-else class="text-center py-32">
<div class="w-16 h-16 mx-auto mb-6 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center"> <div class="w-16 h-16 mx-auto mb-6 rounded-2xl bg-gray-100 dark:bg-gray-800/60 border border-gray-200/80 dark:border-gray-700/30 flex items-center justify-center">
<UIcon name="i-lucide-search-x" class="text-2xl text-gray-400" /> <UIcon name="i-lucide-search-x" class="text-2xl text-gray-400" />
</div> </div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ t('projects.noResults.title') }}</h3> <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">{{ t('projects.noResults.title') }}</h3>
<p class="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto">{{ t('projects.noResults.description') }}</p> <p class="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto leading-relaxed">{{ t('projects.noResults.description') }}</p>
<UButton @click="resetFilters" variant="soft" size="md"> <UButton @click="resetFilters" variant="soft" size="md" icon="i-lucide-rotate-ccw">
{{ t('common.reset') }} {{ t('common.reset') }}
</UButton> </UButton>
</div> </div>
+27 -17
View File
@@ -7,7 +7,7 @@
"fiverr": "Fiverr" "fiverr": "Fiverr"
}, },
"footer": { "footer": {
"copyright": "\u00a9 2026 Killian Dalcin", "copyright": "\u00a9 2026 Killian' DAL-CIN",
"navigation": "Quick Links", "navigation": "Quick Links",
"services": "Services", "services": "Services",
"legalNotices": "Legal Notices", "legalNotices": "Legal Notices",
@@ -20,36 +20,36 @@
} }
}, },
"a11y": { "a11y": {
"logoLabel": "Killian Dalcin \u2014 Full Stack Developer \u2014 Back to homepage", "logoLabel": "Killian' DAL-CIN \u2014 Full Stack Developer \u2014 Back to homepage",
"openMenu": "Open navigation menu", "openMenu": "Open navigation menu",
"closeMenu": "Close navigation menu", "closeMenu": "Close navigation menu",
"closeDrawer": "Close menu", "closeDrawer": "Close menu",
"langToggle": "Change language \u2014 currently English", "langToggle": "Change language \u2014 currently English",
"themeDark": "Switch to light mode", "themeDark": "Switch to light mode",
"themeLight": "Switch to dark mode", "themeLight": "Switch to dark mode",
"gitea": "Killian Dalcin on Gitea (opens in new tab)", "gitea": "Killian' DAL-CIN on Gitea (opens in new tab)",
"linkedin": "Killian Dalcin on LinkedIn (opens in new tab)", "linkedin": "Killian' DAL-CIN on LinkedIn (opens in new tab)",
"fiverr": "Killian Dalcin on Fiverr (opens in new tab)" "fiverr": "Killian' DAL-CIN on Fiverr (opens in new tab)"
}, },
"seo": { "seo": {
"home": { "home": {
"title": "Killian Dalcin \u2014 Freelance Full Stack Developer", "title": "Killian' DAL-CIN \u2014 Freelance Full Stack Developer",
"description": "Portfolio of Killian Dalcin, freelance full stack developer specializing in Vue.js, React and Node.js. High-performance web applications and custom solutions." "description": "Portfolio of Killian' DAL-CIN, freelance full stack developer specializing in Vue.js, React and Node.js. High-performance web applications and custom solutions."
}, },
"projects": { "projects": {
"title": "Projects \u2014 Killian Dalcin", "title": "Projects \u2014 Killian' DAL-CIN",
"description": "Discover my web development projects: Vue.js applications, Node.js APIs, Discord bots and enterprise solutions." "description": "Discover my web development projects: Vue.js applications, Node.js APIs, Discord bots and enterprise solutions."
}, },
"about": { "about": {
"title": "About \u2014 Killian Dalcin", "title": "About \u2014 Killian' DAL-CIN",
"description": "Biography and skills of Killian Dalcin, freelance full stack developer based in France." "description": "Biography and skills of Killian' DAL-CIN, freelance full stack developer based in France."
}, },
"contact": { "contact": {
"title": "Contact \u2014 Killian Dalcin", "title": "Contact \u2014 Killian' DAL-CIN",
"description": "Contact Killian Dalcin to discuss your web development project." "description": "Contact Killian' DAL-CIN to discuss your web development project."
}, },
"fiverr": { "fiverr": {
"title": "Fiverr Services \u2014 Killian Dalcin", "title": "Fiverr Services \u2014 Killian' DAL-CIN",
"description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications." "description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications."
} }
}, },
@@ -131,7 +131,7 @@
} }
}, },
"about": { "about": {
"title": "About Killian - Full Stack Developer", "title": "About Killian'- Full Stack Developer",
"subtitle": "Experienced web developer passionate about Vue.js, React, Node.js, and modern JavaScript technologies.", "subtitle": "Experienced web developer passionate about Vue.js, React, Node.js, and modern JavaScript technologies.",
"intro": { "intro": {
"title": "Professional Full Stack Developer", "title": "Professional Full Stack Developer",
@@ -245,6 +245,10 @@
"title": "They Transformed Their Business With My Services", "title": "They Transformed Their Business With My Services",
"subtitle": "Join 500+ satisfied entrepreneurs. Average rating 5.0/5.0 across all my services." "subtitle": "Join 500+ satisfied entrepreneurs. Average rating 5.0/5.0 across all my services."
}, },
"faq": {
"title": "Fiverr FAQ",
"subtitle": "Everything you need to know before ordering my services on Fiverr."
},
"cta": { "cta": {
"title": "Stop Searching, You Found THE Right Developer", "title": "Stop Searching, You Found THE Right Developer",
"subtitle": "Every day without action = lost opportunities. Launch your project NOW.", "subtitle": "Every day without action = lost opportunities. Launch your project NOW.",
@@ -263,7 +267,6 @@
"findMeOn": "Connect on Social Media", "findMeOn": "Connect on Social Media",
"methods": { "methods": {
"email": "Email Address", "email": "Email Address",
"phone": "Phone Number",
"location": "Location", "location": "Location",
"responseTime": "Response within 24 hours", "responseTime": "Response within 24 hours",
"availability": "Available for remote & freelance" "availability": "Available for remote & freelance"
@@ -285,16 +288,23 @@
} }
}, },
"form": { "form": {
"title": "Send me a message",
"name": "Your Name", "name": "Your Name",
"email": "Email Address", "email": "Email Address",
"subject": "Project Subject", "subject": "Project Subject",
"message": "Project Details", "message": "Project Details",
"submit": "Send Message",
"send": "Send Message", "send": "Send Message",
"sending": "Sending...", "sending": "Sending...",
"success": "Message sent successfully! I'll respond within 24 hours.", "success": "Message sent successfully! I'll respond within 24 hours.",
"error": "Error sending message. Please try again or email directly.", "error": "Error sending message. Please try again or email directly.",
"required": "This field is required", "required": "This field is required",
"invalidEmail": "Please enter a valid email address" "invalidEmail": "Please enter a valid email address",
"validation": {
"nameMin": "Name must be at least 2 characters",
"emailInvalid": "Please enter a valid email address",
"messageMin": "Message must be at least 10 characters"
}
}, },
"info": { "info": {
"title": "Let's Build Something Great", "title": "Let's Build Something Great",
@@ -434,4 +444,4 @@
"generic": "An error occurred.", "generic": "An error occurred.",
"backHome": "Back to home" "backHome": "Back to home"
} }
} }
+27 -17
View File
@@ -7,7 +7,7 @@
"fiverr": "Fiverr" "fiverr": "Fiverr"
}, },
"footer": { "footer": {
"copyright": "\u00a9 2026 Killian Dalcin", "copyright": "\u00a9 2026 Killian' DAL-CIN",
"navigation": "Liens Rapides", "navigation": "Liens Rapides",
"services": "Services", "services": "Services",
"legalNotices": "Mentions L\u00e9gales", "legalNotices": "Mentions L\u00e9gales",
@@ -20,36 +20,36 @@
} }
}, },
"a11y": { "a11y": {
"logoLabel": "Killian Dalcin \u2014 Developpeur Full Stack \u2014 Retour a l'accueil", "logoLabel": "Killian' DAL-CIN \u2014 Developpeur Full Stack \u2014 Retour a l'accueil",
"openMenu": "Ouvrir le menu de navigation", "openMenu": "Ouvrir le menu de navigation",
"closeMenu": "Fermer le menu de navigation", "closeMenu": "Fermer le menu de navigation",
"closeDrawer": "Fermer le menu", "closeDrawer": "Fermer le menu",
"langToggle": "Changer la langue \u2014 actuellement Francais", "langToggle": "Changer la langue \u2014 actuellement Francais",
"themeDark": "Activer le mode clair", "themeDark": "Activer le mode clair",
"themeLight": "Activer le mode sombre", "themeLight": "Activer le mode sombre",
"gitea": "Gitea de Killian Dalcin (nouvelle fenetre)", "gitea": "Gitea de Killian' DAL-CIN (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian Dalcin (nouvelle fenetre)", "linkedin": "LinkedIn de Killian' DAL-CIN (nouvelle fenetre)",
"fiverr": "Fiverr de Killian Dalcin (nouvelle fenetre)" "fiverr": "Fiverr de Killian' DAL-CIN (nouvelle fenetre)"
}, },
"seo": { "seo": {
"home": { "home": {
"title": "Killian Dalcin \u2014 Developpeur Full Stack Freelance", "title": "Killian' DAL-CIN \u2014 Developpeur Full Stack Freelance",
"description": "Portfolio de Killian Dalcin, developpeur full stack freelance specialise en Vue.js, React et Node.js. Applications web performantes et solutions sur-mesure." "description": "Portfolio de Killian' DAL-CIN, developpeur full stack freelance specialise en Vue.js, React et Node.js. Applications web performantes et solutions sur-mesure."
}, },
"projects": { "projects": {
"title": "Projets \u2014 Killian Dalcin", "title": "Projets \u2014 Killian' DAL-CIN",
"description": "Decouvrez mes realisations en developpement web : applications Vue.js, API Node.js, bots Discord et solutions d'entreprise." "description": "Decouvrez mes realisations en developpement web : applications Vue.js, API Node.js, bots Discord et solutions d'entreprise."
}, },
"about": { "about": {
"title": "A propos \u2014 Killian Dalcin", "title": "A propos \u2014 Killian' DAL-CIN",
"description": "Biographie et competences de Killian Dalcin, developpeur full stack freelance base en France." "description": "Biographie et competences de Killian' DAL-CIN, developpeur full stack freelance base en France."
}, },
"contact": { "contact": {
"title": "Contact \u2014 Killian Dalcin", "title": "Contact \u2014 Killian' DAL-CIN",
"description": "Contactez Killian Dalcin pour discuter de votre projet de developpement web." "description": "Contactez Killian' DAL-CIN pour discuter de votre projet de developpement web."
}, },
"fiverr": { "fiverr": {
"title": "Services Fiverr \u2014 Killian Dalcin", "title": "Services Fiverr \u2014 Killian' DAL-CIN",
"description": "Services de developpement disponibles sur Fiverr : bots Discord, plugins Minecraft, applications web." "description": "Services de developpement disponibles sur Fiverr : bots Discord, plugins Minecraft, applications web."
} }
}, },
@@ -131,7 +131,7 @@
} }
}, },
"about": { "about": {
"title": "\u00c0 propos de Killian - D\u00e9veloppeur Full Stack", "title": "\u00c0 propos de Killian'- D\u00e9veloppeur Full Stack",
"subtitle": "D\u00e9veloppeur web exp\u00e9riment\u00e9 passionn\u00e9 par Vue.js, React, Node.js et les technologies JavaScript modernes.", "subtitle": "D\u00e9veloppeur web exp\u00e9riment\u00e9 passionn\u00e9 par Vue.js, React, Node.js et les technologies JavaScript modernes.",
"intro": { "intro": {
"title": "D\u00e9veloppeur Full Stack Professionnel", "title": "D\u00e9veloppeur Full Stack Professionnel",
@@ -245,6 +245,10 @@
"title": "Ils Ont Transform\u00e9 Leur Business Avec Mes Services", "title": "Ils Ont Transform\u00e9 Leur Business Avec Mes Services",
"subtitle": "Rejoignez 500+ entrepreneurs satisfaits. Note moyenne 5.0/5.0 sur l'ensemble de mes services." "subtitle": "Rejoignez 500+ entrepreneurs satisfaits. Note moyenne 5.0/5.0 sur l'ensemble de mes services."
}, },
"faq": {
"title": "Questions Frequentes Fiverr",
"subtitle": "Tout ce que vous devez savoir avant de commander mes services sur Fiverr."
},
"cta": { "cta": {
"title": "Arr\u00eatez de Chercher, Vous Avez Trouv\u00e9 LE Bon D\u00e9veloppeur", "title": "Arr\u00eatez de Chercher, Vous Avez Trouv\u00e9 LE Bon D\u00e9veloppeur",
"subtitle": "Chaque jour sans agir = opportunit\u00e9s perdues. Lancez votre projet MAINTENANT.", "subtitle": "Chaque jour sans agir = opportunit\u00e9s perdues. Lancez votre projet MAINTENANT.",
@@ -263,7 +267,6 @@
"findMeOn": "Connectez-vous sur les R\u00e9seaux Sociaux", "findMeOn": "Connectez-vous sur les R\u00e9seaux Sociaux",
"methods": { "methods": {
"email": "Adresse Email", "email": "Adresse Email",
"phone": "Num\u00e9ro de T\u00e9l\u00e9phone",
"location": "Localisation", "location": "Localisation",
"responseTime": "R\u00e9ponse sous 24 heures", "responseTime": "R\u00e9ponse sous 24 heures",
"availability": "Disponible pour remote & freelance" "availability": "Disponible pour remote & freelance"
@@ -285,16 +288,23 @@
} }
}, },
"form": { "form": {
"title": "Envoyez-moi un message",
"name": "Votre Nom", "name": "Votre Nom",
"email": "Adresse Email", "email": "Adresse Email",
"subject": "Sujet du Projet", "subject": "Sujet du Projet",
"message": "D\u00e9tails du Projet", "message": "D\u00e9tails du Projet",
"submit": "Envoyer le Message",
"send": "Envoyer le Message", "send": "Envoyer le Message",
"sending": "Envoi en cours...", "sending": "Envoi en cours...",
"success": "Message envoy\u00e9 avec succ\u00e8s ! Je r\u00e9pondrai dans les 24 heures.", "success": "Message envoy\u00e9 avec succ\u00e8s ! Je r\u00e9pondrai dans les 24 heures.",
"error": "Erreur lors de l'envoi du message. Veuillez r\u00e9essayer ou envoyer un email directement.", "error": "Erreur lors de l'envoi du message. Veuillez r\u00e9essayer ou envoyer un email directement.",
"required": "Ce champ est requis", "required": "Ce champ est requis",
"invalidEmail": "Veuillez entrer une adresse email valide" "invalidEmail": "Veuillez entrer une adresse email valide",
"validation": {
"nameMin": "Le nom doit contenir au moins 2 caracteres",
"emailInvalid": "Veuillez entrer une adresse email valide",
"messageMin": "Le message doit contenir au moins 10 caracteres"
}
}, },
"info": { "info": {
"title": "Construisons Quelque Chose de Grand", "title": "Construisons Quelque Chose de Grand",
@@ -434,4 +444,4 @@
"generic": "Une erreur est survenue.", "generic": "Une erreur est survenue.",
"backHome": "Retour a l'accueil" "backHome": "Retour a l'accueil"
} }
} }
+25 -26
View File
@@ -26,41 +26,40 @@ export default defineNuxtConfig({
fallback: 'dark', fallback: 'dark',
storage: 'cookie', storage: 'cookie',
storageKey: 'nuxt-color-mode', storageKey: 'nuxt-color-mode',
cookieName: 'nuxt-color-mode',
classSuffix: '' classSuffix: ''
}, },
site: { site: {
url: 'https://killiandalcin.fr', url: 'https://killiandalcin.fr',
name: 'Killian Dalcin - Developpeur Full Stack' name: "Killian' DAL-CIN - Developpeur Full Stack"
}, },
i18n: { i18n: {
strategy: 'prefix_except_default', strategy: 'prefix_except_default',
defaultLocale: 'fr', defaultLocale: 'fr',
baseUrl: 'https://killiandalcin.fr', baseUrl: 'https://killiandalcin.fr',
locales: [ locales: [
{ code: 'fr', language: 'fr-FR', file: 'fr.json' }, { code: 'fr', language: 'fr-FR', file: 'fr.json' },
{ code: 'en', language: 'en-US', file: 'en.json' }, { code: 'en', language: 'en-US', file: 'en.json' },
], ],
langDir: 'locales/', langDir: 'locales/',
detectBrowserLanguage: { detectBrowserLanguage: {
useCookie: true, useCookie: true,
cookieKey: 'i18n_redirected', cookieKey: 'i18n_redirected',
redirectOn: 'root', redirectOn: 'root',
},
}, },
},
runtimeConfig: { runtimeConfig: {
smtpHost: '', smtpHost: '',
smtpUser: '', smtpUser: '',
smtpPass: '', smtpPass: '',
smtpTo: '', smtpTo: '',
public: { public: {
gtag: { gtag: {
id: '', id: '',
},
}, },
}, },
},
gtag: { gtag: {
id: '', id: '',
enabled: import.meta.env.NODE_ENV === 'production', enabled: import.meta.env.NODE_ENV === 'production',
} }
}) })
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "Killian - Full Stack Developer Portfolio", "name": "Killian'- Full Stack Developer Portfolio",
"short_name": "Killian Portfolio", "short_name": "Killian'Portfolio",
"description": "Professional Full Stack Developer specializing in Vue.js, React, Node.js. Expert in Discord bots, web applications, and custom software solutions.", "description": "Professional Full Stack Developer specializing in Vue.js, React, Node.js. Expert in Discord bots, web applications, and custom software solutions.",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
-1
View File
@@ -63,7 +63,6 @@ export interface FAQ {
export interface ContactInfo { export interface ContactInfo {
email: string email: string
phone: string
location: string location: string
} }