Overview
This portfolio is packed with modern features designed to create an impressive online presence. Each feature is built with performance and user experience in mind.
Responsive Design Works beautifully on all devices
Dark Mode Toggle between light and dark themes
Smooth Animations AOS animations and transitions
SEO Optimized Meta tags and sitemap included
Core Features
Dark/Light Mode Toggle
Seamless theme switching with persistent user preference.
Implementation
Configuration
Styling
The theme toggle is implemented using @nuxtjs/color-mode: components/layout/Navbar.vue
< template >
< button
class = "flex gap-2 rounded-xl bg-gray-100 dark:bg-gray-900"
@ click = " $colorMode . preference = $colorMode . value === 'light' ? 'dark' : 'light' "
>
< Icon
v-if = " $colorMode . value === 'light' "
class = "cursor-pointer rounded-lg bg-gray-100 p-1 text-gray-500"
name = "moon"
: outline = " false "
/>
< Icon
v-else
class = "cursor-pointer rounded-lg bg-yellow-50 text-yellow-500"
name = "sunny"
: outline = " false "
/>
</ button >
</ template >
export default defineNuxtConfig ({
modules: [ '@nuxtjs/color-mode' ] ,
colorMode: {
classSuffix: '' , // Use 'dark' class instead of 'dark-mode'
} ,
})
The theme preference is stored in localStorage and persists across sessions.
TailwindCSS dark mode classes are used throughout: < div class = "bg-white dark:bg-gray-800" >
<h1 class="text-gray-900 dark:text-white">
Hello World
</h1>
</ div >
Animated Typing Effect
Dynamic hero section with rotating skill names using Typed.js.
const skills = [ 'Full-stack' , 'Laravel' , 'VueJS' , 'PHP' , 'JS' ]
const typed = ref ( null )
onMounted (() => {
const typedInstance = new Typed ( '.typed' , {
strings: skills ,
typeSpeed: 150 ,
loop: true ,
})
typed . value = typedInstance
})
< template >
< h1 >
Un développeur web freelance
< span class = "typed text-blue-500 dark:text-blue-600" ></ span >
</ h1 >
</ template >
The typing animation starts automatically when the page loads and loops continuously through the skill array.
Project Showcase
Filterable project gallery with multiple categories and detailed information.
Projects Data
Filtering Logic
Display
components/misc/Projects.vue
const projects : Project [] = [
{
image: 'x-memes' ,
title: 'X-Memes' ,
description: 'Plateforme de partage de mêmes favoris venant de Twitter.' ,
technologies: [ 'HTML5' , 'Laravel' , 'VueJS' , 'TailwindCSS' ],
github: 'x-memes' ,
url: 'https://x-memes.com' ,
category: 'website' ,
},
{
image: 'spotify-liked-tracks-sorter' ,
title: 'Spotify liked tracks sorter' ,
description: 'Utilitaire de tri automatique des musiques aimées.' ,
technologies: [ 'HTML5' , 'VueJS' , 'Bootstrap' , 'Api' ],
url: 'https://auto-sorting-spotify-liked-songs.netlify.app' ,
github: 'auto-sorting-spotify-liked-songs' ,
category: 'web-application' ,
},
// More projects...
]
const categories : Category [] = [
{ label: 'Tout' , value: 'all' },
{ label: 'Sites web' , value: 'website' },
{ label: 'Applications web' , value: 'web-application' },
{ label: 'Ressources' , value: 'resources' },
]
const selectedCategory = ref ( 'all' )
const filteredProjects = ref ( projects )
const selectCategory = ( category : string ) => {
selectedCategory . value = category
filteredProjects . value = projects
if ( ! category || category !== 'all' ) {
filteredProjects . value = projects . filter (
( project ) => project . category === category
)
}
}
< template >
<!-- Filter Buttons -->
< Button
v-for = " category in categories "
: type = " selectedCategory === category . value ? 'primary' : 'secondary' "
@ click = " selectCategory ( category . value ) "
>
{{ category . label }}
({{ category . value === 'all' ? projects . length : countProjectsByCategory ( category . value ) }})
</ Button >
<!-- Project Cards -->
< div v-for = " project in filteredProjects " >
< NuxtImg : src = " `images/projects/ ${ project . image } .webp` " />
< h3 > {{ project . title }} </ h3 >
< p > {{ project . description }} </ p >
< Badge v-for = " tech in project . technologies " > {{ tech }} </ Badge >
</ div >
</ template >
Fully functional contact form with spam protection.
< form
name = "contact"
method = "POST"
action = "/contact-submission"
data-netlify-recaptcha = "true"
netlify-honeypot = "bot-field"
data-netlify = "true"
>
<input type="hidden" name="form-name" value="contact" />
<!-- Honeypot for bot protection -->
<p class="hidden">
<label>
Don't fill this out if you're human:
<input name="bot-field" />
</label>
</p>
<Input
id="first_name"
label="Nom"
placeholder="Nom"
required
/>
<Input
id="last_name"
label="Prénom"
placeholder="Prénom"
required
/>
<Input
type="email"
id="email"
placeholder="Adresse e-mail"
label="Adresse e-mail"
required
/>
<Textarea
id="message"
label="Message"
placeholder="Soyez le plus explicite possible"
required
/>
<!-- reCAPTCHA -->
<div data-netlify-recaptcha="true"></div>
<Button type="primary">Envoyer</Button>
</ form >
Remember to enable Netlify Forms in your Netlify dashboard and configure reCAPTCHA for spam protection.
Experience Timeline
Professional experience displayed in an elegant timeline format.
< div class = "space-y-8" >
<ExperienceCard
image="guillaume-cazin.webp"
job="Développeur web freelance"
company="Guillaume Cazin"
period="Freelance (Mars. 2024 - Aujourd'hui)"
:technologies="['HTML5', 'CSS3', 'VueJS', 'PHP', 'Laravel', 'jQuery', 'API']"
/>
<ExperienceCard
image="diatem.webp"
job="Développeur web"
company="Diatem"
period="CDI de 2 ans (Nov. 2020 - Nov. 2022)"
description="Utilisation de VueJS pour créer des interfaces..."
:technologies="['HTML5', 'CSS3', 'VueJS', 'PHP', 'Drupal 8', 'Wordpress', 'Laravel']"
right
/>
</ div >
The timeline features:
Alternating left/right layout
Company logos
Technology badges
Detailed descriptions
Visual timeline connector
Skills Section
Organized skill cards grouped by category.
< div class = "grid gap-5 lg:grid-cols-3" >
<SkillCard
icon="code-slash"
color="blue"
title="Développement"
:skills="[
'HTML',
'CSS',
'Boostrap',
'Tailwind',
'PHP',
'Laravel',
'Twig',
'Vanilla JS',
'VueJS',
'jQuery',
]"
/>
<SkillCard
icon="cog"
color="purple"
title="Outils"
:skills="['Figma', 'PhpStorm', 'Code']"
/>
<SkillCard
icon="cog"
color="yellow"
title="Workflow"
:skills="[
'Workstation Linux',
'Méthodes agile (Scrum, Kanban)',
'Versionning Git',
'Télétravail',
]"
/>
</ div >
Resume/CV Page
Dedicated CV page with downloadable PDF version.
const infos : Information = {
name: 'Guillaume Cazin' ,
age: new Date (). getFullYear () - 1999 ,
role: 'Développeur web' ,
mail: '[email protected] ' ,
phone: '06 10 85 42 18' ,
socials: [
{
label: 'guillaume-cazin.fr' ,
url: 'https://www.guillaume-cazin.fr/' ,
icon: 'link-outline' ,
},
{
label: 'guillaume-cazin' ,
url: 'https://www.linkedin.com/in/guillaume-cazin/' ,
icon: 'logo-linkedin' ,
},
// More social links...
],
}
const skills : Skill [] = [
{ title: 'HTML5 & CSS3' , rating: 'Maîtrise' },
{ title: 'VueJS 3' , rating: 'Maîtrise' },
{ title: 'Laravel 10' , rating: 'Maîtrise' },
// More skills...
]
Printable layout : Desktop view optimized for printing
Mobile fallback : Image version for mobile devices
PDF download : Direct download button
Sections : Contact, About, Skills, Languages, Hobbies, Experience, Education
< a href = "/images/misc/cv-guillaume-cazin.pdf" download >
<Button>Version PDF</Button>
</ a >
<!-- Desktop version -->
< div id = "cv" class = "hidden lg:flex" >
<!-- Structured CV content -->
</ div >
<!-- Mobile version -->
< div class = "block lg:hidden" >
<NuxtImg src="/images/misc/cv.webp" alt="CV" />
</ div >
Component System
Reusable Components
The portfolio uses a comprehensive component library:
Navbar : Responsive navigation with mobile menu
Footer : Site footer with links
Section : Page section wrapper with optional backgrounds
Container : Responsive container
Stack : Vertical spacing utility
Col : Column layout
Card : Generic card with title and description
SkillCard : Skill category card with icon
ExperienceCard : Experience timeline card
Button : Styled button with variants (primary, secondary, ghost, transparent)
Icon : Ionicon wrapper
Text : Typography component
Link : Styled link component
Badge : Technology badge
Projects : Project showcase with filtering
References : Company references gallery
Animate : AOS animation wrapper
BlobBackground : Animated background shapes
Animation System
The portfolio uses nuxt-aos for scroll animations:
components/misc/Animate.vue
< template >
< div
: data-aos = " getAnimation "
: data-aos-duration = " duration "
: data-aos-delay = " delay "
>
< slot / >
</ div >
</ template >
Usage example:
< Animate type = "zoom" to = "in" >
<p>This will zoom in when scrolled into view</p>
</ Animate >
< Animate to = "right" >
<Card title="Slides from right" />
</ Animate >
< Animate to = "left" >
<Card title="Slides from left" />
</ Animate >
Image Optimization @nuxt/image automatically optimizes and lazy-loads images
Code Splitting Automatic code splitting by Nuxt 3
Tree Shaking Unused code eliminated in production builds
Minification CSS and JavaScript minified for production
Image Optimization
<!-- Automatically optimized and lazy-loaded -->
< NuxtImg
src = "/images/misc/avatar.webp"
class = "w-52 rounded-br-3xl rounded-tl-3xl lg:w-80"
alt = "Avatar"
/>
NuxtImg automatically generates responsive images and uses modern formats like WebP.
SEO Features
< Head >
<Meta
name="description"
content="Développeur web freelance full stack spécialisé en Laravel et VueJS."
/>
</ Head >
Page-Specific Meta
useHead ({
title: 'Portfolio - Guillaume Cazin - Développeur web freelance' ,
})
Sitemap
Automatic sitemap generation via @nuxtjs/sitemap:
export default defineNuxtConfig ({
modules: [ '@nuxtjs/sitemap' ] ,
site: {
url: 'https://www.guillaume-cazin.fr' ,
name: 'Guillaume Cazin' ,
trailingSlash: true ,
} ,
})
Robots.txt
routeRules : {
'/contact-submission' : {
robots: false , // Don't index form submission page
},
}
Accessibility Features
Semantic HTML structure
ARIA labels on navigation
Keyboard navigation support
Focus states on interactive elements
Alt text on all images
Proper heading hierarchy
components/layout/Navbar.vue
< nav role = "navigation" aria-label = "navigation" >
<!-- Navigation content -->
</ nav >
Automatic scroll-to-top button that appears after scrolling:
< script setup >
const scrollTop = ref ( 0 )
const scrollTopPositionButtonAppear = 250
const getScrollTop = () => {
scrollTop . value = document . documentElement . scrollTop
}
const scrollToTop = () => {
window . scrollTo ({ top: 0 })
}
onMounted (() => {
window . addEventListener ( 'scroll' , getScrollTop )
})
</ script >
< template >
< div v-if = " scrollTop > scrollTopPositionButtonAppear " >
< Button @ click = " scrollToTop " >
< Icon name = "chevron-up" />
</ Button >
</ div >
</ template >
Background Patterns
Hero Patterns integration for beautiful backgrounds:
< style scoped >
.avatar-background {
@ apply bg-gray- 100 dark :bg-gray-900;
background-image : url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'..." );
}
</ style >
Background patterns from Hero Patterns are embedded as data URIs for optimal performance.
Summary
This portfolio includes:
Visual Features
Dark mode, animations, responsive design, beautiful backgrounds
Interactive Features
Project filtering, contact form, scroll-to-top, mobile menu
Content Features
Experience timeline, skills showcase, project gallery, downloadable CV
Technical Features
SEO optimization, image optimization, code splitting, accessibility
Back to Introduction Learn about the portfolio overview
Quick Start Guide Get started with installation and setup