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
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
+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
**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
+1 -1
View File
@@ -171,7 +171,7 @@ export interface TechStack {
```vue
<template>
<div>
<h1>Portfolio Killian Dalcin</h1>
<h1>Portfolio Killian' DAL-CIN</h1>
<p>Nuxt 4 Foundation</p>
</div>
</template>
+31 -31
View File
@@ -120,7 +120,7 @@ export default defineAppConfig({
- `css: ['~/assets/css/main.css']`
- `colorMode` block: `{ preference: 'dark', fallback: 'dark', storage: 'cookie', storageKey: 'nuxt-color-mode', cookieName: 'nuxt-color-mode', classSuffix: '' }`
- `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.
@@ -171,44 +171,44 @@ fr.json top-level structure:
"formation": "Formation"
},
"footer": {
"copyright": "© 2026 Killian Dalcin"
"copyright": "© 2026 Killian' DAL-CIN"
},
"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",
"closeMenu": "Fermer le menu de navigation",
"closeDrawer": "Fermer le menu",
"langToggle": "Changer la langue — actuellement Francais",
"themeDark": "Activer le mode clair",
"themeLight": "Activer le mode sombre",
"github": "GitHub de Killian Dalcin (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian Dalcin (nouvelle fenetre)",
"fiverr": "Fiverr de Killian Dalcin (nouvelle fenetre)"
"github": "GitHub de Killian' DAL-CIN (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian' DAL-CIN (nouvelle fenetre)",
"fiverr": "Fiverr de Killian' DAL-CIN (nouvelle fenetre)"
},
"seo": {
"home": {
"title": "Killian Dalcin — 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."
"title": "Killian' DAL-CIN — Developpeur Full Stack Freelance",
"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": {
"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."
},
"about": {
"title": "A propos — Killian Dalcin",
"description": "Biographie et competences de Killian Dalcin, developpeur full stack freelance base en France."
"title": "A propos — Killian' DAL-CIN",
"description": "Biographie et competences de Killian' DAL-CIN, developpeur full stack freelance base en France."
},
"contact": {
"title": "Contact — Killian Dalcin",
"description": "Contactez Killian Dalcin pour discuter de votre projet de developpement web."
"title": "Contact — Killian' DAL-CIN",
"description": "Contactez Killian' DAL-CIN pour discuter de votre projet de developpement web."
},
"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."
},
"formation": {
"title": "Formation — Killian Dalcin",
"description": "Formations et cours proposes par Killian Dalcin en developpement web."
"title": "Formation — Killian' DAL-CIN",
"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"
},
"footer": {
"copyright": "© 2026 Killian Dalcin"
"copyright": "© 2026 Killian' DAL-CIN"
},
"a11y": {
"logoLabel": "Killian Dalcin — Full Stack Developer — Back to homepage",
"logoLabel": "Killian' DAL-CIN — Full Stack Developer — Back to homepage",
"openMenu": "Open navigation menu",
"closeMenu": "Close navigation menu",
"closeDrawer": "Close menu",
"langToggle": "Change language — currently English",
"themeDark": "Switch to light mode",
"themeLight": "Switch to dark mode",
"github": "Killian Dalcin on GitHub (opens in new tab)",
"linkedin": "Killian Dalcin on LinkedIn (opens in new tab)",
"fiverr": "Killian Dalcin on Fiverr (opens in new tab)"
"github": "Killian' DAL-CIN on GitHub (opens in new tab)",
"linkedin": "Killian' DAL-CIN on LinkedIn (opens in new tab)",
"fiverr": "Killian' DAL-CIN on Fiverr (opens in new tab)"
},
"seo": {
"home": {
"title": "Killian Dalcin — 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."
"title": "Killian' DAL-CIN — Freelance Full Stack Developer",
"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": {
"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."
},
"about": {
"title": "About — Killian Dalcin",
"description": "Biography and skills of Killian Dalcin, freelance full stack developer based in France."
"title": "About — Killian' DAL-CIN",
"description": "Biography and skills of Killian' DAL-CIN, freelance full stack developer based in France."
},
"contact": {
"title": "Contact — Killian Dalcin",
"description": "Contact Killian Dalcin to discuss your web development project."
"title": "Contact — Killian' DAL-CIN",
"description": "Contact Killian' DAL-CIN to discuss your web development project."
},
"fiverr": {
"title": "Fiverr Services — Killian Dalcin",
"title": "Fiverr Services — Killian' DAL-CIN",
"description": "Development services available on Fiverr: Discord bots, Minecraft plugins, web applications."
},
"formation": {
"title": "Training — Killian Dalcin",
"description": "Training and courses offered by Killian Dalcin in web development."
"title": "Training — Killian' DAL-CIN",
"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:**
- `<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):**
- 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" />`
- 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):
```vue
+2 -2
View File
@@ -128,7 +128,7 @@ useHead({
'@graph': [
{
'@type': 'Person',
name: 'Killian Dalcin',
name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr',
jobTitle: 'Developpeur Full Stack Freelance',
email: 'contact@killiandalcin.fr',
@@ -140,7 +140,7 @@ useHead({
},
{
'@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack',
name: 'Killian' DAL-CIN - Developpeur Full Stack',
url: 'https://killiandalcin.fr',
logo: 'https://killiandalcin.fr/images/logo.webp',
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
### 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
- **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
- **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
### Sitemap
+7 -7
View File
@@ -15,13 +15,13 @@
- **D-02:** Mobile nav via UDrawer (Nuxt UI v3) — hamburger opens left-sliding drawer
- **D-03:** Header sticky permanent
- **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-07:** @nuxtjs/i18n already configured: strategy prefix_except_default, FR default, browser detection + cookie
- **D-08:** Dark mode default for new visitors
- **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-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-13:** All public pages in sitemap except 404
- **D-14:** hreflang FR/EN alternates automatic via @nuxtjs/sitemap + @nuxtjs/i18n integration
@@ -240,7 +240,7 @@ useHead({
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Killian Dalcin',
name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr',
jobTitle: 'Développeur Full Stack Freelance',
sameAs: [
@@ -326,7 +326,7 @@ useSeoMeta({
// nuxt.config.ts
site: {
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: {
url: 'https://killiandalcin.fr',
name: 'Killian Dalcin — Développeur Full Stack',
name: 'Killian' DAL-CIN Développeur Full Stack',
},
colorMode: {
@@ -549,7 +549,7 @@ useHead({
{
'@type': 'Person',
'@id': 'https://killiandalcin.fr/#person',
name: 'Killian Dalcin',
name: 'Killian' DAL-CIN',
url: 'https://killiandalcin.fr',
jobTitle: 'Développeur Full Stack Freelance',
email: 'contact@killiandalcin.fr',
@@ -561,7 +561,7 @@ useHead({
{
'@type': 'ProfessionalService',
'@id': 'https://killiandalcin.fr/#service',
name: 'Killian Dalcin — Développeur Full Stack',
name: 'Killian' DAL-CIN Développeur Full Stack',
url: 'https://killiandalcin.fr',
provider: { '@id': 'https://killiandalcin.fr/#person' },
priceRange: '€€€',
+6 -6
View File
@@ -145,7 +145,7 @@ Components delivered in this phase only:
### AppFooter (COMP-06)
- 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`
- 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`)
- 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`
@@ -175,7 +175,7 @@ Phase 2 scope: header nav labels, footer copyright, mobile drawer, language/them
| 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: Projects | "Projets" | "Projects" |
| 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" |
| 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" |
| Footer copyright | "© 2026 Killian Dalcin" | "© 2026 Killian Dalcin" |
| GitHub icon aria-label | "GitHub de Killian Dalcin (nouvelle fenêtre)" | "Killian Dalcin 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)" |
| Fiverr icon aria-label | "Fiverr de Killian Dalcin (nouvelle fenêtre)" | "Killian Dalcin on Fiverr (opens in new tab)" |
| Footer copyright | "© 2026 Killian' DAL-CIN" | "© 2026 Killian' DAL-CIN" |
| 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' DAL-CIN (nouvelle fenêtre)" | "Killian' DAL-CIN on LinkedIn (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.
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` |
| 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` |
---
+1 -1
View File
@@ -1,6 +1,6 @@
# Architecture Patterns
**Project:** Portfolio Killian Dalcin — Nuxt 4 SSR Migration
**Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
**Researched:** 2026-04-07
**Confidence:** HIGH (based on official Nuxt 4 conventions + existing codebase analysis)
+1 -1
View File
@@ -1,6 +1,6 @@
# Technology Stack
**Project:** Portfolio Killian Dalcin — Nuxt 4 SSR Migration
**Project:** Portfolio Killian' DAL-CIN — Nuxt 4 SSR Migration
**Researched:** 2026-04-07
**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
**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
**Sources:** STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md
+2 -2
View File
@@ -1,9 +1,9 @@
<!-- GSD:project-start source:PROJECT.md -->
## 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.
+2 -2
View File
@@ -6,7 +6,7 @@ A modern, responsive personal portfolio website showcasing professional skills,
## 🎯 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
- **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
**Killian Dal Cin**
**Killian' DAL-CIN**
- Email: contact@killiandalcin.fr
- LinkedIn: [killian-dalcin](https://linkedin.com/in/killian-dal-cin)
+41 -14
View File
@@ -14,10 +14,10 @@ const schema = z.object({
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
name: undefined,
email: undefined,
message: undefined,
const state = reactive({
name: '',
email: '',
message: '',
})
async function onSubmit(event: FormSubmitEvent<Schema>) {
@@ -29,10 +29,9 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
color: 'success',
icon: 'i-lucide-check',
})
// Reset form
state.name = undefined
state.email = undefined
state.message = undefined
state.name = ''
state.email = ''
state.message = ''
} catch {
toast.add({
title: t('contact.form.error'),
@@ -46,21 +45,49 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
</script>
<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">
<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 :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 :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>
<UButton type="submit" :loading="loading" size="lg" class="self-start">
<button
type="submit"
: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') }}
</UButton>
<UIcon name="i-lucide-send" class="w-4 h-4" />
</template>
</button>
</UForm>
</template>
+18 -16
View File
@@ -17,7 +17,7 @@ const translatedCategory = computed(() => {
<template>
<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
itemtype="https://schema.org/CreativeWork"
>
@@ -30,21 +30,26 @@ const translatedCategory = computed(() => {
format="webp"
width="400"
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"
/>
<!-- Overlay on hover -->
<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" />
<!-- Gradient overlay -->
<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>
<!-- Content -->
<div class="p-5 flex flex-col gap-3">
<div class="p-5 sm:p-6 flex flex-col gap-3">
<!-- Category & Date -->
<div class="flex items-center justify-between">
<UBadge v-if="project.category" color="primary" variant="subtle" itemprop="genre">
{{ translatedCategory }}
</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 }}
</time>
</div>
@@ -60,29 +65,26 @@ const translatedCategory = computed(() => {
</p>
<!-- 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
v-for="tech in project.technologies.slice(0, 3)"
: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 }}
</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 }}
</span>
</div>
</div>
<!-- Action link -->
<!-- Hidden SEO 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"
class="absolute inset-0 z-10"
: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>
/>
</article>
</template>
+1 -1
View File
@@ -39,7 +39,7 @@ defineExpose({ openGallery })
<template>
<UModal v-model:open="isOpen" fullscreen>
<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">
<h3 class="text-lg font-semibold">{{ projectTitle }}</h3>
<UButton
+42 -39
View File
@@ -18,70 +18,73 @@ const quickLinks = computed(() => [
</script>
<template>
<footer class="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
<div class="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-8">
<!-- Branding & Tagline -->
<div class="space-y-4">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2.5">
<NuxtImg
src="/images/logo.webp"
alt="Killian Dalcin"
width="36"
height="36"
loading="lazy"
class="rounded-lg"
/>
<span class="text-lg font-bold text-gray-900 dark:text-white">Killian Dalcin</span>
<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-16 md:py-20">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 lg:gap-8">
<!-- Brand column -->
<div class="sm:col-span-2 lg:col-span-1 space-y-5">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2.5 group">
<NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="36" height="36" loading="lazy"
class="rounded-lg transition-transform duration-300 group-hover:scale-110" />
<span class="text-lg font-bold text-gray-900 dark:text-white">Killian' DAL-CIN</span>
</NuxtLink>
<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.
</p>
</div>
<!-- Quick Links -->
<!-- Navigation links -->
<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
</h3>
<nav class="flex flex-col gap-2.5">
<NuxtLink
v-for="link in quickLinks"
: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"
>
<nav class="flex flex-col gap-3">
<NuxtLink v-for="link in quickLinks" :key="link.key" :to="localePath(link.path)"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors duration-200">
{{ t(`nav.${link.key}`) }}
</NuxtLink>
</nav>
</div>
<!-- Social Links -->
<!-- Services links -->
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">
Social
<h3 class="font-mono text-xs text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-5">
Services
</h3>
<div class="flex items-center gap-3">
<a
v-for="link in socialLinks"
:key="link.name"
:href="link.url"
target="_blank"
rel="noopener noreferrer"
<nav class="flex flex-col gap-3">
<span class="text-sm text-gray-600 dark:text-gray-400">Web Development</span>
<span class="text-sm text-gray-600 dark:text-gray-400">Hytale Plugins</span>
<span class="text-sm text-gray-600 dark:text-gray-400">Consulting</span>
<span class="text-sm text-gray-600 dark:text-gray-400">Maintenance</span>
</nav>
</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)"
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"
>
<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-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"
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>
</div>
</div>
</div>
<!-- Bottom bar -->
<div class="mt-10 pt-6 border-t border-gray-200 dark:border-gray-800">
<p class="text-sm text-gray-400 dark:text-gray-500 text-center">
<div
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') }}
</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>
</footer>
+21 -73
View File
@@ -27,40 +27,26 @@ function isActive(path: string): boolean {
</script>
<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="flex items-center justify-between h-16">
<!-- Logo -->
<NuxtLink
:to="localePath('/')"
:aria-label="t('a11y.logoLabel')"
class="flex items-center gap-2.5 shrink-0"
>
<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 :to="localePath('/')" :aria-label="t('a11y.logoLabel')" class="flex items-center gap-2.5 shrink-0">
<NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" 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>
<!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-1" aria-label="Main navigation">
<NuxtLink
v-for="link in navLinks"
:key="link.key"
:to="localePath(link.path)"
<NuxtLink v-for="link in navLinks" :key="link.key" :to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined"
class="px-3 py-2 text-sm font-medium rounded-lg transition-colors"
:class="[
class="px-3 py-2 text-sm font-medium rounded-lg transition-colors" :class="[
isActive(link.path)
? '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'
]"
>
]">
{{ t(`nav.${link.key}`) }}
</NuxtLink>
</nav>
@@ -68,36 +54,19 @@ function isActive(path: string): boolean {
<!-- Right actions -->
<div class="flex items-center gap-1">
<!-- Language toggle -->
<UButton
variant="ghost"
color="neutral"
size="sm"
:aria-label="t('a11y.langToggle')"
@click="toggleLocale"
>
<UButton variant="ghost" color="neutral" size="sm" :aria-label="t('a11y.langToggle')" @click="toggleLocale">
{{ locale === 'fr' ? 'EN' : 'FR' }}
</UButton>
<!-- Theme toggle -->
<UButton
variant="ghost"
color="neutral"
size="sm"
<UButton variant="ghost" color="neutral" size="sm"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
:aria-label="colorMode.value === 'dark' ? t('a11y.themeDark') : t('a11y.themeLight')"
@click="toggleTheme"
/>
@click="toggleTheme" />
<!-- Mobile hamburger -->
<UButton
variant="ghost"
color="neutral"
size="sm"
icon="i-lucide-menu"
class="md:hidden"
:aria-label="t('a11y.openMenu')"
@click="mobileOpen = true"
/>
<UButton variant="ghost" color="neutral" size="sm" icon="i-lucide-menu" class="md:hidden"
:aria-label="t('a11y.openMenu')" @click="mobileOpen = true" />
</div>
</div>
</div>
@@ -106,32 +75,20 @@ function isActive(path: string): boolean {
<USlideover v-model:open="mobileOpen" side="left" class="md:hidden">
<template #header>
<div class="flex items-center gap-2.5">
<NuxtImg
src="/images/logo.webp"
alt="Killian Dalcin"
width="32"
height="32"
class="rounded-lg"
/>
<NuxtImg src="/images/logo.webp" alt="Killian' DAL-CIN" width="32" height="32" class="rounded-lg" />
<span class="text-base font-semibold text-gray-900 dark:text-white">Killian</span>
</div>
</template>
<template #body>
<nav class="flex flex-col gap-1" aria-label="Mobile navigation">
<NuxtLink
v-for="link in navLinks"
:key="link.key"
:to="localePath(link.path)"
<NuxtLink v-for="link in navLinks" :key="link.key" :to="localePath(link.path)"
:aria-current="isActive(link.path) ? 'page' : undefined"
class="px-4 py-3 text-base font-medium rounded-lg transition-colors"
:class="[
class="px-4 py-3 text-base font-medium rounded-lg transition-colors" :class="[
isActive(link.path)
? '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'
]"
@click="mobileOpen = false"
>
]" @click="mobileOpen = false">
{{ t(`nav.${link.key}`) }}
</NuxtLink>
</nav>
@@ -139,21 +96,12 @@ function isActive(path: string): boolean {
<template #footer>
<div class="flex items-center gap-2">
<UButton
variant="ghost"
color="neutral"
:aria-label="t('a11y.langToggle')"
@click="toggleLocale"
>
<UButton variant="ghost" color="neutral" :aria-label="t('a11y.langToggle')" @click="toggleLocale">
{{ locale === 'fr' ? 'EN' : 'FR' }}
</UButton>
<UButton
variant="ghost"
color="neutral"
:icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
<UButton 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')"
@click="toggleTheme"
/>
@click="toggleTheme" />
</div>
</template>
</USlideover>
+49 -13
View File
@@ -1,26 +1,62 @@
<script setup lang="ts">
const { t } = useI18n()
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>
<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="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">
<!-- Decorative shapes -->
<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 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" />
<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">
<!-- Subtle dot pattern -->
<div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.06]" 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">
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('home.cta2.title') }}</h2>
<p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('home.cta2.subtitle') }}</p>
<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 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">
<UButton :to="localePath('/contact')" size="xl" color="white" class="font-semibold">
{{ t('home.cta2.startProject') }}
</UButton>
<UButton :to="localePath('/about')" size="xl" variant="outline" color="white" class="font-semibold">
{{ t('home.cta2.learnMore') }}
</UButton>
<NuxtLink
:to="external ? props.primaryTo : localePath(props.primaryTo)"
:target="external ? '_blank' : undefined"
:rel="external ? 'noopener noreferrer' : undefined"
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"
>
{{ 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>
+6 -6
View File
@@ -20,15 +20,15 @@ const items = computed(() =>
</script>
<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="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">FAQ</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ title }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3">{{ subtitle }}</p>
<div class="text-center mb-16">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// faq</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">{{ title }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 leading-relaxed">{{ subtitle }}</p>
</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 />
</div>
</div>
@@ -4,27 +4,31 @@ const { featuredProjects } = useProjects()
</script>
<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">
<!-- 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>
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase 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>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl">{{ t('home.featuredProjects.subtitle') }}</p>
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// portfolio</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.featuredProjects.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl leading-relaxed">{{ t('home.featuredProjects.subtitle') }}</p>
</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') }}
</UButton>
</div>
<!-- Projects grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
<ProjectCard
v-for="project in featuredProjects"
<!-- Bento grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 lg:gap-6 auto-rows-fr">
<div
v-for="(project, index) in featuredProjects"
: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>
</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">
<!-- Dot grid background pattern -->
<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>
<!-- 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="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<!-- Left: Content -->
<div class="space-y-8">
<!-- 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="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" />
@@ -28,8 +32,11 @@ const localePath = useLocalePath()
<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]">
<span class="text-gray-900 dark:text-white">{{ t('home.title').split(' ').slice(0, -2).join(' ') }} </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>
<span class="text-gray-900 dark:text-white">{{ t('home.title').split(' ').slice(0, -2).join(' ') }}
</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>
<p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-xl leading-relaxed">
{{ t('home.subtitle') }}
@@ -38,15 +45,27 @@ const localePath = useLocalePath()
<!-- CTA Buttons -->
<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') }}
</UButton>
<UButton :to="localePath('/fiverr')" size="xl" variant="outline" icon="i-lucide-dollar-sign" trailing class="font-semibold">
<UIcon name="i-lucide-arrow-right" class="w-4 h-4" />
</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') }}
</UButton>
<UButton :to="localePath('/contact')" size="xl" variant="ghost" icon="i-lucide-message-circle" trailing class="font-semibold">
<UIcon name="i-lucide-external-link" class="w-4 h-4" />
</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') }}
</UButton>
<UIcon name="i-lucide-message-circle" class="w-4 h-4" />
</NuxtLink>
</div>
</div>
@@ -54,9 +73,11 @@ const localePath = useLocalePath()
<div class="hidden lg:block" aria-hidden="true">
<div class="relative">
<!-- 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 -->
<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="w-3 h-3 rounded-full bg-red-400/80" />
<div class="w-3 h-3 rounded-full bg-yellow-400/80" />
@@ -75,11 +96,13 @@ const localePath = useLocalePath()
</div>
<div class="pl-6">
<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 class="pl-6">
<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 class="pl-6">
<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 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">'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 class="pl-10">
<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>
</div>
<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>
</div>
<div>
@@ -116,11 +141,13 @@ const localePath = useLocalePath()
</div>
<!-- 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="text-gray-700 dark:text-gray-300">50+ projects</span>
</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" />
<span class="text-gray-700 dark:text-gray-300">5.0 rating</span>
</div>
+19 -10
View File
@@ -26,29 +26,38 @@ const services = computed(() => [
</script>
<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">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Services</span>
<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>
<section class="py-24 md:py-32 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
<!-- Subtle background gradient -->
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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 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
v-for="(service, index) in services"
: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"
>
<!-- Hover glow effect -->
<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">
<!-- Icon -->
<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="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">{{ service.title }}</h3>
<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>
</section>
</template>
+27 -24
View File
@@ -5,40 +5,43 @@ const { t } = useI18n()
</script>
<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="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">{{ t('testimonials.title') }}</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ 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>
<div class="text-center mb-16">
<span class="font-mono text-sm text-brand-500 dark:text-brand-400 tracking-wider">// testimonials</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('testimonials.title') }}</h2>
<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 -->
<div class="flex justify-center gap-10 mt-10">
<div class="text-center">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.totalReviews }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.clients') }}</p>
<div class="flex justify-center gap-8 sm:gap-12 mt-12">
<div class="text-center group">
<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-2 font-medium">{{ t('testimonials.stats.clients') }}</p>
</div>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.averageRating }}/5</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.rating') }}</p>
<div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center group">
<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-2 font-medium">{{ t('testimonials.stats.rating') }}</p>
</div>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center">
<p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.projectsCompleted }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.projects') }}</p>
<div class="w-px bg-gradient-to-b from-transparent via-gray-300 dark:via-gray-700 to-transparent" />
<div class="text-center group">
<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-2 font-medium">{{ t('testimonials.stats.projects') }}</p>
</div>
</div>
</div>
<!-- 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
v-for="(testimonial, index) in testimonials"
: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 -->
<div class="flex gap-1">
<UIcon
@@ -51,23 +54,23 @@ const { t } = useI18n()
</div>
<!-- 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 }}"
</p>
<!-- 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
:src="testimonial.avatar"
:alt="testimonial.name"
width="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"
/>
<div>
<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>
+2 -3
View File
@@ -4,7 +4,7 @@ export type { SiteConfig, ContactInfo, SocialLink, FiverrService, FiverrConfig }
export const siteConfig: SiteConfig = {
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:
'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',
@@ -12,7 +12,6 @@ export const siteConfig: SiteConfig = {
contact: {
email: 'contact@killiandalcin.fr',
phone: '+33 6 49 19 38 16',
location: 'France',
},
@@ -92,7 +91,7 @@ export const siteConfig: SiteConfig = {
},
organization: {
'@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack',
name: "Killian' DAL-CIN - Developpeur Full Stack",
logo: 'https://killiandalcin.fr/logo.webp',
priceRange: '$$$',
aggregateRating: {
+13 -10
View File
@@ -10,29 +10,32 @@ function handleError() {
</script>
<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 -->
<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" />
<!-- 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 class="relative z-10 text-center space-y-6 max-w-lg">
<!-- Error code with glitch-like styling -->
<div class="relative z-10 text-center space-y-8 max-w-lg">
<!-- Error code -->
<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 }}
</h1>
<!-- Shadow text 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">
<!-- Shadow glow behind -->
<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 }}
</span>
</div>
<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') }}
</p>
<p class="text-gray-500 dark:text-gray-400">
<p class="text-gray-500 dark:text-gray-400 leading-relaxed">
{{ error.statusCode === 404
? 'The page you are looking for does not exist or has been moved.'
: 'Something unexpected happened. Please try again.'
@@ -40,7 +43,7 @@ function handleError() {
</p>
</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') }}
</UButton>
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
<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 />
<main class="flex-1">
<slot />
+47 -51
View File
@@ -65,42 +65,45 @@ const approachCards = computed(() => [
<template>
<div>
<!-- Hero Section -->
<section class="pt-16 pb-20 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto text-center">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">About</span>
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<section class="relative pt-20 pb-20 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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') }}
</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') }}
</p>
<div class="space-y-4 text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto leading-relaxed">
<p>{{ t('about.intro.content') }}</p>
<p>{{ t('about.experience.content') }}</p>
<div class="max-w-3xl mx-auto space-y-6">
<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 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>
</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="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase 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>
<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">// tech-stack</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.skills.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 max-w-2xl mx-auto leading-relaxed">
{{ t('about.subtitle') }}
</p>
</div>
<!-- Tech Categories Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Tech Categories Bento Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-5">
<div
v-for="category in techCategories"
: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="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" />
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">{{ category.title }}</h3>
@@ -117,9 +120,9 @@ const approachCards = computed(() => [
</div>
<!-- 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="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" />
</div>
<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>
<!-- Approach Section -->
<section class="py-20 md:py-28 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-14">
<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>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto">
<section class="relative py-24 md:py-32 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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">// 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') }}
</p>
</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
v-for="(card, index) in approachCards"
: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">
<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">
<!-- Hover glow -->
<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" />
</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>
</div>
</div>
@@ -168,26 +177,13 @@ const approachCards = computed(() => [
</section>
<!-- CTA Section -->
<section class="py-20 md:py-28 px-4">
<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="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 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" />
<div class="relative z-10">
<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>
<CTASection
:title="t('about.cta.title')"
:subtitle="t('about.cta.description')"
:primary-text="t('about.cta.button')"
primary-to="/contact"
:secondary-text="t('home.cta.viewProjects')"
secondary-to="/projects"
/>
</div>
</template>
+62 -57
View File
@@ -18,43 +18,49 @@ useSeoMeta({
<template>
<div>
<!-- Hero Section -->
<section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto text-center">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Contact</span>
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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') }}
</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') }}
</p>
<!-- 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-3xl font-extrabold text-brand-500 dark:text-brand-400">24-48h</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.responseTime') }}</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-2 font-medium">{{ t('contact.stats.responseTime') }}</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-3xl font-extrabold text-brand-500 dark:text-brand-400">100%</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.satisfaction') }}</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-2 font-medium">{{ t('contact.stats.satisfaction') }}</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-3xl font-extrabold text-brand-500 dark:text-brand-400">Remote</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.collaboration') }}</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-2 font-medium">{{ t('contact.stats.collaboration') }}</div>
</div>
</div>
</div>
</section>
<!-- Two Column Layout -->
<section class="py-16 md:py-20 px-4">
<div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-5 gap-10 lg:gap-16">
<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-8 lg:gap-12">
<!-- Left: Contact Form (wider) -->
<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">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.form.title') }}</h2>
<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 flex items-center gap-3">
<div class="w-1 h-6 rounded-full bg-brand-500" />
{{ t('contact.form.title') }}
</h2>
<ContactForm />
</div>
</div>
@@ -62,53 +68,50 @@ useSeoMeta({
<!-- Right: Contact Info + Social -->
<div class="lg:col-span-2 flex flex-col gap-6">
<!-- 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">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.quickContact') }}</h2>
<div class="flex flex-col gap-5">
<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 flex items-center gap-3">
<div class="w-1 h-5 rounded-full bg-brand-500" />
{{ t('contact.quickContact') }}
</h2>
<div class="flex flex-col gap-4">
<a
: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" />
</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
: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="flex items-center gap-4 p-3">
<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" />
</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>
<!-- 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">
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.findMeOn') }}</h2>
<div class="flex flex-col gap-3">
<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 flex items-center 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
v-for="social in siteConfig.social.filter(s => s.icon !== 'i-lucide-mail')"
:key="social.name"
:href="social.url"
target="_blank"
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" />
</div>
<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>
</div>
</div>
@@ -117,38 +120,40 @@ useSeoMeta({
</section>
<!-- FAQ Info Cards -->
<section class="py-16 md:py-20 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Info</span>
<h2 class="text-3xl sm:text-4xl font-bold mt-2 text-gray-900 dark:text-white">{{ t('contact.faq.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3 max-w-2xl mx-auto">
<section class="relative py-20 md:py-28 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<div class="relative z-10 max-w-6xl mx-auto">
<div class="text-center mb-16">
<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') }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<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="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="grid grid-cols-1 md:grid-cols-3 gap-5">
<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 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" />
</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>
</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="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="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 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" />
</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>
</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="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="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 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" />
</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>
</div>
</div>
+34 -46
View File
@@ -33,21 +33,24 @@ const heroStats = computed(() => [
<template>
<div>
<!-- Hero Section -->
<section class="pt-16 pb-16 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto text-center">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Fiverr</span>
<h1 class="text-4xl sm:text-5xl font-bold mt-2 mb-6 text-gray-900 dark:text-white">
<section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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') }}
</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') }}
</p>
<!-- 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 class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ stat.number }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ stat.label }}</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-2 font-medium">{{ stat.label }}</div>
</div>
</div>
@@ -65,19 +68,19 @@ const heroStats = computed(() => [
</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="text-center mb-14">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase 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>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-3">{{ t('fiverr.services.subtitle') }}</p>
<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('fiverr.services.title') }}</h2>
<p class="text-lg text-gray-500 dark:text-gray-400 mt-4 leading-relaxed">{{ t('fiverr.services.subtitle') }}</p>
</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
v-for="service in services"
: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 -->
<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"
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 -->
<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 }}
</span>
</div>
@@ -98,8 +101,8 @@ const heroStats = computed(() => [
<div class="absolute top-3 right-3">
<span
:class="service.url !== '#'
? 'bg-green-500/90 text-white'
: 'bg-yellow-500/90 text-white'"
? 'bg-green-500/90 text-white backdrop-blur-sm'
: 'bg-yellow-500/90 text-white backdrop-blur-sm'"
class="px-2.5 py-1 rounded-lg text-xs font-semibold shadow-lg"
>
{{ service.url !== '#' ? t('fiverr.services.available') : t('fiverr.services.comingSoon') }}
@@ -108,11 +111,11 @@ const heroStats = computed(() => [
</div>
<!-- Content -->
<div class="p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-brand-500 transition-colors">
<div class="p-6 sm:p-7">
<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`) }}
</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`) }}
</p>
@@ -142,7 +145,7 @@ const heroStats = computed(() => [
</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
:faqs="homeFAQs"
:title="t('fiverr.faq.title')"
@@ -151,29 +154,14 @@ const heroStats = computed(() => [
</div>
<!-- CTA Section -->
<section class="py-20 md:py-28 px-4">
<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="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 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" />
<div class="relative z-10">
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('fiverr.cta.title') }}</h2>
<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"
<CTASection
:title="t('fiverr.cta.title')"
:subtitle="t('fiverr.cta.subtitle')"
:primary-text="t('fiverr.cta.button')"
:primary-to="siteConfig.fiverr.profileUrl"
:secondary-text="t('fiverr.profileCta')"
secondary-to="/contact"
external
size="xl"
color="white"
trailing-icon="i-lucide-external-link"
class="font-semibold"
>
{{ t('fiverr.cta.button') }}
</UButton>
</div>
</div>
</div>
</section>
/>
</div>
</template>
+6 -10
View File
@@ -23,7 +23,7 @@ useHead({
'@graph': [
{
'@type': 'Person',
name: 'Killian Dalcin',
name: "Killian' DAL-CIN",
url: 'https://killiandalcin.fr',
jobTitle: 'Developpeur Full Stack Freelance',
email: 'contact@killiandalcin.fr',
@@ -35,14 +35,14 @@ useHead({
},
{
'@type': 'ProfessionalService',
name: 'Killian Dalcin - Developpeur Full Stack',
name: "Killian' DAL-CIN - Developpeur Full Stack",
url: 'https://killiandalcin.fr',
logo: 'https://killiandalcin.fr/images/logo.webp',
priceRange: '$$$',
areaServed: 'Worldwide',
},
],
}),
}),
},
],
})
@@ -54,7 +54,7 @@ useHead({
<HeroSection />
<!-- 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 />
</div>
@@ -62,16 +62,12 @@ useHead({
<ServicesSection />
<!-- 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 />
</div>
<!-- FAQ Section -->
<FAQSection
:faqs="homeFAQs"
:title="t('faq.title')"
:subtitle="t('faq.subtitle')"
/>
<FAQSection :faqs="homeFAQs" :title="t('faq.title')" :subtitle="t('faq.subtitle')" />
<!-- CTA Section -->
<CTASection />
+80 -62
View File
@@ -30,49 +30,61 @@ useSeoMeta({
<template>
<div v-if="project">
<!-- Back navigation -->
<div class="bg-gray-50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<UButton
variant="ghost"
icon="i-lucide-arrow-left"
to="/projects"
size="sm"
class="text-gray-500 hover:text-gray-900 dark:hover:text-white"
>
{{ t('projects.projectDetail.backToProjects') }}
</UButton>
</div>
</div>
<!-- Hero section -->
<section class="bg-gray-50 dark:bg-gray-900/30 pb-16 pt-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16 items-center">
<!-- Project Image -->
<div class="rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-800 shadow-lg">
<!-- Full-width hero image -->
<section class="relative overflow-hidden">
<!-- Hero image with overlay -->
<div class="relative h-[40vh] sm:h-[50vh] lg:h-[60vh]">
<NuxtImg
v-if="project.image"
:src="project.image"
:alt="project.title"
class="w-full h-auto object-cover"
class="w-full h-full object-cover"
format="webp"
loading="lazy"
loading="eager"
/>
<!-- Gradient overlay -->
<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" />
<!-- Back button (floating) -->
<div class="absolute top-6 left-4 sm:left-6 lg:left-8 z-20">
<UButton
variant="solid"
color="neutral"
icon="i-lucide-arrow-left"
to="/projects"
size="sm"
class="shadow-lg backdrop-blur-sm"
>
{{ t('projects.projectDetail.backToProjects') }}
</UButton>
</div>
<!-- Project Info -->
<div class="flex flex-col justify-center space-y-6">
<div class="flex items-center gap-3">
<!-- Title overlay at bottom -->
<div class="absolute bottom-0 left-0 right-0 z-10 px-4 sm:px-6 lg:px-8 pb-10">
<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>
<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 lg:text-5xl font-bold text-gray-900 dark:text-white max-w-3xl tracking-tight">{{ project.title }}</h1>
</div>
</div>
</div>
</section>
<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>
<!-- Content area -->
<section class="py-12 md:py-16 px-4 sm:px-6 lg:px-8">
<div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-10 lg:gap-16">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-14">
<!-- Description -->
<div>
<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 pt-2">
<div class="flex flex-wrap gap-3">
<UButton
v-if="project.demoUrl"
:to="project.demoUrl"
@@ -108,29 +120,23 @@ useSeoMeta({
</UButton>
</div>
</div>
</div>
</div>
</section>
<!-- Content -->
<section class="py-16 px-4">
<div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-10 lg:gap-16">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-16">
<!-- About -->
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.aboutProject') }}</h2>
<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">
{{ project.longDescription || project.description }}
</p>
<!-- Features -->
<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">
<li v-for="feature in project.features" :key="feature" class="flex items-start gap-3">
<div class="w-6 h-6 rounded-full bg-brand-500/10 flex items-center justify-center shrink-0 mt-0.5">
<li v-for="feature in project.features" :key="feature" class="flex items-start gap-3 group">
<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" />
</div>
<span class="text-gray-600 dark:text-gray-300">{{ feature }}</span>
@@ -140,27 +146,33 @@ useSeoMeta({
</div>
<!-- Technologies -->
<div v-if="project.technologies.length">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.technologiesUsed') }}</h2>
<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 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">
<TechBadge v-for="tech in project.technologies" :key="tech" :tech="tech" />
</div>
</div>
<!-- Gallery Thumbnails -->
<div v-if="project.gallery?.length">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.gallery') }}</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<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 flex items-center gap-3">
<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
v-for="(image, index) in project.gallery"
: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)"
>
<NuxtImg
:src="image"
: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"
format="webp"
/>
@@ -173,17 +185,20 @@ useSeoMeta({
</div>
<!-- Sidebar -->
<aside class="space-y-6">
<aside class="sticky top-24 space-y-6">
<!-- 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">
<h3 class="font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.projectInfo') }}</h3>
<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 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 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="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 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>
<UBadge variant="subtle" size="xs">{{ project.category }}</UBadge>
</div>
@@ -191,14 +206,17 @@ useSeoMeta({
</div>
<!-- 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">
<h3 class="font-bold text-gray-900 dark:text-white mb-5">{{ t('projects.projectDetail.relatedProjects') }}</h3>
<div class="space-y-4">
<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 flex items-center gap-2">
<UIcon name="i-lucide-layers" class="text-brand-500 w-4 h-4" />
{{ t('projects.projectDetail.relatedProjects') }}
</h3>
<div class="space-y-3">
<NuxtLink
v-for="related in relatedProjects"
:key="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
v-if="related.image"
@@ -211,7 +229,7 @@ useSeoMeta({
/>
<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="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>
</NuxtLink>
</div>
+25 -22
View File
@@ -53,37 +53,40 @@ function resetFilters() {
<template>
<div>
<!-- Hero -->
<section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-7xl mx-auto text-center">
<span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Portfolio</span>
<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>
<section class="relative pt-20 pb-16 px-4 sm:px-6 lg:px-8 overflow-hidden">
<div class="absolute inset-0 bg-gray-50/80 dark:bg-gray-900/40" aria-hidden="true" />
<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" />
<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 -->
<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">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ totalProjects }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('nav.projects') }}</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-2 font-medium">{{ t('nav.projects') }}</p>
</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">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ featuredCount }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('home.featuredProjects.title') }}</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-2 font-medium">{{ t('home.featuredProjects.title') }}</p>
</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">
<p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ 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-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-2 font-medium">{{ t('projects.categories.all') }}</p>
</div>
</div>
</div>
</section>
<!-- 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">
<!-- 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
v-model="searchQuery"
icon="i-lucide-search"
@@ -108,18 +111,18 @@ function resetFilters() {
</div>
<!-- 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" />
</div>
<!-- Empty State -->
<div v-else class="text-center py-24">
<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 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/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" />
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ 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>
<UButton @click="resetFilters" variant="soft" size="md">
<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 leading-relaxed">{{ t('projects.noResults.description') }}</p>
<UButton @click="resetFilters" variant="soft" size="md" icon="i-lucide-rotate-ccw">
{{ t('common.reset') }}
</UButton>
</div>
+26 -16
View File
@@ -7,7 +7,7 @@
"fiverr": "Fiverr"
},
"footer": {
"copyright": "\u00a9 2026 Killian Dalcin",
"copyright": "\u00a9 2026 Killian' DAL-CIN",
"navigation": "Quick Links",
"services": "Services",
"legalNotices": "Legal Notices",
@@ -20,36 +20,36 @@
}
},
"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",
"closeMenu": "Close navigation menu",
"closeDrawer": "Close menu",
"langToggle": "Change language \u2014 currently English",
"themeDark": "Switch to light mode",
"themeLight": "Switch to dark mode",
"gitea": "Killian Dalcin on Gitea (opens in new tab)",
"linkedin": "Killian Dalcin on LinkedIn (opens in new tab)",
"fiverr": "Killian Dalcin on Fiverr (opens in new tab)"
"gitea": "Killian' DAL-CIN on Gitea (opens in new tab)",
"linkedin": "Killian' DAL-CIN on LinkedIn (opens in new tab)",
"fiverr": "Killian' DAL-CIN on Fiverr (opens in new tab)"
},
"seo": {
"home": {
"title": "Killian Dalcin \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."
"title": "Killian' DAL-CIN \u2014 Freelance Full Stack Developer",
"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": {
"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."
},
"about": {
"title": "About \u2014 Killian Dalcin",
"description": "Biography and skills of Killian Dalcin, freelance full stack developer based in France."
"title": "About \u2014 Killian' DAL-CIN",
"description": "Biography and skills of Killian' DAL-CIN, freelance full stack developer based in France."
},
"contact": {
"title": "Contact \u2014 Killian Dalcin",
"description": "Contact Killian Dalcin to discuss your web development project."
"title": "Contact \u2014 Killian' DAL-CIN",
"description": "Contact Killian' DAL-CIN to discuss your web development project."
},
"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."
}
},
@@ -131,7 +131,7 @@
}
},
"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.",
"intro": {
"title": "Professional Full Stack Developer",
@@ -245,6 +245,10 @@
"title": "They Transformed Their Business With 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": {
"title": "Stop Searching, You Found THE Right Developer",
"subtitle": "Every day without action = lost opportunities. Launch your project NOW.",
@@ -263,7 +267,6 @@
"findMeOn": "Connect on Social Media",
"methods": {
"email": "Email Address",
"phone": "Phone Number",
"location": "Location",
"responseTime": "Response within 24 hours",
"availability": "Available for remote & freelance"
@@ -285,16 +288,23 @@
}
},
"form": {
"title": "Send me a message",
"name": "Your Name",
"email": "Email Address",
"subject": "Project Subject",
"message": "Project Details",
"submit": "Send Message",
"send": "Send Message",
"sending": "Sending...",
"success": "Message sent successfully! I'll respond within 24 hours.",
"error": "Error sending message. Please try again or email directly.",
"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": {
"title": "Let's Build Something Great",
+26 -16
View File
@@ -7,7 +7,7 @@
"fiverr": "Fiverr"
},
"footer": {
"copyright": "\u00a9 2026 Killian Dalcin",
"copyright": "\u00a9 2026 Killian' DAL-CIN",
"navigation": "Liens Rapides",
"services": "Services",
"legalNotices": "Mentions L\u00e9gales",
@@ -20,36 +20,36 @@
}
},
"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",
"closeMenu": "Fermer le menu de navigation",
"closeDrawer": "Fermer le menu",
"langToggle": "Changer la langue \u2014 actuellement Francais",
"themeDark": "Activer le mode clair",
"themeLight": "Activer le mode sombre",
"gitea": "Gitea de Killian Dalcin (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian Dalcin (nouvelle fenetre)",
"fiverr": "Fiverr de Killian Dalcin (nouvelle fenetre)"
"gitea": "Gitea de Killian' DAL-CIN (nouvelle fenetre)",
"linkedin": "LinkedIn de Killian' DAL-CIN (nouvelle fenetre)",
"fiverr": "Fiverr de Killian' DAL-CIN (nouvelle fenetre)"
},
"seo": {
"home": {
"title": "Killian Dalcin \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."
"title": "Killian' DAL-CIN \u2014 Developpeur Full Stack Freelance",
"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": {
"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."
},
"about": {
"title": "A propos \u2014 Killian Dalcin",
"description": "Biographie et competences de Killian Dalcin, developpeur full stack freelance base en France."
"title": "A propos \u2014 Killian' DAL-CIN",
"description": "Biographie et competences de Killian' DAL-CIN, developpeur full stack freelance base en France."
},
"contact": {
"title": "Contact \u2014 Killian Dalcin",
"description": "Contactez Killian Dalcin pour discuter de votre projet de developpement web."
"title": "Contact \u2014 Killian' DAL-CIN",
"description": "Contactez Killian' DAL-CIN pour discuter de votre projet de developpement web."
},
"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."
}
},
@@ -131,7 +131,7 @@
}
},
"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.",
"intro": {
"title": "D\u00e9veloppeur Full Stack Professionnel",
@@ -245,6 +245,10 @@
"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."
},
"faq": {
"title": "Questions Frequentes Fiverr",
"subtitle": "Tout ce que vous devez savoir avant de commander mes services sur Fiverr."
},
"cta": {
"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.",
@@ -263,7 +267,6 @@
"findMeOn": "Connectez-vous sur les R\u00e9seaux Sociaux",
"methods": {
"email": "Adresse Email",
"phone": "Num\u00e9ro de T\u00e9l\u00e9phone",
"location": "Localisation",
"responseTime": "R\u00e9ponse sous 24 heures",
"availability": "Disponible pour remote & freelance"
@@ -285,16 +288,23 @@
}
},
"form": {
"title": "Envoyez-moi un message",
"name": "Votre Nom",
"email": "Adresse Email",
"subject": "Sujet du Projet",
"message": "D\u00e9tails du Projet",
"submit": "Envoyer le Message",
"send": "Envoyer le Message",
"sending": "Envoi en cours...",
"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.",
"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": {
"title": "Construisons Quelque Chose de Grand",
+4 -5
View File
@@ -26,12 +26,11 @@ export default defineNuxtConfig({
fallback: 'dark',
storage: 'cookie',
storageKey: 'nuxt-color-mode',
cookieName: 'nuxt-color-mode',
classSuffix: ''
},
site: {
url: 'https://killiandalcin.fr',
name: 'Killian Dalcin - Developpeur Full Stack'
name: "Killian' DAL-CIN - Developpeur Full Stack"
},
i18n: {
strategy: 'prefix_except_default',
@@ -47,7 +46,7 @@ export default defineNuxtConfig({
cookieKey: 'i18n_redirected',
redirectOn: 'root',
},
},
},
runtimeConfig: {
smtpHost: '',
smtpUser: '',
@@ -58,9 +57,9 @@ export default defineNuxtConfig({
id: '',
},
},
},
},
gtag: {
id: '',
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",
"short_name": "Killian Portfolio",
"name": "Killian'- Full Stack Developer 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.",
"start_url": "/",
"display": "standalone",
-1
View File
@@ -63,7 +63,6 @@ export interface FAQ {
export interface ContactInfo {
email: string
phone: string
location: string
}