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.
This commit is contained in:
2026-04-08 19:08:55 +02:00
parent 578a0afa1a
commit 355df8dbbe
17 changed files with 933 additions and 593 deletions
+64 -68
View File
@@ -16,77 +16,73 @@ const translatedCategory = computed(() => {
</script> </script>
<template> <template>
<article class="group" itemscope itemtype="https://schema.org/CreativeWork"> <article
<UCard> 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"
<!-- Image --> itemscope
<template #header> itemtype="https://schema.org/CreativeWork"
<NuxtLink :to="`/project/${project.id}`"> >
<NuxtImg <!-- Image -->
:src="project.image" <NuxtLink :to="`/project/${project.id}`" class="block relative overflow-hidden">
:alt="`${project.title} - ${project.description.slice(0, 60)}...`" <NuxtImg
loading="lazy" :src="project.image"
format="webp" :alt="`${project.title} - ${project.description.slice(0, 60)}...`"
width="400" loading="lazy"
height="300" format="webp"
class="w-full h-48 object-cover" width="400"
itemprop="image" height="300"
/> class="w-full h-48 object-cover transition-transform duration-500 group-hover:scale-105"
</NuxtLink> itemprop="image"
</template> />
<!-- 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 --> <!-- Content -->
<div class="flex flex-col gap-3"> <div class="p-5 flex flex-col gap-3">
<!-- Category & Date --> <!-- Category & Date -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<UBadge v-if="project.category" color="primary" variant="subtle" itemprop="genre"> <UBadge v-if="project.category" color="primary" variant="subtle" itemprop="genre">
{{ translatedCategory }} {{ translatedCategory }}
</UBadge> </UBadge>
<time v-if="project.date" class="text-sm text-muted" :datetime="project.date" itemprop="dateCreated"> <time v-if="project.date" class="text-xs text-gray-400 dark:text-gray-500 font-medium" :datetime="project.date" itemprop="dateCreated">
{{ project.date }} {{ project.date }}
</time> </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>
</div> </div>
<!-- Action --> <!-- Title -->
<template #footer> <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">
<UButton {{ project.title }}
:to="`/project/${project.id}`" </h3>
variant="ghost"
icon="i-lucide-chevron-right" <!-- Description -->
trailing <p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed" itemprop="description">
block {{ project.description }}
:aria-label="`${t('projects.buttons.viewProject')} - ${project.title}`" </p>
itemprop="url"
<!-- 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') }} {{ tech }}
</UButton> </span>
</template> <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">
</UCard> +{{ 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> </article>
</template> </template>
+8 -4
View File
@@ -57,18 +57,22 @@ const levelColor = computed(() => {
</script> </script>
<template> <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 <NuxtImg
v-if="showImage && techData.image" v-if="showImage && techData.image"
:src="techData.image" :src="techData.image"
:alt="`${techData.name} logo`" :alt="`${techData.name} logo`"
width="24" width="20"
height="24" height="20"
loading="lazy" loading="lazy"
class="shrink-0" class="shrink-0"
itemprop="image" 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"> <UBadge v-if="showLevel" :color="levelColor" variant="subtle" size="xs">
{{ techData.level }} {{ techData.level }}
</UBadge> </UBadge>
+72 -17
View File
@@ -1,32 +1,87 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath()
const socialLinks = [ const socialLinks = [
{ name: 'gitea', url: 'https://gitea.kamisama.ovh/kayjaydee', icon: 'simple-icons:gitea', ariaKey: 'a11y.gitea' }, { 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: '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' }, { 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> </script>
<template> <template>
<footer class="py-6 bg-gray-100 dark:bg-gray-800"> <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 flex flex-col md:flex-row items-center justify-between gap-4"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
<p class="text-sm text-gray-600 dark:text-gray-400"> <div class="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-8">
{{ t('footer.copyright') }} <!-- Branding & Tagline -->
</p> <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"> <!-- Quick Links -->
<a <div>
v-for="link in socialLinks" <h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">
:key="link.name" Navigation
:href="link.url" </h3>
target="_blank" <nav class="flex flex-col gap-2.5">
rel="noopener noreferrer" <NuxtLink
:aria-label="t(link.ariaKey)" v-for="link in quickLinks"
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" :key="link.key"
> :to="localePath(link.path)"
<UIcon :name="link.icon" class="w-5 h-5 text-gray-500 dark:text-gray-400 hover:text-primary-500 transition-colors duration-150" /> class="text-sm text-gray-500 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors"
</a> >
{{ 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>
</div> </div>
</footer> </footer>
+20 -11
View File
@@ -1,19 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath()
</script> </script>
<template> <template>
<section class="py-16 px-4"> <section class="py-20 md:py-28 px-4">
<div class="max-w-3xl mx-auto text-center"> <div class="max-w-5xl mx-auto">
<h2 class="text-3xl font-bold mb-4">{{ t('home.cta2.title') }}</h2> <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">
<p class="text-lg text-muted mb-8">{{ t('home.cta2.subtitle') }}</p> <!-- Decorative shapes -->
<div class="flex gap-4 justify-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" />
<UButton to="/contact" size="lg"> <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" />
{{ t('home.cta2.startProject') }}
</UButton> <div class="relative z-10">
<UButton to="/about" size="lg" variant="outline"> <h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('home.cta2.title') }}</h2>
{{ t('home.cta2.learnMore') }} <p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('home.cta2.subtitle') }}</p>
</UButton> <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>
</div> </div>
</section> </section>
+9 -5
View File
@@ -20,13 +20,17 @@ const items = computed(() =>
</script> </script>
<template> <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="max-w-3xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ title }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">FAQ</span>
<p class="text-lg text-muted">{{ subtitle }}</p> <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> </div>
<UAccordion :items="items" type="single" collapsible />
</div> </div>
</section> </section>
</template> </template>
@@ -4,13 +4,22 @@ const { featuredProjects } = useProjects()
</script> </script>
<template> <template>
<section class="py-16 px-4"> <section class="py-20 md:py-28 px-4">
<div class="max-w-6xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-12"> <!-- Section header -->
<h2 class="text-3xl font-bold mb-4">{{ t('home.featuredProjects.title') }}</h2> <div class="flex flex-col md:flex-row md:items-end md:justify-between gap-4 mb-14">
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('home.featuredProjects.subtitle') }}</p> <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>
<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 <ProjectCard
v-for="project in featuredProjects" v-for="project in featuredProjects"
:key="project.id" :key="project.id"
+123 -18
View File
@@ -1,26 +1,131 @@
<script setup lang="ts"> <script setup lang="ts">
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath()
</script> </script>
<template> <template>
<section class="py-20 md:py-32"> <section class="relative min-h-[80vh] flex items-center overflow-hidden bg-white dark:bg-gray-950">
<div class="max-w-4xl mx-auto text-center px-4"> <!-- Dot grid background pattern -->
<h1 class="text-4xl md:text-6xl font-bold mb-6"> <div class="absolute inset-0 opacity-[0.03] dark:opacity-[0.06]" aria-hidden="true">
{{ t('home.title') }} <div class="absolute inset-0" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 32px 32px;" />
</h1> </div>
<p class="text-xl md:text-2xl text-muted mb-10">
{{ t('home.subtitle') }} <!-- Gradient glow -->
</p> <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="flex flex-col sm:flex-row gap-4 justify-center">
<UButton to="/projects" size="xl" icon="i-lucide-arrow-right" trailing> <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">
{{ t('home.cta.viewProjects') }} <div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
</UButton> <!-- Left: Content -->
<UButton to="/fiverr" size="xl" variant="outline" icon="i-lucide-dollar-sign" trailing> <div class="space-y-8">
{{ t('nav.fiverr') }} <!-- Status badge -->
</UButton> <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">
<UButton to="/contact" size="xl" variant="outline" icon="i-lucide-message-circle" trailing> <span class="relative flex h-2 w-2">
{{ t('home.cta.contactMe') }} <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-400 opacity-75" />
</UButton> <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>
</div> </div>
</section> </section>
+19 -13
View File
@@ -26,22 +26,28 @@ const services = computed(() => [
</script> </script>
<template> <template>
<section class="py-16 px-4"> <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-6xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('home.services.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Services</span>
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('home.services.subtitle') }}</p> <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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UCard v-for="(service, index) in services" :key="index"> <div
<div class="flex items-start gap-4"> v-for="(service, index) in services"
<UIcon :name="service.icon" class="text-primary text-2xl shrink-0 mt-1" /> :key="index"
<div> 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"
<h3 class="text-xl font-bold mb-2">{{ service.title }}</h3> >
<p class="text-muted">{{ service.description }}</p> <!-- Icon -->
</div> <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> </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>
</div> </div>
</section> </section>
+46 -25
View File
@@ -5,63 +5,84 @@ const { t } = useI18n()
</script> </script>
<template> <template>
<section class="py-16 px-4"> <section class="py-20 md:py-28 px-4">
<div class="max-w-6xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('testimonials.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">{{ t('testimonials.title') }}</span>
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('testimonials.subtitle') }}</p> <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 --> <!-- Stats row -->
<div class="flex justify-center gap-8 mt-8"> <div class="flex justify-center gap-10 mt-10">
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.totalReviews }}</p> <p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.totalReviews }}</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.clients') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.clients') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.averageRating }}/5</p> <p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.averageRating }}/5</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.rating') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.rating') }}</p>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800" />
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-bold text-primary">{{ testimonialsStats.projectsCompleted }}</p> <p class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ testimonialsStats.projectsCompleted }}</p>
<p class="text-sm text-muted">{{ t('testimonials.stats.projects') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('testimonials.stats.projects') }}</p>
</div> </div>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <!-- Horizontal scrolling testimonials -->
<UCard v-for="(testimonial, index) in testimonials" :key="index"> <div class="flex gap-6 overflow-x-auto pb-4 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
<div class="flex flex-col gap-3"> <div
<!-- Rating --> 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"> <div class="flex gap-1">
<UIcon <UIcon
v-for="star in 5" v-for="star in 5"
:key="star" :key="star"
name="i-lucide-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> </div>
<!-- Content --> <!-- Quote -->
<p class="text-sm italic">"{{ testimonial.content }}"</p> <p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed flex-1 italic">
"{{ testimonial.content }}"
</p>
<!-- Author --> <!-- 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 <NuxtImg
:src="testimonial.avatar" :src="testimonial.avatar"
:alt="testimonial.name" :alt="testimonial.name"
width="40" width="40"
height="40" height="40"
class="rounded-full" class="rounded-full ring-2 ring-gray-100 dark:ring-gray-800"
loading="lazy" loading="lazy"
/> />
<div> <div>
<p class="font-semibold text-sm">{{ testimonial.name }}</p> <p class="font-semibold text-sm text-gray-900 dark:text-white">{{ testimonial.name }}</p>
<p class="text-xs text-muted">{{ testimonial.project_type }} - {{ testimonial.platform }}</p> <p class="text-xs text-gray-500 dark:text-gray-400">{{ testimonial.project_type }} - {{ testimonial.platform }}</p>
</div> </div>
</div> </div>
</div> </div>
</UCard> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<style scoped>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
+34 -8
View File
@@ -10,13 +10,39 @@ function handleError() {
</script> </script>
<template> <template>
<div class="min-h-screen flex flex-col items-center justify-center gap-6 px-4"> <div class="min-h-screen flex flex-col items-center justify-center gap-8 px-4 bg-white dark:bg-gray-950">
<h1 class="text-8xl font-bold text-primary">{{ error.statusCode }}</h1> <!-- Decorative background -->
<p class="text-xl text-gray-500 dark:text-gray-400 text-center max-w-md"> <div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
{{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }} <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" />
</p> </div>
<UButton size="lg" @click="handleError">
{{ t('error.backHome') }} <div class="relative z-10 text-center space-y-6 max-w-lg">
</UButton> <!-- Error code with glitch-like styling -->
<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">
{{ 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">
{{ error.statusCode }}
</span>
</div>
<div class="space-y-3">
<p class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">
{{ error.statusCode === 404 ? t('error.notFound') : t('error.generic') }}
</p>
<p class="text-gray-500 dark:text-gray-400">
{{ error.statusCode === 404
? 'The page you are looking for does not exist or has been moved.'
: 'Something unexpected happened. Please try again.'
}}
</p>
</div>
<UButton size="xl" icon="i-lucide-home" class="font-semibold" @click="handleError">
{{ t('error.backHome') }}
</UButton>
</div>
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100">
<AppHeader /> <AppHeader />
<main class="flex-1"> <main class="flex-1">
<slot /> <slot />
+67 -42
View File
@@ -2,6 +2,7 @@
import { techStack } from '~/data/techstack' import { techStack } from '~/data/techstack'
const { t } = useI18n() const { t } = useI18n()
const localePath = useLocalePath()
useSeoMeta({ useSeoMeta({
title: () => t('seo.about.title'), title: () => t('seo.about.title'),
@@ -64,15 +65,16 @@ const approachCards = computed(() => [
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="py-20 px-4 text-center"> <section class="pt-16 pb-20 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl font-bold mb-6"> <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">
{{ t('about.title') }} {{ t('about.title') }}
</h1> </h1>
<p class="text-xl text-muted mb-8"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto">
{{ t('about.subtitle') }} {{ t('about.subtitle') }}
</p> </p>
<div class="space-y-4 text-lg text-muted max-w-3xl mx-auto"> <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.intro.content') }}</p>
<p>{{ t('about.experience.content') }}</p> <p>{{ t('about.experience.content') }}</p>
</div> </div>
@@ -80,23 +82,30 @@ const approachCards = computed(() => [
</section> </section>
<!-- Skills Section --> <!-- Skills Section -->
<section class="py-16 px-4"> <section class="py-20 md:py-28 px-4">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('about.skills.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Tech Stack</span>
<p class="text-lg text-muted max-w-2xl mx-auto"> <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">
{{ t('about.subtitle') }} {{ t('about.subtitle') }}
</p> </p>
</div> </div>
<!-- Tech Categories Grid --> <!-- Tech Categories Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<UCard v-for="category in techCategories" :key="category.key"> <div
<div class="flex items-center gap-3 mb-4"> v-for="category in techCategories"
<UIcon :name="category.icon" class="text-2xl text-primary" /> :key="category.key"
<h3 class="text-xl font-bold">{{ category.title }}</h3> class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8"
>
<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">
<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>
</div> </div>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-2">
<TechBadge <TechBadge
v-for="tech in techStack[category.key]" v-for="tech in techStack[category.key]"
:key="tech.name" :key="tech.name"
@@ -104,16 +113,18 @@ const approachCards = computed(() => [
:show-level="true" :show-level="true"
/> />
</div> </div>
</UCard> </div>
</div> </div>
<!-- Operating Systems --> <!-- Operating Systems -->
<UCard> <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="flex items-center gap-3 mb-4"> <div class="flex items-center gap-3 mb-6">
<UIcon name="i-lucide-monitor" class="text-2xl text-primary" /> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center">
<h3 class="text-xl font-bold">{{ t('about.skills.systems') }}</h3> <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>
</div> </div>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-2">
<TechBadge <TechBadge
v-for="tech in techStack.operating_systems" v-for="tech in techStack.operating_systems"
:key="tech.name" :key="tech.name"
@@ -121,46 +132,60 @@ const approachCards = computed(() => [
:show-level="false" :show-level="false"
/> />
</div> </div>
</UCard> </div>
</div> </div>
</section> </section>
<!-- Approach Section --> <!-- Approach Section -->
<section class="py-16 px-4"> <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="max-w-7xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('about.approach.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Methodology</span>
<p class="text-lg text-muted max-w-2xl mx-auto"> <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">
{{ t('about.approach.subtitle') }} {{ t('about.approach.subtitle') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UCard v-for="(card, index) in approachCards" :key="index"> <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"
>
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<UIcon :name="card.icon" class="text-2xl text-primary shrink-0 mt-1" /> <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">
<UIcon :name="card.icon" class="text-xl text-brand-600 dark:text-brand-400" />
</div>
<div> <div>
<h3 class="text-lg font-bold mb-2">{{ card.title }}</h3> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ card.title }}</h3>
<p class="text-muted">{{ card.description }}</p> <p class="text-gray-500 dark:text-gray-400 leading-relaxed">{{ card.description }}</p>
</div> </div>
</div> </div>
</UCard> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- CTA Section --> <!-- CTA Section -->
<section class="py-16 px-4 text-center"> <section class="py-20 md:py-28 px-4">
<div class="max-w-2xl mx-auto"> <div class="max-w-5xl mx-auto">
<h2 class="text-3xl font-bold mb-4">{{ t('about.cta.title') }}</h2> <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">
<p class="text-lg text-muted mb-8">{{ t('about.cta.description') }}</p> <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="flex flex-wrap justify-center gap-4"> <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" />
<UButton to="/contact" size="lg">
{{ t('about.cta.button') }} <div class="relative z-10">
</UButton> <h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('about.cta.title') }}</h2>
<UButton to="/projects" size="lg" variant="outline"> <p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('about.cta.description') }}</p>
{{ t('home.cta.viewProjects') }} <div class="flex flex-wrap justify-center gap-4">
</UButton> <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>
</div> </div>
</section> </section>
+85 -65
View File
@@ -18,72 +18,83 @@ useSeoMeta({
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="py-20 px-4 text-center"> <section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl font-bold mb-6"> <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">
{{ t('contact.title') }} {{ t('contact.title') }}
</h1> </h1>
<p class="text-xl text-muted mb-8"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto">
{{ t('contact.subtitle') }} {{ t('contact.subtitle') }}
</p> </p>
<!-- Stats --> <!-- Stats -->
<div class="flex flex-wrap justify-center gap-8"> <div class="flex flex-wrap justify-center gap-10">
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-primary">24-48h</div> <div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">24-48h</div>
<div class="text-sm text-muted">{{ t('contact.stats.responseTime') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.responseTime') }}</div>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800 hidden sm:block" />
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-primary">100%</div> <div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">100%</div>
<div class="text-sm text-muted">{{ t('contact.stats.satisfaction') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.satisfaction') }}</div>
</div> </div>
<div class="w-px bg-gray-200 dark:bg-gray-800 hidden sm:block" />
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-primary">Remote</div> <div class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">Remote</div>
<div class="text-sm text-muted">{{ t('contact.stats.collaboration') }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('contact.stats.collaboration') }}</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Two Column Layout --> <!-- Two Column Layout -->
<section class="py-16 px-4"> <section class="py-16 md:py-20 px-4">
<div class="max-w-5xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-5 gap-10 lg:gap-16">
<!-- Left: Contact Form --> <!-- Left: Contact Form (wider) -->
<UCard> <div class="lg:col-span-3">
<template #header> <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">{{ t('contact.form.title') }}</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.form.title') }}</h2>
</template> <ContactForm />
<ContactForm /> </div>
</UCard> </div>
<!-- Right: Contact Info + Social --> <!-- Right: Contact Info + Social -->
<div class="flex flex-col gap-6"> <div class="lg:col-span-2 flex flex-col gap-6">
<!-- Contact Info --> <!-- Contact Info -->
<UCard> <div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8">
<template #header> <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.quickContact') }}</h2>
<h2 class="text-xl font-bold">{{ t('contact.quickContact') }}</h2> <div class="flex flex-col gap-5">
</template> <a
<div class="flex flex-col gap-4"> :href="`mailto:${siteConfig.contact.email}`"
<a :href="`mailto:${siteConfig.contact.email}`" class="flex items-center gap-3 hover:text-primary transition-colors"> class="flex items-center gap-4 group"
<UIcon name="i-lucide-mail" class="text-xl text-primary shrink-0" /> >
<span>{{ siteConfig.contact.email }}</span> <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-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>
</a> </a>
<a :href="`tel:${siteConfig.contact.phone.replace(/\s/g, '')}`" class="flex items-center gap-3 hover:text-primary transition-colors"> <a
<UIcon name="i-lucide-phone" class="text-xl text-primary shrink-0" /> :href="`tel:${siteConfig.contact.phone.replace(/\s/g, '')}`"
<span>{{ siteConfig.contact.phone }}</span> 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> </a>
<div class="flex items-center gap-3"> <div class="flex items-center gap-4">
<UIcon name="i-lucide-map-pin" class="text-xl text-primary shrink-0" /> <div class="w-10 h-10 rounded-xl bg-brand-500/10 dark:bg-brand-500/15 flex items-center justify-center shrink-0">
<span>{{ siteConfig.contact.location }}</span> <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>
</div> </div>
</div> </div>
</UCard> </div>
<!-- Social Links --> <!-- Social Links -->
<UCard> <div class="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6 sm:p-8">
<template #header> <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{{ t('contact.findMeOn') }}</h2>
<h2 class="text-xl font-bold">{{ t('contact.findMeOn') }}</h2>
</template>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<a <a
v-for="social in siteConfig.social.filter(s => s.icon !== 'i-lucide-mail')" v-for="social in siteConfig.social.filter(s => s.icon !== 'i-lucide-mail')"
@@ -91,46 +102,55 @@ useSeoMeta({
:href="social.url" :href="social.url"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="flex items-center gap-3 hover:text-primary transition-colors" class="flex items-center gap-4 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors group"
> >
<UIcon :name="social.icon" class="text-xl shrink-0" /> <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">
<span>{{ social.name }}</span> <UIcon :name="social.icon" class="text-gray-500 dark:text-gray-400 group-hover:text-brand-500 transition-colors" />
<UIcon name="i-lucide-external-link" class="text-sm text-muted ml-auto" /> </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" />
</a> </a>
</div> </div>
</UCard> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- FAQ Info Cards --> <!-- FAQ Info Cards -->
<section class="py-16 px-4"> <section class="py-16 md:py-20 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-5xl mx-auto"> <div class="max-w-6xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('contact.faq.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Info</span>
<p class="text-lg text-muted max-w-2xl mx-auto"> <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">
{{ t('contact.faq.subtitle') }} {{ t('contact.faq.subtitle') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<UCard class="text-center"> <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">
<UIcon name="i-lucide-clock" class="text-3xl text-primary mb-4 mx-auto" /> <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">
<h3 class="text-lg font-bold mb-2">{{ t('contact.faq.responseTime.title') }}</h3> <UIcon name="i-lucide-clock" class="text-2xl text-brand-600 dark:text-brand-400" />
<p class="text-muted text-sm">{{ t('contact.faq.responseTime.description') }}</p> </div>
</UCard> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ 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>
<UCard class="text-center"> <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">
<UIcon name="i-lucide-building" class="text-3xl text-primary mb-4 mx-auto" /> <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">
<h3 class="text-lg font-bold mb-2">{{ t('contact.faq.projectTypes.title') }}</h3> <UIcon name="i-lucide-building" class="text-2xl text-brand-600 dark:text-brand-400" />
<p class="text-muted text-sm">{{ t('contact.faq.projectTypes.description') }}</p> </div>
</UCard> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ 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>
<UCard class="text-center"> <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">
<UIcon name="i-lucide-users" class="text-3xl text-primary mb-4 mx-auto" /> <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">
<h3 class="text-lg font-bold mb-2">{{ t('contact.faq.collaboration.title') }}</h3> <UIcon name="i-lucide-users" class="text-2xl text-brand-600 dark:text-brand-400" />
<p class="text-muted text-sm">{{ t('contact.faq.collaboration.description') }}</p> </div>
</UCard> <h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">{{ 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> </div>
</div> </div>
</section> </section>
+104 -77
View File
@@ -33,20 +33,21 @@ const heroStats = computed(() => [
<template> <template>
<div> <div>
<!-- Hero Section --> <!-- Hero Section -->
<section class="py-20 px-4 text-center"> <section class="pt-16 pb-16 px-4 bg-gray-50 dark:bg-gray-900/30">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto text-center">
<h1 class="text-4xl sm:text-5xl font-bold mb-6"> <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">
{{ t('fiverr.title') }} {{ t('fiverr.title') }}
</h1> </h1>
<p class="text-xl text-muted mb-8"> <p class="text-xl text-gray-500 dark:text-gray-400 mb-10 max-w-2xl mx-auto">
{{ t('fiverr.subtitle') }} {{ t('fiverr.subtitle') }}
</p> </p>
<!-- Stats --> <!-- Stats -->
<div class="flex flex-wrap justify-center gap-8 mb-8"> <div class="flex flex-wrap justify-center gap-10 mb-10">
<div v-for="stat in heroStats" :key="stat.label" class="text-center"> <div v-for="stat in heroStats" :key="stat.label" class="text-center">
<div class="text-3xl font-bold text-primary">{{ stat.number }}</div> <div class="text-4xl font-extrabold text-brand-500 dark:text-brand-400">{{ stat.number }}</div>
<div class="text-sm text-muted">{{ stat.label }}</div> <div class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ stat.label }}</div>
</div> </div>
</div> </div>
@@ -54,8 +55,9 @@ const heroStats = computed(() => [
:to="siteConfig.fiverr.profileUrl" :to="siteConfig.fiverr.profileUrl"
target="_blank" target="_blank"
external external
size="lg" size="xl"
trailing-icon="i-lucide-external-link" trailing-icon="i-lucide-external-link"
class="font-semibold"
> >
{{ t('fiverr.profileCta') }} {{ t('fiverr.profileCta') }}
</UButton> </UButton>
@@ -63,89 +65,114 @@ const heroStats = computed(() => [
</section> </section>
<!-- Services Section --> <!-- Services Section -->
<section class="py-16 px-4"> <section class="py-20 md:py-28 px-4">
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<div class="text-center mb-12"> <div class="text-center mb-14">
<h2 class="text-3xl font-bold mb-4">{{ t('fiverr.services.title') }}</h2> <span class="text-sm font-semibold text-brand-500 dark:text-brand-400 uppercase tracking-wider">Services</span>
<p class="text-lg text-muted">{{ t('fiverr.services.subtitle') }}</p> <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> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<UCard v-for="service in services" :key="service.id"> <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"
>
<!-- Service Image --> <!-- Service Image -->
<NuxtImg <div class="relative overflow-hidden">
:src="service.image" <NuxtImg
:alt="t(`fiverr.serviceData.${service.id}.title`)" :src="service.image"
class="w-full h-48 object-cover rounded-lg mb-4" :alt="t(`fiverr.serviceData.${service.id}.title`)"
loading="lazy" class="w-full h-52 object-cover transition-transform duration-500 group-hover:scale-105"
/> loading="lazy"
/>
<!-- Price & Status Badges --> <div class="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
<div class="flex items-center gap-2 mb-3"> <!-- Price badge overlay -->
<UBadge color="primary" variant="subtle"> <div class="absolute bottom-3 left-3">
{{ t('fiverr.pricing.startingAt') }} {{ service.price }} <span class="px-3 py-1.5 rounded-lg bg-brand-500 text-white text-sm font-bold shadow-lg">
</UBadge> {{ t('fiverr.pricing.startingAt') }} {{ service.price }}
<UBadge </span>
:color="service.url !== '#' ? 'success' : 'warning'" </div>
variant="subtle" <!-- Status badge -->
> <div class="absolute top-3 right-3">
{{ service.url !== '#' ? t('fiverr.services.available') : t('fiverr.services.comingSoon') }} <span
</UBadge> :class="service.url !== '#'
? 'bg-green-500/90 text-white'
: 'bg-yellow-500/90 text-white'"
class="px-2.5 py-1 rounded-lg text-xs font-semibold shadow-lg"
>
{{ service.url !== '#' ? t('fiverr.services.available') : t('fiverr.services.comingSoon') }}
</span>
</div>
</div> </div>
<!-- Title & Description --> <!-- Content -->
<h3 class="text-xl font-bold mb-2"> <div class="p-6">
{{ t(`fiverr.serviceData.${service.id}.title`) }} <h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-brand-500 transition-colors">
</h3> {{ t(`fiverr.serviceData.${service.id}.title`) }}
<p class="text-muted mb-4"> </h3>
{{ t(`fiverr.serviceData.${service.id}.description`) }} <p class="text-gray-500 dark:text-gray-400 mb-5 leading-relaxed">
</p> {{ t(`fiverr.serviceData.${service.id}.description`) }}
</p>
<!-- Action Button --> <!-- Action Button -->
<UButton <UButton
v-if="service.url !== '#'" v-if="service.url !== '#'"
:to="service.url" :to="service.url"
target="_blank" target="_blank"
external external
size="sm" trailing-icon="i-lucide-external-link"
trailing-icon="i-lucide-external-link" class="font-semibold"
> >
{{ t('fiverr.services.orderNow') }} {{ t('fiverr.services.orderNow') }}
</UButton> </UButton>
<UButton <UButton
v-else v-else
size="sm" variant="outline"
variant="outline" disabled
disabled class="font-semibold"
> >
{{ t('fiverr.services.comingSoon') }} {{ t('fiverr.services.comingSoon') }}
</UButton> </UButton>
</UCard> </div>
</div>
</div> </div>
</div> </div>
</section> </section>
<!-- FAQ Section --> <!-- FAQ Section -->
<FAQSection <div class="bg-gray-50 dark:bg-gray-900/30">
:faqs="homeFAQs" <FAQSection
:title="t('fiverr.faq.title')" :faqs="homeFAQs"
:subtitle="t('fiverr.faq.subtitle')" :title="t('fiverr.faq.title')"
/> :subtitle="t('fiverr.faq.subtitle')"
/>
</div>
<!-- CTA Section --> <!-- CTA Section -->
<section class="py-16 px-4 text-center"> <section class="py-20 md:py-28 px-4">
<div class="max-w-2xl mx-auto"> <div class="max-w-5xl mx-auto">
<h2 class="text-3xl font-bold mb-4">{{ t('fiverr.cta.title') }}</h2> <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">
<p class="text-lg text-muted mb-8">{{ t('fiverr.cta.subtitle') }}</p> <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" />
<UButton <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" />
:to="siteConfig.fiverr.profileUrl"
target="_blank" <div class="relative z-10">
external <h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">{{ t('fiverr.cta.title') }}</h2>
size="lg" <p class="text-lg text-white/80 mb-10 max-w-2xl mx-auto">{{ t('fiverr.cta.subtitle') }}</p>
trailing-icon="i-lucide-external-link" <UButton
> :to="siteConfig.fiverr.profileUrl"
{{ t('fiverr.cta.button') }} target="_blank"
</UButton> external
size="xl"
color="white"
trailing-icon="i-lucide-external-link"
class="font-semibold"
>
{{ t('fiverr.cta.button') }}
</UButton>
</div>
</div>
</div> </div>
</section> </section>
</div> </div>
+17 -15
View File
@@ -53,24 +53,26 @@ useHead({
<!-- Hero Section --> <!-- Hero Section -->
<HeroSection /> <HeroSection />
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <!-- Featured Projects Section -->
<!-- Featured Projects Section --> <div class="bg-gray-50 dark:bg-gray-900/30">
<FeaturedProjectsSection /> <FeaturedProjectsSection />
<!-- Services Section -->
<ServicesSection />
<!-- Testimonials Section -->
<TestimonialsSection />
<!-- FAQ Section -->
<FAQSection
:faqs="homeFAQs"
:title="t('faq.title')"
:subtitle="t('faq.subtitle')"
/>
</div> </div>
<!-- Services Section -->
<ServicesSection />
<!-- Testimonials Section -->
<div class="bg-gray-50 dark:bg-gray-900/30">
<TestimonialsSection />
</div>
<!-- FAQ Section -->
<FAQSection
:faqs="homeFAQs"
:title="t('faq.title')"
:subtitle="t('faq.subtitle')"
/>
<!-- CTA Section --> <!-- CTA Section -->
<CTASection /> <CTASection />
</div> </div>
+182 -166
View File
@@ -29,181 +29,197 @@ useSeoMeta({
</script> </script>
<template> <template>
<div v-if="project" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div v-if="project">
<!-- Breadcrumb --> <!-- Back navigation -->
<nav class="mb-8"> <div class="bg-gray-50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-800">
<UButton <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
variant="link" <UButton
icon="i-lucide-arrow-left" variant="ghost"
to="/projects" icon="i-lucide-arrow-left"
> to="/projects"
{{ t('projects.projectDetail.backToProjects') }} size="sm"
</UButton> class="text-gray-500 hover:text-gray-900 dark:hover:text-white"
</nav> >
{{ t('projects.projectDetail.backToProjects') }}
<!-- Hero Grid --> </UButton>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<!-- Project Image -->
<div class="rounded-lg overflow-hidden">
<NuxtImg
v-if="project.image"
:src="project.image"
:alt="project.title"
class="w-full h-auto object-cover"
format="webp"
loading="lazy"
/>
</div>
<!-- Project Info -->
<div class="flex flex-col justify-center">
<div class="flex items-center gap-3 mb-4">
<UBadge v-if="project.category" variant="subtle">{{ project.category }}</UBadge>
<span v-if="project.date" class="text-sm text-muted">{{ project.date }}</span>
</div>
<h1 class="text-3xl font-bold mb-4">{{ project.title }}</h1>
<p class="text-lg text-muted mb-6">{{ project.description }}</p>
<!-- CTA Buttons -->
<div class="flex flex-wrap gap-3">
<UButton
v-if="project.demoUrl"
:to="project.demoUrl"
target="_blank"
icon="i-lucide-external-link"
>
{{ t('projects.projectDetail.viewDemo') }}
</UButton>
<UButton
v-if="project.githubUrl"
:to="project.githubUrl"
target="_blank"
variant="soft"
icon="i-lucide-github"
>
{{ t('projects.projectDetail.sourceCode') }}
</UButton>
<UButton
v-for="button in project.buttons"
:key="button.title"
:to="button.link"
target="_blank"
variant="outline"
icon="i-lucide-external-link"
>
{{ button.title }}
</UButton>
</div>
</div> </div>
</div> </div>
<!-- Content Grid: Main + Sidebar --> <!-- Hero section -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <section class="bg-gray-50 dark:bg-gray-900/30 pb-16 pt-8">
<!-- Main Content --> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="lg:col-span-2 space-y-12"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16 items-center">
<!-- About --> <!-- Project Image -->
<section> <div class="rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-800 shadow-lg">
<h2 class="text-2xl font-bold mb-4">{{ t('projects.projectDetail.aboutProject') }}</h2> <NuxtImg
<p class="text-muted leading-relaxed"> v-if="project.image"
{{ project.longDescription || project.description }} :src="project.image"
</p> :alt="project.title"
class="w-full h-auto object-cover"
<!-- Features --> format="webp"
<div v-if="project.features" class="mt-6"> loading="lazy"
<h3 class="text-lg font-semibold mb-3">{{ t('projects.projectDetail.keyFeatures') }}</h3> />
<ul class="space-y-2">
<li v-for="feature in project.features" :key="feature" class="flex items-start gap-2">
<UIcon name="i-lucide-check" class="text-primary mt-1 shrink-0" />
<span>{{ feature }}</span>
</li>
</ul>
</div> </div>
</section>
<!-- Technologies --> <!-- Project Info -->
<section v-if="project.technologies.length"> <div class="flex flex-col justify-center space-y-6">
<h2 class="text-2xl font-bold mb-4">{{ t('projects.projectDetail.technologiesUsed') }}</h2> <div class="flex items-center gap-3">
<div class="flex flex-wrap gap-2"> <UBadge v-if="project.category" variant="subtle" size="md">{{ project.category }}</UBadge>
<TechBadge v-for="tech in project.technologies" :key="tech" :tech="tech" /> <span v-if="project.date" class="text-sm text-gray-400 dark:text-gray-500 font-medium">{{ project.date }}</span>
</div>
</section>
<!-- Gallery Thumbnails -->
<section v-if="project.gallery?.length">
<h2 class="text-2xl font-bold mb-4">{{ t('projects.projectDetail.gallery') }}</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<button
v-for="(image, index) in project.gallery"
:key="index"
class="relative rounded-lg overflow-hidden group cursor-pointer"
@click="galleryRef?.openGallery(index)"
>
<NuxtImg
:src="image"
:alt="`${project.title} - Image ${index + 1}`"
class="w-full h-32 object-cover"
loading="lazy"
format="webp"
/>
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center">
<UIcon name="i-lucide-zoom-in" class="text-white opacity-0 group-hover:opacity-100 transition-opacity text-2xl" />
</div>
</button>
</div>
</section>
</div>
<!-- Sidebar -->
<aside class="space-y-6">
<!-- Project Info Card -->
<UCard>
<template #header>
<h3 class="font-semibold">{{ t('projects.projectDetail.projectInfo') }}</h3>
</template>
<div class="space-y-3 text-sm">
<div v-if="project.date" class="flex justify-between">
<span class="text-muted">{{ t('projects.projectDetail.date') }}</span>
<span class="font-medium">{{ project.date }}</span>
</div> </div>
<div v-if="project.category" class="flex justify-between">
<span class="text-muted">{{ t('projects.projectDetail.category') }}</span>
<span class="font-medium">{{ project.category }}</span>
</div>
</div>
</UCard>
<!-- Related Projects --> <h1 class="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white">{{ project.title }}</h1>
<div v-if="relatedProjects.length > 0"> <p class="text-lg text-gray-500 dark:text-gray-400 leading-relaxed">{{ project.description }}</p>
<h3 class="font-semibold mb-4">{{ t('projects.projectDetail.relatedProjects') }}</h3>
<div class="space-y-3"> <!-- CTA Buttons -->
<NuxtLink <div class="flex flex-wrap gap-3 pt-2">
v-for="related in relatedProjects" <UButton
:key="related.id" v-if="project.demoUrl"
:to="`/project/${related.id}`" :to="project.demoUrl"
class="flex gap-3 p-2 rounded-lg hover:bg-elevated transition-colors" target="_blank"
> icon="i-lucide-external-link"
<NuxtImg size="lg"
v-if="related.image" class="font-semibold"
:src="related.image" >
:alt="related.title" {{ t('projects.projectDetail.viewDemo') }}
width="60" </UButton>
height="45"
class="rounded object-cover shrink-0" <UButton
loading="lazy" v-if="project.githubUrl"
/> :to="project.githubUrl"
<div class="min-w-0"> target="_blank"
<p class="font-medium text-sm truncate">{{ related.title }}</p> variant="soft"
<p class="text-xs text-muted line-clamp-2">{{ related.description }}</p> icon="i-lucide-github"
</div> size="lg"
</NuxtLink> >
{{ t('projects.projectDetail.sourceCode') }}
</UButton>
<UButton
v-for="button in project.buttons"
:key="button.title"
:to="button.link"
target="_blank"
variant="outline"
icon="i-lucide-external-link"
size="lg"
>
{{ button.title }}
</UButton>
</div>
</div> </div>
</div> </div>
</aside> </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>
<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>
<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">
<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>
</li>
</ul>
</div>
</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 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">
<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"
@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"
loading="lazy"
format="webp"
/>
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors duration-300 flex items-center justify-center">
<UIcon name="i-lucide-zoom-in" class="text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 text-xl" />
</div>
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<aside class="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="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">
<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>
</div>
<div v-if="project.category" class="flex justify-between items-center py-2">
<span class="text-gray-500 dark:text-gray-400">{{ t('projects.projectDetail.category') }}</span>
<UBadge variant="subtle" size="xs">{{ project.category }}</UBadge>
</div>
</div>
</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">
<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"
>
<NuxtImg
v-if="related.image"
:src="related.image"
:alt="related.title"
width="60"
height="45"
class="rounded-lg object-cover shrink-0"
loading="lazy"
/>
<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>
</div>
</NuxtLink>
</div>
</div>
</aside>
</div>
</div>
</section>
<!-- Gallery Modal --> <!-- Gallery Modal -->
<ProjectGallery <ProjectGallery
+67 -52
View File
@@ -51,64 +51,79 @@ function resetFilters() {
</script> </script>
<template> <template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div>
<!-- Hero --> <!-- Hero -->
<div class="text-center mb-12"> <section class="pt-16 pb-12 px-4 bg-gray-50 dark:bg-gray-900/30">
<h1 class="text-4xl font-bold mb-4">{{ t('projects.title') }}</h1> <div class="max-w-7xl mx-auto text-center">
<p class="text-lg text-muted max-w-2xl mx-auto">{{ t('projects.subtitle') }}</p> <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>
<!-- Stats --> <!-- Stats -->
<div class="flex justify-center gap-8 mt-8"> <div class="flex justify-center gap-10 mt-10">
<div class="text-center"> <div class="text-center">
<p class="text-3xl font-bold text-primary">{{ totalProjects }}</p> <p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ totalProjects }}</p>
<p class="text-sm text-muted">{{ t('nav.projects') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('nav.projects') }}</p>
</div> </div>
<div class="text-center"> <div class="w-px bg-gray-200 dark:bg-gray-800" />
<p class="text-3xl font-bold text-primary">{{ featuredCount }}</p> <div class="text-center">
<p class="text-sm text-muted">{{ t('home.featuredProjects.title') }}</p> <p class="text-3xl font-extrabold text-brand-500 dark:text-brand-400">{{ featuredCount }}</p>
</div> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('home.featuredProjects.title') }}</p>
<div class="text-center"> </div>
<p class="text-3xl font-bold text-primary">{{ categories.length - 1 }}</p> <div class="w-px bg-gray-200 dark:bg-gray-800" />
<p class="text-sm text-muted">{{ t('projects.categories.all') }}</p> <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>
</div>
</div> </div>
</div> </div>
</div> </section>
<!-- Filters --> <!-- Filters & Grid -->
<div class="flex flex-col sm:flex-row gap-4 items-center mb-8"> <section class="py-12 px-4">
<UInput <div class="max-w-7xl mx-auto">
v-model="searchQuery" <!-- Filter bar -->
icon="i-lucide-search" <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">
:placeholder="t('common.search') + '...'" <UInput
class="w-full sm:w-80" v-model="searchQuery"
/> icon="i-lucide-search"
:placeholder="t('common.search') + '...'"
class="w-full sm:w-72"
size="md"
/>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<UButton <UButton
v-for="category in categories" v-for="category in categories"
:key="category" :key="category"
:variant="selectedCategory === category ? 'solid' : 'soft'" :variant="selectedCategory === category ? 'solid' : 'soft'"
size="sm" :color="selectedCategory === category ? 'primary' : 'neutral'"
@click="selectedCategory = category" size="sm"
> class="font-medium"
{{ category === 'all' ? t('projects.categories.all') : t(`projects.categories.${category.replace(/\s+/g, '').toLowerCase()}`) || category }} @click="selectedCategory = category"
</UButton> >
{{ category === 'all' ? t('projects.categories.all') : t(`projects.categories.${category.replace(/\s+/g, '').toLowerCase()}`) || category }}
</UButton>
</div>
</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">
<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">
<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">
{{ t('common.reset') }}
</UButton>
</div>
</div> </div>
</div> </section>
<!-- Projects Grid -->
<div v-if="filteredProjects.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ProjectCard v-for="project in filteredProjects" :key="project.id" :project="project" />
</div>
<!-- Empty State -->
<div v-else class="text-center py-16">
<UIcon name="i-lucide-search-x" class="text-4xl text-muted mb-4" />
<h3 class="text-lg font-semibold mb-2">{{ t('projects.noResults.title') }}</h3>
<p class="text-muted mb-6">{{ t('projects.noResults.description') }}</p>
<UButton @click="resetFilters" variant="soft">
{{ t('common.reset') }}
</UButton>
</div>
</div> </div>
</template> </template>