feat: redesign entire portfolio with bold modern dark theme

Complete visual overhaul of all pages and components with generous spacing,
bold typography, hover effects, gradient accents, and section differentiation.
Hero features animated terminal mockup and gradient text. Cards use hover
transforms with brand-colored shadows. CTAs use gradient backgrounds.
All i18n keys, data structures, SEO meta, and composable logic preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 19:08:55 +02:00
parent 9739becbb7
commit 9779e4e133
17 changed files with 933 additions and 593 deletions
+64 -68
View File
@@ -16,77 +16,73 @@ const translatedCategory = computed(() => {
</script>
<template>
<article class="group" itemscope itemtype="https://schema.org/CreativeWork">
<UCard>
<!-- Image -->
<template #header>
<NuxtLink :to="`/project/${project.id}`">
<NuxtImg
:src="project.image"
:alt="`${project.title} - ${project.description.slice(0, 60)}...`"
loading="lazy"
format="webp"
width="400"
height="300"
class="w-full h-48 object-cover"
itemprop="image"
/>
</NuxtLink>
</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"
itemscope
itemtype="https://schema.org/CreativeWork"
>
<!-- Image -->
<NuxtLink :to="`/project/${project.id}`" class="block relative overflow-hidden">
<NuxtImg
:src="project.image"
:alt="`${project.title} - ${project.description.slice(0, 60)}...`"
loading="lazy"
format="webp"
width="400"
height="300"
class="w-full h-48 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" />
</NuxtLink>
<!-- Content -->
<div class="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-sm text-muted" :datetime="project.date" itemprop="dateCreated">
{{ project.date }}
</time>
</div>
<!-- Title -->
<h3 class="text-lg font-bold" itemprop="name">
{{ project.title }}
</h3>
<!-- Description -->
<p class="text-sm text-muted line-clamp-2" itemprop="description">
{{ project.description }}
</p>
<!-- Technologies -->
<div v-if="project.technologies?.length" class="flex flex-wrap gap-1" itemprop="keywords">
<UBadge
v-for="tech in project.technologies.slice(0, 3)"
:key="tech"
variant="subtle"
color="neutral"
size="sm"
>
{{ tech }}
</UBadge>
<UBadge v-if="project.technologies.length > 3" variant="subtle" color="neutral" size="sm">
+{{ project.technologies.length - 3 }}
</UBadge>
</div>
<!-- Content -->
<div class="p-5 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">
{{ project.date }}
</time>
</div>
<!-- Action -->
<template #footer>
<UButton
:to="`/project/${project.id}`"
variant="ghost"
icon="i-lucide-chevron-right"
trailing
block
:aria-label="`${t('projects.buttons.viewProject')} - ${project.title}`"
itemprop="url"
<!-- Title -->
<h3 class="text-lg font-bold text-gray-900 dark:text-white group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors" itemprop="name">
{{ project.title }}
</h3>
<!-- Description -->
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed" itemprop="description">
{{ project.description }}
</p>
<!-- Technologies -->
<div v-if="project.technologies?.length" class="flex flex-wrap gap-1.5 pt-1" 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"
>
{{ t('projects.buttons.viewProject') }}
</UButton>
</template>
</UCard>
{{ 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">
+{{ project.technologies.length - 3 }}
</span>
</div>
<!-- Action link -->
<NuxtLink
:to="`/project/${project.id}`"
class="inline-flex items-center gap-1.5 text-sm font-semibold text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300 transition-colors mt-1 group/link"
:aria-label="`${t('projects.buttons.viewProject')} - ${project.title}`"
itemprop="url"
>
{{ t('projects.buttons.viewProject') }}
<UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform group-hover/link:translate-x-0.5" />
</NuxtLink>
</div>
</article>
</template>
+8 -4
View File
@@ -57,18 +57,22 @@ const levelColor = computed(() => {
</script>
<template>
<div class="flex items-center gap-2" itemscope itemtype="https://schema.org/ComputerLanguage">
<div
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700/50 transition-colors hover:border-brand-500/30"
itemscope
itemtype="https://schema.org/ComputerLanguage"
>
<NuxtImg
v-if="showImage && techData.image"
:src="techData.image"
:alt="`${techData.name} logo`"
width="24"
height="24"
width="20"
height="20"
loading="lazy"
class="shrink-0"
itemprop="image"
/>
<span class="text-sm font-medium" itemprop="name">{{ techData.name }}</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" itemprop="name">{{ techData.name }}</span>
<UBadge v-if="showLevel" :color="levelColor" variant="subtle" size="xs">
{{ techData.level }}
</UBadge>
+72 -17
View File
@@ -1,32 +1,87 @@
<script setup lang="ts">
const { t } = useI18n()
const localePath = useLocalePath()
const socialLinks = [
{ name: 'gitea', url: 'https://gitea.kamisama.ovh/kayjaydee', icon: 'simple-icons:gitea', ariaKey: 'a11y.gitea' },
{ name: 'linkedin', url: 'https://linkedin.com/in/killian-dal-cin', icon: 'simple-icons:linkedin', ariaKey: 'a11y.linkedin' },
{ name: 'fiverr', url: 'https://www.fiverr.com/users/mr_kayjaydee', icon: 'simple-icons:fiverr', ariaKey: 'a11y.fiverr' },
]
const quickLinks = computed(() => [
{ key: 'home', path: '/' },
{ key: 'projects', path: '/projects' },
{ key: 'about', path: '/about' },
{ key: 'contact', path: '/contact' },
{ key: 'fiverr', path: '/fiverr' },
])
</script>
<template>
<footer class="py-6 bg-gray-100 dark:bg-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t('footer.copyright') }}
</p>
<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>
</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>
<div class="flex items-center gap-4">
<a
v-for="link in socialLinks"
:key="link.name"
:href="link.url"
target="_blank"
rel="noopener noreferrer"
:aria-label="t(link.ariaKey)"
class="min-w-11 min-h-11 inline-flex items-center justify-center rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 transition-colors"
>
<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" />
</a>
<!-- Quick Links -->
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">
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"
>
{{ t(`nav.${link.key}`) }}
</NuxtLink>
</nav>
</div>
<!-- Social Links -->
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">
Social
</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"
: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" />
</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">
{{ t('footer.copyright') }}
</p>
</div>
</div>
</footer>
+20 -11
View File
@@ -1,19 +1,28 @@
<script setup lang="ts">
const { t } = useI18n()
const localePath = useLocalePath()
</script>
<template>
<section class="py-16 px-4">
<div class="max-w-3xl mx-auto text-center">
<h2 class="text-3xl font-bold mb-4">{{ t('home.cta2.title') }}</h2>
<p class="text-lg text-muted mb-8">{{ t('home.cta2.subtitle') }}</p>
<div class="flex gap-4 justify-center">
<UButton to="/contact" size="lg">
{{ t('home.cta2.startProject') }}
</UButton>
<UButton to="/about" size="lg" variant="outline">
{{ t('home.cta2.learnMore') }}
</UButton>
<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">
<!-- 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 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>
<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>
</div>
</div>
</div>
</div>
</section>
+9 -5
View File
@@ -20,13 +20,17 @@ const items = computed(() =>
</script>
<template>
<section class="py-16 px-4">
<section class="py-20 md:py-28 px-4">
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4">{{ title }}</h2>
<p class="text-lg text-muted">{{ subtitle }}</p>
<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>
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-2">
<UAccordion :items="items" type="single" collapsible />
</div>
<UAccordion :items="items" type="single" collapsible />
</div>
</section>
</template>
@@ -4,13 +4,22 @@ const { featuredProjects } = useProjects()
</script>
<template>
<section class="py-16 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4">{{ t('home.featuredProjects.title') }}</h2>
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('home.featuredProjects.subtitle') }}</p>
<section class="py-20 md:py-28 px-4">
<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>
<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>
</div>
<UButton to="/projects" variant="ghost" trailing-icon="i-lucide-arrow-right" class="shrink-0 self-start md:self-auto">
{{ t('home.cta.viewProjects') }}
</UButton>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- 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"
:key="project.id"
+123 -18
View File
@@ -1,26 +1,131 @@
<script setup lang="ts">
const { t } = useI18n()
const localePath = useLocalePath()
</script>
<template>
<section class="py-20 md:py-32">
<div class="max-w-4xl mx-auto text-center px-4">
<h1 class="text-4xl md:text-6xl font-bold mb-6">
{{ t('home.title') }}
</h1>
<p class="text-xl md:text-2xl text-muted mb-10">
{{ t('home.subtitle') }}
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton to="/projects" size="xl" icon="i-lucide-arrow-right" trailing>
{{ t('home.cta.viewProjects') }}
</UButton>
<UButton to="/fiverr" size="xl" variant="outline" icon="i-lucide-dollar-sign" trailing>
{{ t('nav.fiverr') }}
</UButton>
<UButton to="/contact" size="xl" variant="outline" icon="i-lucide-message-circle" trailing>
{{ t('home.cta.contactMe') }}
</UButton>
<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>
<!-- 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="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">
<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" />
</span>
<span class="text-sm font-medium text-brand-700 dark:text-brand-400">Available for projects</span>
</div>
<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>
</h1>
<p class="text-lg sm:text-xl text-gray-600 dark:text-gray-400 max-w-xl leading-relaxed">
{{ t('home.subtitle') }}
</p>
</div>
<!-- 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">
{{ t('home.cta.viewProjects') }}
</UButton>
<UButton :to="localePath('/fiverr')" size="xl" variant="outline" icon="i-lucide-dollar-sign" trailing class="font-semibold">
{{ t('nav.fiverr') }}
</UButton>
<UButton :to="localePath('/contact')" size="xl" variant="ghost" icon="i-lucide-message-circle" trailing class="font-semibold">
{{ t('home.cta.contactMe') }}
</UButton>
</div>
</div>
<!-- Right: Decorative terminal/code block -->
<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">
<!-- 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 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" />
<div class="w-3 h-3 rounded-full bg-green-400/80" />
</div>
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2 font-mono">killian@dev ~</span>
</div>
<!-- Code content -->
<div class="p-5 font-mono text-sm leading-7 space-y-1">
<div>
<span class="text-brand-500">const</span>
<span class="text-gray-900 dark:text-white"> developer</span>
<span class="text-gray-500"> = </span>
<span class="text-gray-500">{</span>
</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>
</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>
</div>
<div class="pl-6">
<span class="text-purple-500 dark:text-purple-400">skills</span><span class="text-gray-500">: [</span>
</div>
<div class="pl-10">
<span class="text-amber-600 dark:text-amber-400">'Vue.js'</span><span class="text-gray-500">, </span>
<span class="text-amber-600 dark:text-amber-400">'Nuxt'</span><span class="text-gray-500">, </span>
<span class="text-amber-600 dark:text-amber-400">'Node.js'</span><span class="text-gray-500">,</span>
</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>
</div>
<div class="pl-10">
<span class="text-amber-600 dark:text-amber-400">'Docker'</span><span class="text-gray-500">, </span>
<span class="text-amber-600 dark:text-amber-400">'TypeScript'</span>
</div>
<div class="pl-6">
<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-brand-500">true</span>
</div>
<div>
<span class="text-gray-500">}</span>
</div>
<!-- Blinking cursor -->
<div class="mt-2 flex items-center gap-1">
<span class="text-brand-500">$</span>
<span class="w-2.5 h-5 bg-brand-500 animate-pulse" />
</div>
</div>
</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">
<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">
<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>
</div>
</div>
</div>
</div>
</section>
+19 -13
View File
@@ -26,22 +26,28 @@ const services = computed(() => [
</script>
<template>
<section class="py-16 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4">{{ t('home.services.title') }}</h2>
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('home.services.subtitle') }}</p>
<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>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UCard v-for="(service, index) in services" :key="index">
<div class="flex items-start gap-4">
<UIcon :name="service.icon" class="text-primary text-2xl shrink-0 mt-1" />
<div>
<h3 class="text-xl font-bold mb-2">{{ service.title }}</h3>
<p class="text-muted">{{ service.description }}</p>
</div>
<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"
>
<!-- 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">
<UIcon :name="service.icon" class="text-brand-600 dark:text-brand-400 text-xl" />
</div>
</UCard>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">{{ service.title }}</h3>
<p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ service.description }}</p>
</div>
</div>
</div>
</section>
+46 -25
View File
@@ -5,63 +5,84 @@ const { t } = useI18n()
</script>
<template>
<section class="py-16 px-4">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold mb-4">{{ t('testimonials.title') }}</h2>
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('testimonials.subtitle') }}</p>
<section class="py-20 md:py-28 px-4">
<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>
<!-- Stats -->
<div class="flex justify-center gap-8 mt-8">
<!-- Stats row -->
<div class="flex justify-center gap-10 mt-10">
<div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.totalReviews }}</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.clients') }}</p>
<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>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.averageRating }}/5</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.rating') }}</p>
<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>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.projectsCompleted }}</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.projects') }}</p>
<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>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<UCard v-for="(testimonial, index) in testimonials" :key="index">
<div class="flex flex-col gap-3">
<!-- Rating -->
<!-- Horizontal scrolling testimonials -->
<div class="flex gap-6 overflow-x-auto pb-4 -mx-4 px-4 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"
>
<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">
<!-- Rating stars -->
<div class="flex gap-1">
<UIcon
v-for="star in 5"
:key="star"
name="i-lucide-star"
:class="star <= testimonial.rating ? 'text-yellow-400' : 'text-gray-300'"
class="w-4 h-4"
:class="star <= testimonial.rating ? 'text-yellow-400' : 'text-gray-300 dark:text-gray-700'"
/>
</div>
<!-- Content -->
<p class="text-sm italic">"{{ testimonial.content }}"</p>
<!-- Quote -->
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed flex-1 italic">
"{{ testimonial.content }}"
</p>
<!-- Author -->
<div class="flex items-center gap-3 mt-2">
<div class="flex items-center gap-3 pt-2 border-t border-gray-100 dark:border-gray-800">
<NuxtImg
:src="testimonial.avatar"
:alt="testimonial.name"
width="40"
height="40"
class="rounded-full"
class="rounded-full ring-2 ring-gray-100 dark:ring-gray-800"
loading="lazy"
/>
<div>
<p class="font-semibold text-sm">{{ testimonial.name }}</p>
<p class="text-xs text-muted">{{ testimonial.project_type }} - {{ testimonial.platform }}</p>
<p class="font-semibold text-sm text-gray-900 dark:text-white">{{ testimonial.name }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ testimonial.project_type }} - {{ testimonial.platform }}</p>
</div>
</div>
</div>
</UCard>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>