chore(03-04): remove legacy SPA files and verify GA4 config
- Delete entire src/ directory (160+ legacy Vue SPA files) - Delete old/, nginx.conf, index.html, eslint.config.ts, env.d.ts - GA4 nuxt-gtag already correctly configured (production-only, runtimeConfig) - No formation.vue exists, /formation returns 404 naturally Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -1,22 +0,0 @@
|
|||||||
import { globalIgnores } from 'eslint/config'
|
|
||||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
|
||||||
import pluginVue from 'eslint-plugin-vue'
|
|
||||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
|
|
||||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
|
||||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
|
||||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
|
||||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
|
||||||
|
|
||||||
export default defineConfigWithVueTs(
|
|
||||||
{
|
|
||||||
name: 'app/files-to-lint',
|
|
||||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
|
||||||
},
|
|
||||||
|
|
||||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
|
||||||
|
|
||||||
pluginVue.configs['flat/essential'],
|
|
||||||
vueTsConfigs.recommended,
|
|
||||||
skipFormatting,
|
|
||||||
)
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
|
|
||||||
<!-- Google tag (gtag.js) -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-CDVVNFY6MV"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag() { dataLayer.push(arguments); }
|
|
||||||
gtag('js', new Date());
|
|
||||||
|
|
||||||
gtag('config', 'G-CDVVNFY6MV');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Google AdSense -->
|
|
||||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5219367964457248"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<!-- Primary Meta Tags -->
|
|
||||||
<title>Full Stack Developer Freelance Vue.js React Node.js | Killian Dalcin</title>
|
|
||||||
<meta name="title" content="Full Stack Developer Freelance Vue.js React Node.js | Killian Dalcin">
|
|
||||||
<meta name="description"
|
|
||||||
content="Expert Full Stack Developer freelance specialized in Vue.js, React and Node.js. ✅ Custom web application development ✅ Professional Discord bots ✅ High-performance APIs. Free quote within 24h.">
|
|
||||||
<meta name="keywords"
|
|
||||||
content="full stack developer freelance, vue.js developer freelance, react developer freelance, node.js developer freelance, custom discord bot development, enterprise web application development, javascript typescript expert, rest api graphql developer, freelance web developer france, saas mvp startup development">
|
|
||||||
<meta name="author" content="Killian Dalcin">
|
|
||||||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
|
|
||||||
<meta name="language" content="English">
|
|
||||||
<meta name="revisit-after" content="3 days">
|
|
||||||
<meta name="geo.region" content="FR">
|
|
||||||
<meta name="geo.placename" content="France">
|
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:url" content="https://killiandalcin.fr">
|
|
||||||
<meta property="og:title" content="Full Stack Developer Freelance Vue.js React Node.js | Killian Dalcin">
|
|
||||||
<meta property="og:description"
|
|
||||||
content="Need an expert Full Stack Developer? I create custom web applications, Discord bots and high-performance APIs. Modern technologies, clean code, fast delivery. Free consultation.">
|
|
||||||
<meta property="og:image" content="https://killiandalcin.fr/portfolio-preview.webp">
|
|
||||||
<meta property="og:image:width" content="1200">
|
|
||||||
<meta property="og:image:height" content="630">
|
|
||||||
<meta property="og:locale" content="en_US">
|
|
||||||
<meta property="og:locale:alternate" content="fr_FR">
|
|
||||||
<meta property="og:site_name" content="Killian Dalcin - Full Stack Developer">
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta property="twitter:card" content="summary_large_image">
|
|
||||||
<meta property="twitter:url" content="https://killiandalcin.fr">
|
|
||||||
<meta property="twitter:title" content="Full Stack Developer Freelance Vue.js React Node.js | Killian Dalcin">
|
|
||||||
<meta property="twitter:description"
|
|
||||||
content="Expert Full Stack Developer freelance. Custom web application development, Discord bots, high-performance APIs. Vue.js, React, Node.js. Free quote within 24h.">
|
|
||||||
<meta property="twitter:image" content="https://killiandalcin.fr/portfolio-preview.webp">
|
|
||||||
<meta property="twitter:creator" content="@killiandalcin">
|
|
||||||
|
|
||||||
<!-- Canonical URL -->
|
|
||||||
<link rel="canonical" href="https://killiandalcin.fr">
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<link rel="icon" type="image/webp" href="/favicon.webp">
|
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
|
||||||
|
|
||||||
<!-- Preconnect to external domains -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
|
|
||||||
<!-- Theme Color -->
|
|
||||||
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)">
|
|
||||||
<meta name="theme-color" content="#2563eb" media="(prefers-color-scheme: light)">
|
|
||||||
|
|
||||||
<!-- Structured Data -->
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "ProfessionalService",
|
|
||||||
"@id": "https://killiandalcin.fr/#organization",
|
|
||||||
"name": "Killian Dalcin - Full Stack Developer Freelance",
|
|
||||||
"alternateName": "Killian Dev",
|
|
||||||
"url": "https://killiandalcin.fr",
|
|
||||||
"logo": "https://killiandalcin.fr/logo.webp",
|
|
||||||
"image": "https://killiandalcin.fr/portfolio-preview.webp",
|
|
||||||
"description": "Full Stack Developer freelance expert in Vue.js, React and Node.js. Specialized in custom web application development, professional Discord bots and high-performance APIs.",
|
|
||||||
"priceRange": "€€€",
|
|
||||||
"telephone": "+33-649-193-816",
|
|
||||||
"email": "contact@killiandalcin.fr",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"addressCountry": "FR",
|
|
||||||
"addressRegion": "France"
|
|
||||||
},
|
|
||||||
"openingHoursSpecification": {
|
|
||||||
"@type": "OpeningHoursSpecification",
|
|
||||||
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
|
||||||
"opens": "09:00",
|
|
||||||
"closes": "18:00"
|
|
||||||
},
|
|
||||||
"founder": {
|
|
||||||
"@type": "Person",
|
|
||||||
"name": "Killian Dalcin",
|
|
||||||
"jobTitle": "Senior Full Stack Developer",
|
|
||||||
"alumniOf": "Computer Engineering School",
|
|
||||||
"knowsAbout": ["Vue.js", "React", "Node.js", "TypeScript", "JavaScript", "MongoDB", "PostgreSQL", "Docker", "REST API", "GraphQL", "Discord.js", "Web Development", "Software Architecture"],
|
|
||||||
"sameAs": [
|
|
||||||
"https://github.com/killiandalcin",
|
|
||||||
"https://linkedin.com/in/killian-dalcin",
|
|
||||||
"https://www.fiverr.com/users/mr_kayjaydee",
|
|
||||||
"https://twitter.com/killiandalcin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hasOfferCatalog": {
|
|
||||||
"@type": "OfferCatalog",
|
|
||||||
"name": "Web Development Services",
|
|
||||||
"itemListElement": [
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Vue.js/React Web Application Development",
|
|
||||||
"description": "Creation of modern and high-performance web applications with Vue.js or React. Responsive user interface, SEO optimization, scalable architecture."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Node.js Backend Development & API",
|
|
||||||
"description": "Design of robust REST and GraphQL APIs with Node.js. Microservices architecture, secure authentication, optimal performance."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Custom Discord Bot Development",
|
|
||||||
"description": "Development of professional Discord bots with advanced features. Moderation, music, games, API integrations, web dashboard."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Offer",
|
|
||||||
"itemOffered": {
|
|
||||||
"@type": "Service",
|
|
||||||
"name": "Maintenance & Technical Support",
|
|
||||||
"description": "Continuous maintenance, security updates and technical support for your applications. 24/7 monitoring and rapid interventions."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"aggregateRating": {
|
|
||||||
"@type": "AggregateRating",
|
|
||||||
"ratingValue": "5",
|
|
||||||
"bestRating": "5",
|
|
||||||
"worstRating": "1",
|
|
||||||
"ratingCount": "47",
|
|
||||||
"reviewCount": "47"
|
|
||||||
},
|
|
||||||
"review": [
|
|
||||||
{
|
|
||||||
"@type": "Review",
|
|
||||||
"reviewRating": {
|
|
||||||
"@type": "Rating",
|
|
||||||
"ratingValue": "5",
|
|
||||||
"bestRating": "5"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"name": "Marie L."
|
|
||||||
},
|
|
||||||
"reviewBody": "Excellent developer! Vue.js application delivered on time with exceptional code quality. I highly recommend."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Review",
|
|
||||||
"reviewRating": {
|
|
||||||
"@type": "Rating",
|
|
||||||
"ratingValue": "5",
|
|
||||||
"bestRating": "5"
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"@type": "Person",
|
|
||||||
"name": "Thomas B."
|
|
||||||
},
|
|
||||||
"reviewBody": "Discord bot working perfectly with all requested features. Responsive and professional support. Thank you!"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Breadcrumb Schema -->
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "BreadcrumbList",
|
|
||||||
"itemListElement": [
|
|
||||||
{
|
|
||||||
"@type": "ListItem",
|
|
||||||
"position": 1,
|
|
||||||
"name": "Home",
|
|
||||||
"item": "https://killiandalcin.fr"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- FAQ Schema -->
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "FAQPage",
|
|
||||||
"mainEntity": [
|
|
||||||
{
|
|
||||||
"@type": "Question",
|
|
||||||
"name": "What are your rates for custom web development?",
|
|
||||||
"acceptedAnswer": {
|
|
||||||
"@type": "Answer",
|
|
||||||
"text": "My rates vary according to project complexity. A simple web application starts from €2000, while a complex platform can go up to €15000+. I always provide a detailed free quote within 24h after analyzing your needs."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Question",
|
|
||||||
"name": "How long does it take to develop a Vue.js application?",
|
|
||||||
"acceptedAnswer": {
|
|
||||||
"@type": "Answer",
|
|
||||||
"text": "The timeline depends on complexity: a simple application (3-5 pages) takes 2-3 weeks, a medium application (10-15 pages with backend) 4-8 weeks, and a complex platform 2-4 months. I always provide a detailed schedule."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "Question",
|
|
||||||
"name": "Do you offer maintenance after delivery?",
|
|
||||||
"acceptedAnswer": {
|
|
||||||
"@type": "Answer",
|
|
||||||
"text": "Yes, I offer monthly maintenance contracts including: security updates, bug fixes, small evolutions, 24/7 monitoring and technical support. Rates start from €300/month depending on your needs."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script defer src="https://umami.killiandalcin.fr/script.js"
|
|
||||||
data-website-id="83631152-9b6b-4724-aad1-828459ff36dc"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
|
|
||||||
<!-- No JavaScript Fallback -->
|
|
||||||
<noscript>
|
|
||||||
<div style="text-align: center; padding: 2rem;">
|
|
||||||
<h1>JavaScript Required</h1>
|
|
||||||
<p>This portfolio requires JavaScript to function properly. Please enable JavaScript in your browser settings to
|
|
||||||
view the full experience.</p>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optional: Add error pages
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
cards = [
|
|
||||||
{
|
|
||||||
title: "Virtual Tour",
|
|
||||||
image: require("../assets/images/virtualtour.png"),
|
|
||||||
description: "My high school teacher and me had an idea to create a Virtual tour with 360° vidéos to allow everyone to visit the school from the web.",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
title: "Visit",
|
|
||||||
link: "https://www.lycee-chabanne16.fr/visites/BACSN/index.htm",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Xinko",
|
|
||||||
image: require("../assets/images/xinko.png"),
|
|
||||||
description: "Xinko is a multipurpose bot that can help you create and manage your discord servers with ease and fun. It has many commands and features.",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
title: "Website",
|
|
||||||
link: "https://google.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Invite",
|
|
||||||
link: "https://discord.com/api/oauth2/authorize?client_id=1035571329866407976&permissions=292288982151&scope=applications.commands%20bot",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Image Manipulation",
|
|
||||||
image: require("../assets/images/dig.png"),
|
|
||||||
description: "Discord Image Generation: NPM package for code-based image manipulation. Originally an API, now open-source.",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
title: "Repository",
|
|
||||||
link: "https://git.mrkayjaydee.xyz/Mr-KayJayDee/discord-image-generation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "NPM Package",
|
|
||||||
link: "https://www.npmjs.com/package/discord-image-generation",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Primate Web Admin",
|
|
||||||
image: require("../assets/images/primate.png"),
|
|
||||||
description: "Primate Web Admin is a Web interface to manage Primate that is a Munki-like deployment tool for Windows.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Instagram Bot",
|
|
||||||
image: require("../assets/images/instagram.png"),
|
|
||||||
description: "Fully functional Instagram bot using Insta.js by androz2091. It has many commands. Generate images with commands like: !stonk or !invert.",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
title: "Repository",
|
|
||||||
link: "https://git.mrkayjaydee.xyz/Mr-KayJayDee/instagram-bot",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Crowdin Status Bot",
|
|
||||||
image: require("../assets/images/crowdin.png"),
|
|
||||||
description: "A bot that fetches Crowdin translation status and updates Discord messages with the latest status. Stay informed on progress!",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
title: "Repository",
|
|
||||||
link: "https://git.mrkayjaydee.xyz/Mr-KayJayDee/discord-crowdin-status",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
{
|
|
||||||
"programming": [
|
|
||||||
{
|
|
||||||
"name": "JavaScript",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "javascript.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Bash",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "bash.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Markdown",
|
|
||||||
"level": "Beginner",
|
|
||||||
"image": "markdown.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "TypeScript",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "typescript.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Node.js",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "nodejs.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Nginx",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "nginx.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"front": [
|
|
||||||
{
|
|
||||||
"name": "Angular",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "angular.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "HTML",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "HTML.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CSS",
|
|
||||||
"level": "Beginner",
|
|
||||||
"image": "css.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "React",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "react.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Vue.JS",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "vuejs.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Figma",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "figma.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Wordpress",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "wordpress.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"database": [
|
|
||||||
{
|
|
||||||
"name": "MongoDB",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "mongodb.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Redis",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "redis.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MYSQL",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "mysql.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SQLite",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "sqlite.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"devtools": [
|
|
||||||
{
|
|
||||||
"name": "Docker",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "docker.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Discord Bot",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "discordbot.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Postman",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "postman.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "FileZilla",
|
|
||||||
"level": "Beginner",
|
|
||||||
"image": "filezilla.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Termius",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "termius.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GitHub",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "github.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Git",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "git.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "npm",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "npm.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GitLab",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "gitlab.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Visual Studio Code",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "vscode.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Atom",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "atom.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DB Browser for SQLite",
|
|
||||||
"level": "Beginner",
|
|
||||||
"image": "sqlitebrowser.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "HeidiSQL",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "heidisql.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MySQL Workbench",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "mysqlworkbench.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GitKraken",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "gitkraken.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"operating_systems": [
|
|
||||||
{
|
|
||||||
"name": "Linux",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "linux.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debian",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "debian.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Arch Linux",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "archlinux.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Ubuntu",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "ubuntu.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Kali Linux",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "kalilinux.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "macOS",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "macos.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Windows",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "windows.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Deepin",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "deepin.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Android",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "android.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Wear OS",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "wearos.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "watchOS",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "watchos.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "iOS",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "ios.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"socials": [
|
|
||||||
{
|
|
||||||
"name": "Discord",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "discord.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Instagram",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "instagram.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "LinkedIn",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "linkedin.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Twitter",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "twitter.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reddit",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "reddit.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Messenger",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "messenger.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "WhatsApp",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "whatsapp.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Facebook",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "facebook.png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Telegram",
|
|
||||||
"level": "Intermediate",
|
|
||||||
"image": "telegram.png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { RouterView, useRoute } from 'vue-router'
|
|
||||||
import { watch, nextTick } from 'vue'
|
|
||||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
|
||||||
import AppFooter from '@/components/layout/AppFooter.vue'
|
|
||||||
import { useTheme } from '@/composables/useTheme'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
// Initialize theme
|
|
||||||
useTheme()
|
|
||||||
|
|
||||||
// Force scroll to top on route change (backup solution)
|
|
||||||
watch(() => route.fullPath, () => {
|
|
||||||
nextTick(() => {
|
|
||||||
window.scrollTo({ top: 0, behavior: 'auto' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="app" class="min-h-screen flex flex-col">
|
|
||||||
<AppHeader />
|
|
||||||
|
|
||||||
<div class="flex-grow">
|
|
||||||
<RouterView v-slot="{ Component }" :key="$route.fullPath">
|
|
||||||
<transition mode="out-in" enter-active-class="transition duration-300 ease-out"
|
|
||||||
enter-from-class="transform opacity-0" enter-to-class="transform opacity-100"
|
|
||||||
leave-active-class="transition duration-200 ease-in" leave-from-class="transform opacity-100"
|
|
||||||
leave-to-class="transform opacity-0">
|
|
||||||
<component :is="Component" />
|
|
||||||
</transition>
|
|
||||||
</RouterView>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AppFooter />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Remove default margins */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus styles */
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid #2563eb;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 798 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,901 +0,0 @@
|
|||||||
@import 'tailwindcss/preflight';
|
|
||||||
@import 'tailwindcss/utilities';
|
|
||||||
|
|
||||||
/* Modern CSS Reset & Base Styles */
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Properties - Design System */
|
|
||||||
:root {
|
|
||||||
/* Colors */
|
|
||||||
--color-primary: #85cb85;
|
|
||||||
--color-primary-dark: #6bb06b;
|
|
||||||
--color-primary-light: #a3d6a3;
|
|
||||||
--color-secondary: #7c3aed;
|
|
||||||
--color-accent: #f59e0b;
|
|
||||||
--color-success: #10b981;
|
|
||||||
--color-warning: #f59e0b;
|
|
||||||
--color-error: #ef4444;
|
|
||||||
--color-info: #3b82f6;
|
|
||||||
|
|
||||||
/* Grays */
|
|
||||||
--color-gray-50: #f9fafb;
|
|
||||||
--color-gray-100: #f3f4f6;
|
|
||||||
--color-gray-200: #e5e7eb;
|
|
||||||
--color-gray-300: #d1d5db;
|
|
||||||
--color-gray-400: #9ca3af;
|
|
||||||
--color-gray-500: #6b7280;
|
|
||||||
--color-gray-600: #4b5563;
|
|
||||||
--color-gray-700: #374151;
|
|
||||||
--color-gray-800: #1f2937;
|
|
||||||
--color-gray-900: #111827;
|
|
||||||
|
|
||||||
/* Background */
|
|
||||||
--bg-primary: #ffffff;
|
|
||||||
--bg-secondary: var(--color-gray-50);
|
|
||||||
--bg-tertiary: var(--color-gray-100);
|
|
||||||
--bg-dark: var(--color-gray-900);
|
|
||||||
|
|
||||||
/* Text */
|
|
||||||
--text-primary: var(--color-gray-900);
|
|
||||||
--text-secondary: var(--color-gray-600);
|
|
||||||
--text-tertiary: var(--color-gray-500);
|
|
||||||
--text-inverse: #ffffff;
|
|
||||||
|
|
||||||
/* Spacing */
|
|
||||||
--space-xs: 0.25rem;
|
|
||||||
--space-sm: 0.5rem;
|
|
||||||
--space-md: 1rem;
|
|
||||||
--space-lg: 1.5rem;
|
|
||||||
--space-xl: 2rem;
|
|
||||||
--space-2xl: 3rem;
|
|
||||||
--space-3xl: 4rem;
|
|
||||||
--space-4xl: 6rem;
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
--font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--font-family-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
||||||
|
|
||||||
--font-size-xs: 0.75rem;
|
|
||||||
--font-size-sm: 0.875rem;
|
|
||||||
--font-size-base: 1rem;
|
|
||||||
--font-size-lg: 1.125rem;
|
|
||||||
--font-size-xl: 1.25rem;
|
|
||||||
--font-size-2xl: 1.5rem;
|
|
||||||
--font-size-3xl: 1.875rem;
|
|
||||||
--font-size-4xl: 2.25rem;
|
|
||||||
--font-size-5xl: 3rem;
|
|
||||||
--font-size-6xl: 3.75rem;
|
|
||||||
|
|
||||||
--font-weight-light: 300;
|
|
||||||
--font-weight-normal: 400;
|
|
||||||
--font-weight-medium: 500;
|
|
||||||
--font-weight-semibold: 600;
|
|
||||||
--font-weight-bold: 700;
|
|
||||||
--font-weight-extrabold: 800;
|
|
||||||
|
|
||||||
--line-height-tight: 1.25;
|
|
||||||
--line-height-snug: 1.375;
|
|
||||||
--line-height-normal: 1.5;
|
|
||||||
--line-height-relaxed: 1.625;
|
|
||||||
--line-height-loose: 2;
|
|
||||||
|
|
||||||
/* Borders */
|
|
||||||
--border-radius-sm: 0.375rem;
|
|
||||||
--border-radius-md: 0.5rem;
|
|
||||||
--border-radius-lg: 0.75rem;
|
|
||||||
--border-radius-xl: 1rem;
|
|
||||||
--border-radius-2xl: 1.5rem;
|
|
||||||
--border-radius-full: 9999px;
|
|
||||||
|
|
||||||
--border-width: 1px;
|
|
||||||
--border-color: var(--color-gray-200);
|
|
||||||
|
|
||||||
/* Shadows */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|
||||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
|
||||||
|
|
||||||
/* Transitions */
|
|
||||||
--transition-fast: 150ms ease-in-out;
|
|
||||||
--transition-normal: 300ms ease-in-out;
|
|
||||||
--transition-slow: 500ms ease-in-out;
|
|
||||||
|
|
||||||
/* Z-index */
|
|
||||||
--z-dropdown: 1000;
|
|
||||||
--z-sticky: 1020;
|
|
||||||
--z-fixed: 1030;
|
|
||||||
--z-modal: 1040;
|
|
||||||
--z-popover: 1050;
|
|
||||||
--z-tooltip: 1060;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Mode Variables */
|
|
||||||
.dark {
|
|
||||||
/* Colors - Dark mode overrides */
|
|
||||||
--color-primary: #a3d6a3;
|
|
||||||
--color-primary-dark: #85cb85;
|
|
||||||
--color-primary-light: #c3e6c3;
|
|
||||||
|
|
||||||
/* Background - Dark mode */
|
|
||||||
--bg-primary: #111827;
|
|
||||||
--bg-secondary: #1f2937;
|
|
||||||
--bg-tertiary: #374151;
|
|
||||||
--bg-dark: #000000;
|
|
||||||
|
|
||||||
/* Text - Dark mode */
|
|
||||||
--text-primary: #f9fafb;
|
|
||||||
--text-secondary: #d1d5db;
|
|
||||||
--text-tertiary: #9ca3af;
|
|
||||||
--text-inverse: #111827;
|
|
||||||
|
|
||||||
/* Borders - Dark mode */
|
|
||||||
--border-color: #374151;
|
|
||||||
|
|
||||||
/* Shadows - Dark mode */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
||||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
|
|
||||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base HTML Elements */
|
|
||||||
html {
|
|
||||||
font-family: var(--font-family-sans);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
line-height: var(--line-height-normal);
|
|
||||||
color: var(--text-primary);
|
|
||||||
background-color: var(--bg-primary);
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typography */
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
line-height: var(--line-height-tight);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: var(--font-size-5xl);
|
|
||||||
font-weight: var(--font-weight-extrabold);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: var(--font-size-4xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: var(--font-size-3xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: var(--font-size-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: var(--line-height-relaxed);
|
|
||||||
margin-bottom: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--color-primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout Components */
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.container {
|
|
||||||
padding: 0 var(--space-lg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.container {
|
|
||||||
padding: 0 var(--space-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button System */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
padding: var(--space-md) var(--space-xl);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
line-height: 1.2;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: var(--border-radius-xl);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:focus {
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Primary Button - Vert avec effet moderne */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
|
||||||
color: #1f2937;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 2px 8px rgba(133, 203, 133, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary-dark) 0%, var(--color-primary) 100%);
|
|
||||||
color: #1f2937;
|
|
||||||
box-shadow: 0 4px 12px rgba(133, 203, 133, 0.25);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
color: #1f2937;
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 1px 4px rgba(133, 203, 133, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Secondary Button - Contour vert */
|
|
||||||
.btn-secondary {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
|
||||||
color: #1f2937;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 3px 10px rgba(133, 203, 133, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ghost Button - Subtil */
|
|
||||||
.btn-ghost {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Outline Button - Alias pour secondary */
|
|
||||||
.btn-outline {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
|
||||||
color: #1f2937;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 3px 10px rgba(133, 203, 133, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Sizes */
|
|
||||||
.btn-sm {
|
|
||||||
padding: var(--space-sm) var(--space-lg);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-lg {
|
|
||||||
padding: var(--space-lg) var(--space-2xl);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button with icons */
|
|
||||||
.btn-icon {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card System */
|
|
||||||
.card {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: var(--border-width) solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xl);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
transition: all var(--transition-normal);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: var(--shadow-xl);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: var(--space-xl);
|
|
||||||
border-bottom: var(--border-width) solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
padding: var(--space-xl);
|
|
||||||
border-top: var(--border-width) solid var(--border-color);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badge System */
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-xs);
|
|
||||||
padding: var(--space-xs) var(--space-md);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
border-radius: var(--border-radius-full);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-primary {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-secondary {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-success {
|
|
||||||
background: var(--color-success);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-warning {
|
|
||||||
background: var(--color-warning);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-error {
|
|
||||||
background: var(--color-error);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements */
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input,
|
|
||||||
.form-textarea,
|
|
||||||
.form-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-md);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
line-height: var(--line-height-normal);
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: var(--border-width) solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus,
|
|
||||||
.form-textarea:focus,
|
|
||||||
.form-select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation */
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
position: relative;
|
|
||||||
padding: var(--space-sm) var(--space-md);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: color var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover,
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -2px;
|
|
||||||
left: 50%;
|
|
||||||
width: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover::after,
|
|
||||||
.nav-link.active::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: var(--z-sticky);
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-bottom: var(--border-width) solid var(--border-color);
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header dark mode */
|
|
||||||
.dark .header {
|
|
||||||
background: rgba(17, 24, 39, 0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-md);
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
|
||||||
border-radius: var(--border-radius-lg);
|
|
||||||
color: var(--text-inverse);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero Section */
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23f3f4f6' fill-opacity='0.4'%3E%3Ccircle cx='30' cy='30' r='1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero dark mode pattern */
|
|
||||||
.dark .hero::before {
|
|
||||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23374151' fill-opacity='0.4'%3E%3Ccircle cx='30' cy='30' r='1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(var(--font-size-4xl), 5vw, var(--font-size-6xl));
|
|
||||||
font-weight: var(--font-weight-extrabold);
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--space-2xl);
|
|
||||||
line-height: var(--line-height-relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Spacing */
|
|
||||||
.section {
|
|
||||||
padding: var(--space-4xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-sm {
|
|
||||||
padding: var(--space-2xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-lg {
|
|
||||||
padding: var(--space-4xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid System */
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-1 {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.grid-cols-4,
|
|
||||||
.grid-cols-3,
|
|
||||||
.grid-cols-2 {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Utilities */
|
|
||||||
.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-sm {
|
|
||||||
gap: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-md {
|
|
||||||
gap: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-lg {
|
|
||||||
gap: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-xl {
|
|
||||||
gap: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-sm {
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-md {
|
|
||||||
margin-bottom: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-lg {
|
|
||||||
margin-bottom: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-xl {
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2xl {
|
|
||||||
margin-bottom: var(--space-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
/* Page Entrance Animations */
|
|
||||||
@keyframes pageEnter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInRight {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInLeft {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-30px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scaleIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Page Animation Classes */
|
|
||||||
.page-enter {
|
|
||||||
animation: pageEnter 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Load Animation Classes */
|
|
||||||
.animate-fade-in-up {
|
|
||||||
animation: fadeInUp 0.6s ease-out forwards;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fadeIn 0.6s ease-out forwards;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-in-right {
|
|
||||||
animation: slideInRight 0.6s ease-out forwards;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-in-left {
|
|
||||||
animation: slideInLeft 0.6s ease-out forwards;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-scale-in {
|
|
||||||
animation: scaleIn 0.6s ease-out forwards;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Menu */
|
|
||||||
.mobile-menu {
|
|
||||||
position: fixed;
|
|
||||||
top: 80px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border-bottom: var(--border-width) solid var(--border-color);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
z-index: var(--z-dropdown);
|
|
||||||
transform: translateY(-100%);
|
|
||||||
transition: transform var(--transition-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu.open {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: var(--space-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-nav .nav-link {
|
|
||||||
padding: var(--space-md) 0;
|
|
||||||
border-bottom: var(--border-width) solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-nav .nav-link:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: var(--space-4xl) 0 var(--space-xl);
|
|
||||||
border-top: var(--border-width) solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a:hover {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--font-size-5xl: 2.5rem;
|
|
||||||
--font-size-4xl: 2rem;
|
|
||||||
--font-size-3xl: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0 var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
min-height: 80vh;
|
|
||||||
padding: var(--space-2xl) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: var(--space-2xl) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus States */
|
|
||||||
*:focus-visible {
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth Transitions */
|
|
||||||
* {
|
|
||||||
transition-property:
|
|
||||||
color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow,
|
|
||||||
transform, filter, backdrop-filter;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 150ms;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="contact-method">
|
|
||||||
<div class="contact-icon" :class="iconClass">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="iconPath" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="contact-info">
|
|
||||||
<div class="contact-title">{{ title }}</div>
|
|
||||||
<component :is="linkComponent" :href="href" :class="linkClass">
|
|
||||||
{{ text }}
|
|
||||||
</component>
|
|
||||||
<div class="contact-description">{{ description }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
type: 'email' | 'phone' | 'location'
|
|
||||||
title: string
|
|
||||||
text: string
|
|
||||||
description: string
|
|
||||||
href?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
|
|
||||||
const iconClass = computed(() => `contact-icon-${props.type}`)
|
|
||||||
|
|
||||||
const linkComponent = computed(() => props.href ? 'a' : 'div')
|
|
||||||
|
|
||||||
const linkClass = computed(() => props.href ? 'contact-link' : 'contact-text')
|
|
||||||
|
|
||||||
const iconPath = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case 'email':
|
|
||||||
return 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'
|
|
||||||
case 'phone':
|
|
||||||
return 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z'
|
|
||||||
case 'location':
|
|
||||||
return 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z'
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/ContactMethod.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="cta-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="cta-content text-center">
|
|
||||||
<h2 class="cta-title">{{ title }}</h2>
|
|
||||||
<p class="cta-subtitle">{{ subtitle }}</p>
|
|
||||||
<a :href="ctaUrl" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-lg">
|
|
||||||
{{ ctaText }}
|
|
||||||
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
ctaUrl: string
|
|
||||||
ctaText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/FiverrCta.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="fiverr-hero">
|
|
||||||
<div class="container">
|
|
||||||
<div class="hero-content text-center">
|
|
||||||
<h1 class="hero-title">{{ title }}</h1>
|
|
||||||
<p class="hero-subtitle">{{ subtitle }}</p>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div v-for="stat in stats" :key="stat.label" class="stat-item">
|
|
||||||
<div class="stat-number">{{ stat.number }}</div>
|
|
||||||
<div class="stat-label">{{ stat.label }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTA Button -->
|
|
||||||
<div class="hero-cta">
|
|
||||||
<a :href="ctaUrl" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-lg">
|
|
||||||
{{ ctaText }}
|
|
||||||
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Stat {
|
|
||||||
number: string | number
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
stats: Stat[]
|
|
||||||
ctaUrl: string
|
|
||||||
ctaText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/FiverrHero.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<template>
|
|
||||||
<article class="card group service-card">
|
|
||||||
<!-- Image -->
|
|
||||||
<div class="service-image">
|
|
||||||
<img :src="imageUrl" :alt="title" loading="lazy">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Price & Status -->
|
|
||||||
<div class="service-header">
|
|
||||||
<span class="badge badge-primary">{{ price }}</span>
|
|
||||||
<span :class="statusBadgeClass">{{ statusText }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<h3 class="service-title">{{ title }}</h3>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="service-description">{{ description }}</p>
|
|
||||||
|
|
||||||
<!-- Features -->
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="features-title">{{ featuresTitle }}</div>
|
|
||||||
<ul class="features-items">
|
|
||||||
<li v-for="(feature, index) in displayedFeatures" :key="index" class="feature-item">
|
|
||||||
<svg class="feature-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
{{ feature }}
|
|
||||||
</li>
|
|
||||||
<li v-if="hasMoreFeatures" class="more-features">
|
|
||||||
+{{ remainingFeaturesCount }} {{ moreFeaturesText }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action -->
|
|
||||||
<div class="service-actions">
|
|
||||||
<a v-if="isAvailable" :href="url" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">
|
|
||||||
{{ orderText }}
|
|
||||||
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button v-else class="btn btn-secondary btn-sm" disabled>
|
|
||||||
{{ learnMoreText }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
price: string
|
|
||||||
url: string
|
|
||||||
imageUrl: string
|
|
||||||
features: string[]
|
|
||||||
featuresTitle: string
|
|
||||||
orderText: string
|
|
||||||
learnMoreText: string
|
|
||||||
moreFeaturesText: string
|
|
||||||
comingSoonText: string
|
|
||||||
availableText: string
|
|
||||||
maxFeatures?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
maxFeatures: 5
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAvailable = computed(() => props.url !== '#')
|
|
||||||
|
|
||||||
const displayedFeatures = computed(() =>
|
|
||||||
props.features.slice(0, props.maxFeatures)
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasMoreFeatures = computed(() =>
|
|
||||||
props.features.length > props.maxFeatures
|
|
||||||
)
|
|
||||||
|
|
||||||
const remainingFeaturesCount = computed(() =>
|
|
||||||
props.features.length - props.maxFeatures
|
|
||||||
)
|
|
||||||
|
|
||||||
const statusText = computed(() =>
|
|
||||||
isAvailable.value ? props.availableText : props.comingSoonText
|
|
||||||
)
|
|
||||||
|
|
||||||
const statusBadgeClass = computed(() =>
|
|
||||||
isAvailable.value ? 'badge badge-success text-xs' : 'badge badge-warning text-xs'
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/FiverrServiceCard.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
|
||||||
import { useAssets } from '@/composables/useAssets'
|
|
||||||
import './styles/GalleryModal.css'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isOpen: boolean
|
|
||||||
currentImage: string
|
|
||||||
currentIndex: number
|
|
||||||
totalImages: number
|
|
||||||
hasNext: boolean
|
|
||||||
hasPrevious: boolean
|
|
||||||
projectTitle: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
close: []
|
|
||||||
next: []
|
|
||||||
previous: []
|
|
||||||
goTo: [index: number]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { getImageUrl } = useAssets()
|
|
||||||
|
|
||||||
// Keyboard navigation
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
|
||||||
if (!props.isOpen) return
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case 'Escape':
|
|
||||||
emit('close')
|
|
||||||
break
|
|
||||||
case 'ArrowLeft':
|
|
||||||
if (props.hasPrevious) emit('previous')
|
|
||||||
break
|
|
||||||
case 'ArrowRight':
|
|
||||||
if (props.hasNext) emit('next')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', handleKeydown)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="isOpen" class="gallery-modal" @click="emit('close')">
|
|
||||||
<div class="gallery-modal-overlay"></div>
|
|
||||||
|
|
||||||
<!-- Close Button -->
|
|
||||||
<button class="gallery-close" @click="emit('close')">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Navigation Buttons -->
|
|
||||||
<button v-if="hasPrevious" class="gallery-nav gallery-nav-prev" @click.stop="emit('previous')">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button v-if="hasNext" class="gallery-nav gallery-nav-next" @click.stop="emit('next')">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Main Image -->
|
|
||||||
<div class="gallery-content" @click.stop>
|
|
||||||
<img :src="getImageUrl(currentImage)" :alt="`${projectTitle} - Image ${currentIndex + 1}`"
|
|
||||||
class="gallery-image">
|
|
||||||
|
|
||||||
<!-- Image Info -->
|
|
||||||
<div class="gallery-info">
|
|
||||||
<h3 class="gallery-title">{{ projectTitle }}</h3>
|
|
||||||
<p class="gallery-counter">{{ currentIndex + 1 }} / {{ totalImages }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thumbnails -->
|
|
||||||
<div v-if="totalImages > 1" class="gallery-thumbnails">
|
|
||||||
<button v-for="(_, index) in totalImages" :key="index" class="gallery-thumbnail"
|
|
||||||
:class="{ active: index === currentIndex }" @click="emit('goTo', index)">
|
|
||||||
<div class="thumbnail-indicator"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
|
|
||||||
const { currentLocale, switchLocale, isEnglish, isFrench } = useI18n()
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
|
||||||
{ code: 'en', name: 'English', flag: '🇬🇧' }
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="language-switcher">
|
|
||||||
<div class="language-switcher-buttons">
|
|
||||||
<button v-for="lang in languages" :key="lang.code" @click="switchLocale(lang.code)" :class="[
|
|
||||||
'language-btn',
|
|
||||||
{ 'active': currentLocale === lang.code }
|
|
||||||
]" :title="lang.name">
|
|
||||||
<span class="flag">{{ lang.flag }}</span>
|
|
||||||
<span class="lang-code">{{ lang.code.toUpperCase() }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/LanguageSwitcher.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { type Project } from '@/types'
|
|
||||||
import { useAssets } from '@/composables/useAssets'
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
project: Project
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const { getImageUrl } = useAssets()
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
// Get the actual image URL
|
|
||||||
const imageUrl = computed(() => {
|
|
||||||
return getImageUrl(props.project.image)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get translated project data
|
|
||||||
const translatedTitle = computed(() => {
|
|
||||||
return t(`projectData.${props.project.id}.title`, props.project.title)
|
|
||||||
})
|
|
||||||
|
|
||||||
const translatedDescription = computed(() => {
|
|
||||||
return t(`projectData.${props.project.id}.description`, props.project.description)
|
|
||||||
})
|
|
||||||
|
|
||||||
const translatedCategory = computed(() => {
|
|
||||||
if (!props.project.category) return ''
|
|
||||||
const categoryKey = props.project.category.replace(/\s+/g, '').toLowerCase()
|
|
||||||
return t(`projects.categories.${categoryKey}`, props.project.category)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<article class="card group" itemscope itemtype="https://schema.org/CreativeWork">
|
|
||||||
<!-- Image -->
|
|
||||||
<div class="project-image">
|
|
||||||
<img :src="imageUrl" :alt="`${translatedTitle} - ${translatedDescription.slice(0, 60)}...`" loading="lazy"
|
|
||||||
width="400" height="300" itemprop="image">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Category & Date -->
|
|
||||||
<div class="project-meta">
|
|
||||||
<span v-if="project.category" class="badge badge-primary" itemprop="genre">
|
|
||||||
{{ translatedCategory }}
|
|
||||||
</span>
|
|
||||||
<time v-if="project.date" class="text-sm text-secondary" :datetime="project.date" itemprop="dateCreated">
|
|
||||||
{{ project.date }}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<h3 class="project-title" itemprop="name">
|
|
||||||
{{ translatedTitle }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="project-description" itemprop="description">
|
|
||||||
{{ translatedDescription }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Technologies -->
|
|
||||||
<div v-if="project.technologies && project.technologies.length > 0" class="project-technologies"
|
|
||||||
itemprop="keywords">
|
|
||||||
<span v-for="tech in project.technologies.slice(0, 3)" :key="tech" class="badge badge-secondary text-xs">
|
|
||||||
{{ tech }}
|
|
||||||
</span>
|
|
||||||
<span v-if="project.technologies.length > 3" class="badge badge-secondary text-xs">
|
|
||||||
+{{ project.technologies.length - 3 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action -->
|
|
||||||
<div class="project-actions">
|
|
||||||
<RouterLink :to="`/project/${project.id}`" class="btn btn-secondary btn-sm"
|
|
||||||
:aria-label="`View details about ${translatedTitle} project`" itemprop="url">
|
|
||||||
{{ t('projects.buttons.viewProject') }}
|
|
||||||
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
||||||
</svg>
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/ProjectCard.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="faq-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="faq-header text-center mb-2xl">
|
|
||||||
<h2 class="section-title">{{ title }}</h2>
|
|
||||||
<p class="section-subtitle">{{ subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-grid">
|
|
||||||
<div v-for="(faq, index) in faqs" :key="index" class="faq-item" :class="{ 'active': activeIndex === index }">
|
|
||||||
<button class="faq-question" @click="toggleFAQ(index)" :aria-expanded="activeIndex === index">
|
|
||||||
<span class="question-text">{{ faq.question }}</span>
|
|
||||||
<svg class="faq-icon" :class="{ 'rotated': activeIndex === index }" fill="none" stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="faq-answer" :class="{ 'open': activeIndex === index }">
|
|
||||||
<div class="answer-content">
|
|
||||||
<p v-html="faq.answer"></p>
|
|
||||||
<div v-if="faq.features" class="faq-features">
|
|
||||||
<h4>{{ t('faq.keyPoints') }}</h4>
|
|
||||||
<ul>
|
|
||||||
<li v-for="feature in faq.features" :key="feature">{{ feature }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useI18n } from '@/composables/useI18n'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface FAQ {
|
|
||||||
question: string
|
|
||||||
answer: string
|
|
||||||
features?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
faqs: FAQ[]
|
|
||||||
ctaTitle: string
|
|
||||||
ctaSubtitle: string
|
|
||||||
ctaText: string
|
|
||||||
ctaLink: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
|
|
||||||
const activeIndex = ref<number | null>(null)
|
|
||||||
|
|
||||||
const toggleFAQ = (index: number) => {
|
|
||||||
activeIndex.value = activeIndex.value === index ? null : index
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/ServiceFAQ.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { type Technology } from '@/types'
|
|
||||||
import { useAssets } from '@/composables/useAssets'
|
|
||||||
import { techStack } from '@/data/techstack'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tech: Technology | string
|
|
||||||
showLevel?: boolean
|
|
||||||
showImage?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
showLevel: true,
|
|
||||||
showImage: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const { getImageUrl } = useAssets()
|
|
||||||
|
|
||||||
// Get the technology data (handle both string and object)
|
|
||||||
const techData = computed((): Technology => {
|
|
||||||
if (typeof props.tech === 'string') {
|
|
||||||
const techName = props.tech as string
|
|
||||||
|
|
||||||
// Create a mapping for technologies that don't match exactly
|
|
||||||
const techMapping: Record<string, string> = {
|
|
||||||
'Three.js': 'JavaScript',
|
|
||||||
'WebGL': 'JavaScript',
|
|
||||||
'Discord.js': 'JavaScript',
|
|
||||||
'Express': 'Node.js',
|
|
||||||
'Canvas': 'JavaScript',
|
|
||||||
'Insta.js': 'JavaScript',
|
|
||||||
'Instagram API': 'JavaScript',
|
|
||||||
'Crowdin API': 'JavaScript',
|
|
||||||
'Cron': 'Node.js'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the exact match first
|
|
||||||
let foundTech = Object.values(techStack)
|
|
||||||
.flat()
|
|
||||||
.find(t => t.name.toLowerCase() === techName.toLowerCase())
|
|
||||||
|
|
||||||
// If not found, try the mapping
|
|
||||||
if (!foundTech && techMapping[techName]) {
|
|
||||||
const mappedName = techMapping[techName]
|
|
||||||
foundTech = Object.values(techStack)
|
|
||||||
.flat()
|
|
||||||
.find(t => t.name.toLowerCase() === mappedName.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundTech) {
|
|
||||||
return foundTech
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: create a basic tech object from string
|
|
||||||
return {
|
|
||||||
name: techName,
|
|
||||||
image: '', // No image for unknown techs
|
|
||||||
level: 'Intermediate' as const
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.tech as Technology
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get the actual image URL
|
|
||||||
const imageUrl = computed(() => {
|
|
||||||
if (!techData.value.image) return ''
|
|
||||||
return getImageUrl(techData.value.image)
|
|
||||||
})
|
|
||||||
|
|
||||||
const getLevelColor = (level: Technology['level']) => {
|
|
||||||
switch (level) {
|
|
||||||
case 'Advanced':
|
|
||||||
return 'badge-success'
|
|
||||||
case 'Intermediate':
|
|
||||||
return 'badge-primary'
|
|
||||||
case 'Beginner':
|
|
||||||
return 'badge-secondary'
|
|
||||||
default:
|
|
||||||
return 'badge-secondary'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="tech-badge" itemscope itemtype="https://schema.org/ComputerLanguage">
|
|
||||||
<!-- Tech image -->
|
|
||||||
<img v-if="showImage && imageUrl" :src="imageUrl" :alt="`${techData.name} programming language logo`"
|
|
||||||
class="tech-image" loading="lazy" width="24" height="24" itemprop="image">
|
|
||||||
|
|
||||||
<!-- Tech name -->
|
|
||||||
<span class="tech-name" itemprop="name">{{ techData.name }}</span>
|
|
||||||
|
|
||||||
<!-- Level indicator -->
|
|
||||||
<span v-if="showLevel" :class="['badge', getLevelColor(techData.level)]" class="tech-level"
|
|
||||||
:aria-label="`Skill level: ${techData.level}`">
|
|
||||||
{{ techData.level }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/TechBadge.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="testimonials-section">
|
|
||||||
<div class="container">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="testimonials-header">
|
|
||||||
<h2 class="section-title">{{ title }}</h2>
|
|
||||||
<p class="section-subtitle">{{ subtitle }}</p>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<TestimonialsStats :stats="stats" :labels="statsLabels" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Testimonials Grid -->
|
|
||||||
<div class="testimonials-grid">
|
|
||||||
<TestimonialCard v-for="(testimonial, index) in testimonials" :key="index" :testimonial="testimonial"
|
|
||||||
:class="{ 'featured': testimonial.featured }" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<TestimonialsCTA :title="ctaTitle" :subtitle="ctaSubtitle" :text="ctaText" :link="ctaLink"
|
|
||||||
:reviews-link="reviewsLink" :reviews-text="reviewsText" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import TestimonialsStats from '@/components/testimonials/TestimonialsStats.vue'
|
|
||||||
import TestimonialCard from '@/components/testimonials/TestimonialCard.vue'
|
|
||||||
import TestimonialsCTA from '@/components/testimonials/TestimonialsCTA.vue'
|
|
||||||
|
|
||||||
interface Testimonial {
|
|
||||||
name: string
|
|
||||||
role: string
|
|
||||||
company: string
|
|
||||||
avatar: string
|
|
||||||
rating: number
|
|
||||||
content: string
|
|
||||||
date: string
|
|
||||||
platform: string
|
|
||||||
featured?: boolean
|
|
||||||
project_type: string
|
|
||||||
results?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Stats {
|
|
||||||
totalReviews: number
|
|
||||||
averageRating: number
|
|
||||||
projectsCompleted: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatsLabels {
|
|
||||||
clients: string
|
|
||||||
rating: string
|
|
||||||
projects: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
subtitle: string
|
|
||||||
testimonials: Testimonial[]
|
|
||||||
stats: Stats
|
|
||||||
statsLabels: StatsLabels
|
|
||||||
ctaTitle: string
|
|
||||||
ctaSubtitle: string
|
|
||||||
ctaText: string
|
|
||||||
ctaLink: string
|
|
||||||
reviewsLink: string
|
|
||||||
reviewsText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import '@/components/styles/TestimonialsSection.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useTheme } from '@/composables/useTheme'
|
|
||||||
|
|
||||||
const { isDark, toggleTheme } = useTheme()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button @click="toggleTheme" class="theme-toggle"
|
|
||||||
:aria-label="isDark ? 'Activer le mode clair' : 'Activer le mode sombre'"
|
|
||||||
:title="isDark ? 'Activer le mode clair' : 'Activer le mode sombre'">
|
|
||||||
<!-- Sun icon for light mode -->
|
|
||||||
<svg v-if="isDark" class="theme-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Moon icon for dark mode -->
|
|
||||||
<svg v-else class="theme-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@import './styles/ThemeToggle.css';
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||