chore(initial): ajout de la structure de base du projet avec Vite et Vue 3
- Création des fichiers de configuration pour ESLint, Prettier, et Tailwind CSS - Ajout de la configuration de l'éditeur avec .editorconfig - Mise en place de la structure de répertoires pour les composants, les pages, et les données - Intégration de la gestion des langues avec vue-i18n - Ajout de la configuration de Vite et des dépendances nécessaires - Création des fichiers de localisation pour l'anglais et le français - Ajout de la structure de base pour le portfolio avec des exemples de projets - Mise en place des composants de base pour l'interface utilisateur
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
6
.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# portfolio
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
22
eslint.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
)
|
24
index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description"
|
||||||
|
content="Portfolio de Killian, développeur Full Stack spécialisé dans les technologies web modernes">
|
||||||
|
<meta name="author" content="Killian">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
<title>Killian - Développeur Full Stack</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
70
old/projects.info
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
279
old/techstack.json
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6098
package-lock.json
generated
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/head": "^2.0.0",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^9.14.4",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"@tsconfig/node22": "^22.0.1",
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.22.0",
|
||||||
|
"eslint-plugin-vue": "~10.0.0",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"npm-run-all2": "^7.0.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"tailwindcss": "^4.1.10",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"vite": "^6.2.4",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2",
|
||||||
|
"vue-tsc": "^2.2.8"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 45 KiB |
40
public/images/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Images du Portfolio
|
||||||
|
|
||||||
|
Ce dossier doit contenir les images pour votre portfolio :
|
||||||
|
|
||||||
|
## Structure des dossiers
|
||||||
|
|
||||||
|
```
|
||||||
|
images/
|
||||||
|
├── virtualtour.png # Image du projet Virtual Tour
|
||||||
|
├── xinko.png # Image du projet Xinko
|
||||||
|
├── dig.png # Image du projet Image Manipulation
|
||||||
|
├── primate.png # Image du projet Primate Web Admin
|
||||||
|
├── instagram.png # Image du projet Instagram Bot
|
||||||
|
├── crowdin.png # Image du projet Crowdin Status Bot
|
||||||
|
└── tech/ # Dossier pour les logos des technologies
|
||||||
|
├── javascript.png
|
||||||
|
├── typescript.png
|
||||||
|
├── nodejs.png
|
||||||
|
├── vuejs.png
|
||||||
|
├── react.png
|
||||||
|
├── angular.png
|
||||||
|
└── ... # Autres logos de technologies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommandations
|
||||||
|
|
||||||
|
- **Format** : PNG ou JPG
|
||||||
|
- **Dimensions recommandées pour les projets** : 1200x630px (ratio 1.9:1)
|
||||||
|
- **Dimensions recommandées pour les logos tech** : 100x100px
|
||||||
|
- **Taille maximale** : 500KB par image
|
||||||
|
|
||||||
|
## Images temporaires
|
||||||
|
|
||||||
|
En attendant vos vraies images, vous pouvez utiliser des services comme :
|
||||||
|
|
||||||
|
- https://placeholder.com/
|
||||||
|
- https://placehold.co/
|
||||||
|
- https://via.placeholder.com/
|
||||||
|
|
||||||
|
Exemple : `https://placehold.co/1200x630/2563eb/ffffff?text=Virtual+Tour`
|
53
src/App.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<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>
|
BIN
src/assets/images/Rectangle 55.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/images/android.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/images/angular.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/assets/images/archlinux.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/images/atom.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/bash.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src/assets/images/crowdin.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/css.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/images/debian.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/assets/images/deepin.png
Normal file
After Width: | Height: | Size: 608 B |
BIN
src/assets/images/dig.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
src/assets/images/discord.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/images/discordbot.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/images/discordd.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/docker.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/assets/images/facebook.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/figma.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/images/filezilla.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/images/git.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/images/github.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
src/assets/images/gitkraken.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/gitlab.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
src/assets/images/heidisql.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
src/assets/images/html.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/images/instagram.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/instagramd.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/ios.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/images/javascript.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/images/kalilinux.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/linkedin.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/linkedind.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/images/linux.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
src/assets/images/macos.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/images/mail.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/images/markdown.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/messenger.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/mongodb.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/images/mysql.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/mysqlworkbench.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/nginx.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/images/nodejs.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/images/npm.png
Normal file
After Width: | Height: | Size: 957 B |
BIN
src/assets/images/postman.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/images/primate.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
src/assets/images/react.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/reddit.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/images/redis.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/assets/images/sequelpro.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src/assets/images/sqlite.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/images/telegram.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
src/assets/images/termius.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/twitter.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
src/assets/images/twitterd.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/typescript.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/ubuntu.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src/assets/images/virtualtour.png
Normal file
After Width: | Height: | Size: 528 KiB |
BIN
src/assets/images/vscode.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/images/vuejs.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/watchos.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/wearos.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/whatsapp.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/images/windows.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/images/wordpress.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/images/xinko.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
1
src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 276 B |
848
src/assets/main.css
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
@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 */
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
88
src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<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>
|
||||||
|
.language-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switcher-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: var(--space-xs);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-code {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.lang-code {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-btn {
|
||||||
|
padding: var(--space-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
258
src/components/ProjectCard.vue
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<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">
|
||||||
|
<!-- Image -->
|
||||||
|
<div class="relative overflow-hidden" style="aspect-ratio: 16/9;">
|
||||||
|
<img :src="imageUrl" :alt="project.title"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110" loading="lazy">
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
<div class="absolute bottom-4 left-4 right-4">
|
||||||
|
<div v-if="project.buttons && project.buttons.length > 0" class="flex gap-2">
|
||||||
|
<a v-for="button in project.buttons" :key="button.title" :href="button.link" target="_blank"
|
||||||
|
rel="noopener noreferrer" class="btn btn-primary btn-sm" @click.stop>
|
||||||
|
{{ t(`projects.buttons.${button.title.toLowerCase().replace(/\s+/g, '')}`, button.title) }}
|
||||||
|
<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">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Category & Date -->
|
||||||
|
<div class="flex items-center justify-between mb-md">
|
||||||
|
<span v-if="project.category" class="badge badge-primary">
|
||||||
|
{{ translatedCategory }}
|
||||||
|
</span>
|
||||||
|
<span v-if="project.date" class="text-sm text-secondary">
|
||||||
|
{{ project.date }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h3 class="text-xl font-bold mb-md group-hover:text-primary transition-colors">
|
||||||
|
{{ translatedTitle }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-secondary mb-lg line-clamp-3">
|
||||||
|
{{ translatedDescription }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Technologies -->
|
||||||
|
<div v-if="project.technologies && project.technologies.length > 0" class="flex flex-wrap gap-2 mb-lg">
|
||||||
|
<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="flex items-center justify-between">
|
||||||
|
<RouterLink :to="`/project/${project.id}`" class="btn btn-secondary btn-sm">
|
||||||
|
{{ t('projects.buttons.viewProject') }}
|
||||||
|
<svg class="btn-icon" 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>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Line clamp utility */
|
||||||
|
.line-clamp-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom utilities */
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-cover {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inset-0 {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-4 {
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-4 {
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-4 {
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xl {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\:text-primary {
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .group-hover\:text-primary {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-colors {
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-transform {
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-opacity {
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-300 {
|
||||||
|
transition-duration: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\:scale-110 {
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .group-hover\:scale-110 {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-hover\:opacity-100 {
|
||||||
|
transition: opacity var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .group-hover\:opacity-100 {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-1 {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
133
src/components/TechBadge.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<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(() => {
|
||||||
|
if (typeof props.tech === '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() === props.tech.toLowerCase())
|
||||||
|
|
||||||
|
// If not found, try the mapping
|
||||||
|
if (!foundTech && techMapping[props.tech]) {
|
||||||
|
foundTech = Object.values(techStack)
|
||||||
|
.flat()
|
||||||
|
.find(t => t.name.toLowerCase() === techMapping[props.tech].toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundTech) {
|
||||||
|
return foundTech
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: create a basic tech object from string
|
||||||
|
return {
|
||||||
|
name: props.tech,
|
||||||
|
image: '', // No image for unknown techs
|
||||||
|
level: 'Intermediate' as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.tech
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the actual image URL
|
||||||
|
const imageUrl = computed(() => {
|
||||||
|
if (!techData.value.image) return ''
|
||||||
|
return getImageUrl(techData.value.image)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getLevelColor = (level: string) => {
|
||||||
|
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">
|
||||||
|
<!-- Tech image -->
|
||||||
|
<img v-if="showImage && imageUrl" :src="imageUrl" :alt="techData.name" class="tech-image" loading="lazy">
|
||||||
|
|
||||||
|
<!-- Tech name -->
|
||||||
|
<span class="tech-name">{{ techData.name }}</span>
|
||||||
|
|
||||||
|
<!-- Level indicator -->
|
||||||
|
<span v-if="showLevel" :class="['badge', getLevelColor(techData.level)]" class="tech-level">
|
||||||
|
{{ techData.level }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tech-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-badge:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-image {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-name {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-level {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
}
|
||||||
|
</style>
|
68
src/components/ThemeToggle.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<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>
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
:global(.dark) .theme-toggle {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .theme-toggle:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
}
|
||||||
|
</style>
|
7
src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<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>
|
7
src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<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>
|
7
src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
19
src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
212
src/components/layout/AppFooter.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useAssets } from '@/composables/useAssets'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { useSiteConfig } from '@/composables/useSiteConfig'
|
||||||
|
|
||||||
|
const { getImageUrl } = useAssets()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { siteConfig } = useSiteConfig()
|
||||||
|
|
||||||
|
const quickLinks = computed(() => [
|
||||||
|
{ name: t('nav.home'), path: '/' },
|
||||||
|
{ name: t('nav.projects'), path: '/projects' },
|
||||||
|
{ name: t('nav.about'), path: '/about' },
|
||||||
|
{ name: t('nav.contact'), path: '/contact' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const services = computed(() => [
|
||||||
|
t('footer.servicesList.webDev'),
|
||||||
|
t('footer.servicesList.mobileApps'),
|
||||||
|
t('footer.servicesList.apiBackend'),
|
||||||
|
t('footer.servicesList.consulting')
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Main Footer Content -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-xl mb-2xl">
|
||||||
|
<!-- Brand -->
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="flex items-center gap-md mb-lg">
|
||||||
|
<img :src="getImageUrl('@/assets/images/logo.png')" alt="Killian" class="footer-logo">
|
||||||
|
<span class="text-xl font-bold footer-brand">{{ siteConfig.name }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-lg max-w-md footer-description">
|
||||||
|
{{ siteConfig.description }}
|
||||||
|
</p>
|
||||||
|
<!-- Social Links -->
|
||||||
|
<div class="flex gap-md">
|
||||||
|
<a v-for="social in siteConfig.social" :key="social.name" :href="social.url" target="_blank"
|
||||||
|
rel="noopener noreferrer" class="social-link" :title="social.name">
|
||||||
|
|
||||||
|
<!-- GitHub Icon -->
|
||||||
|
<svg v-if="social.icon === 'github'" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.237 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- LinkedIn Icon -->
|
||||||
|
<svg v-else-if="social.icon === 'linkedin'" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Discord Icon -->
|
||||||
|
<svg v-else-if="social.icon === 'discord'" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419-.0190 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9460 2.4189-2.1568 2.4189Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Email Icon -->
|
||||||
|
<svg v-else-if="social.icon === 'email'" class="w-5 h-5" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="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" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold mb-lg footer-title">{{ t('footer.navigation') }}</h3>
|
||||||
|
<ul class="space-y-sm">
|
||||||
|
<li v-for="link in quickLinks" :key="link.name">
|
||||||
|
<RouterLink :to="link.path" class="footer-link">
|
||||||
|
{{ link.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold mb-lg footer-title">{{ t('footer.services') }}</h3>
|
||||||
|
<ul class="space-y-sm">
|
||||||
|
<li v-for="service in services" :key="service">
|
||||||
|
<span class="footer-link cursor-default">{{ service }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Bar -->
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-center gap-md">
|
||||||
|
<p class="text-sm footer-copyright">
|
||||||
|
© {{ new Date().getFullYear() }} {{ siteConfig.author }}. {{ t('footer.copyright') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-lg text-sm">
|
||||||
|
<a href="#" class="footer-link">{{ t('footer.legalNotices') }}</a>
|
||||||
|
<a href="#" class="footer-link">{{ t('footer.privacyPolicy') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Footer Base */
|
||||||
|
.footer {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: var(--space-4xl) 0 var(--space-xl);
|
||||||
|
border-top: var(--border-width) solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer logo */
|
||||||
|
.footer-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Brand */
|
||||||
|
.footer-brand {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Titles */
|
||||||
|
.footer-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Description */
|
||||||
|
.footer-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Copyright */
|
||||||
|
.footer-copyright {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Bottom */
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: var(--border-width) solid var(--border-color);
|
||||||
|
padding-top: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Links */
|
||||||
|
.social-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: var(--border-width) solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
scale: 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Links */
|
||||||
|
.footer-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom utilities */
|
||||||
|
.space-y-sm>*+* {
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-md {
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-default {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:col-span-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md\:flex-row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
113
src/components/layout/AppHeader.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useAssets } from '@/composables/useAssets'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const { getImageUrl } = useAssets()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const isMenuOpen = ref(false)
|
||||||
|
|
||||||
|
const navigation = computed(() => [
|
||||||
|
{ name: t('nav.home'), path: '/' },
|
||||||
|
{ name: t('nav.projects'), path: '/projects' },
|
||||||
|
{ name: t('nav.about'), path: '/about' },
|
||||||
|
{ name: t('nav.contact'), path: '/contact' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
isMenuOpen.value = !isMenuOpen.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-content">
|
||||||
|
<!-- Logo -->
|
||||||
|
<RouterLink to="/" class="logo">
|
||||||
|
<img :src="getImageUrl('@/assets/images/logo.png')" alt="Killian" class="logo-image">
|
||||||
|
<span>Killian</span>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
|
<nav class="nav hidden md:flex">
|
||||||
|
<RouterLink v-for="item in navigation" :key="item.name" :to="item.path" class="nav-link"
|
||||||
|
:class="{ 'active': $route.path === item.path }">
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Right side controls -->
|
||||||
|
<div class="header-actions">
|
||||||
|
<!-- Language switcher -->
|
||||||
|
<LanguageSwitcher />
|
||||||
|
|
||||||
|
<!-- Theme toggle -->
|
||||||
|
<ThemeToggle />
|
||||||
|
|
||||||
|
<!-- Mobile menu button -->
|
||||||
|
<button @click="toggleMenu" class="md:hidden btn btn-ghost p-2" aria-label="Toggle menu">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path v-if="!isMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Navigation -->
|
||||||
|
<div class="mobile-menu md:hidden" :class="{ 'open': isMenuOpen }">
|
||||||
|
<nav class="mobile-menu-nav">
|
||||||
|
<RouterLink v-for="item in navigation" :key="item.name" :to="item.path" class="nav-link"
|
||||||
|
@click="isMenuOpen = false">
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Header actions */
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo styling */
|
||||||
|
.logo-image {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive utilities */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md\:hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation active state */
|
||||||
|
.nav-link.router-link-active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.router-link-active::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
49
src/composables/useAssets.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Composable for handling dynamic asset imports in Vite
|
||||||
|
*/
|
||||||
|
export function useAssets() {
|
||||||
|
// Pre-load all images using Vite's import.meta.glob
|
||||||
|
const imageModules = import.meta.glob('../assets/images/*', { eager: true })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image URL from assets folder
|
||||||
|
* @param path - Path like '@/assets/images/filename.png' or 'filename.png'
|
||||||
|
* @returns string - The image URL
|
||||||
|
*/
|
||||||
|
const getImageUrl = (path: string | undefined): string => {
|
||||||
|
try {
|
||||||
|
// Handle undefined or empty path
|
||||||
|
if (!path || path.trim() === '') {
|
||||||
|
console.warn('getImageUrl called with empty or undefined path')
|
||||||
|
return `https://via.placeholder.com/400x300/f3f4f6/9ca3af?text=${encodeURIComponent('No image')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the path to get just the filename
|
||||||
|
let cleanPath = path
|
||||||
|
if (path.startsWith('@/assets/images/')) {
|
||||||
|
cleanPath = path.replace('@/assets/images/', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the full path for the module lookup
|
||||||
|
const fullPath = `../assets/images/${cleanPath}`
|
||||||
|
|
||||||
|
// Get the image module
|
||||||
|
const imageModule = imageModules[fullPath] as { default: string }
|
||||||
|
|
||||||
|
if (imageModule && imageModule.default) {
|
||||||
|
return imageModule.default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to construct URL directly
|
||||||
|
return new URL(`../assets/images/${cleanPath}`, import.meta.url).href
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load image: ${path}`, error)
|
||||||
|
// Return a placeholder image
|
||||||
|
return `https://via.placeholder.com/400x300/f3f4f6/9ca3af?text=${encodeURIComponent('Image not found')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getImageUrl
|
||||||
|
}
|
||||||
|
}
|
34
src/composables/useI18n.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useI18n as useVueI18n } from 'vue-i18n'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
const { locale, t, availableLocales } = useVueI18n()
|
||||||
|
|
||||||
|
const currentLocale = computed(() => locale.value)
|
||||||
|
|
||||||
|
const isEnglish = computed(() => locale.value === 'en')
|
||||||
|
const isFrench = computed(() => locale.value === 'fr')
|
||||||
|
|
||||||
|
const switchLocale = (newLocale: string) => {
|
||||||
|
if (availableLocales.includes(newLocale)) {
|
||||||
|
locale.value = newLocale
|
||||||
|
localStorage.setItem('locale', newLocale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleLocale = () => {
|
||||||
|
const newLocale = locale.value === 'en' ? 'fr' : 'en'
|
||||||
|
switchLocale(newLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
locale,
|
||||||
|
currentLocale,
|
||||||
|
isEnglish,
|
||||||
|
isFrench,
|
||||||
|
switchLocale,
|
||||||
|
toggleLocale,
|
||||||
|
availableLocales
|
||||||
|
}
|
||||||
|
}
|
77
src/composables/useSeo.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface SeoOptions {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
ogTitle?: string
|
||||||
|
ogDescription?: string
|
||||||
|
ogImage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSeo(options: SeoOptions = {}) {
|
||||||
|
const originalTitle = document.title
|
||||||
|
const metaElements: HTMLMetaElement[] = []
|
||||||
|
|
||||||
|
const setTitle = (title: string) => {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
const setMetaTag = (name: string, content: string, property?: boolean) => {
|
||||||
|
let meta = document.querySelector(`meta[${property ? 'property' : 'name'}="${name}"]`) as HTMLMetaElement
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
meta = document.createElement('meta')
|
||||||
|
if (property) {
|
||||||
|
meta.setAttribute('property', name)
|
||||||
|
} else {
|
||||||
|
meta.setAttribute('name', name)
|
||||||
|
}
|
||||||
|
document.head.appendChild(meta)
|
||||||
|
metaElements.push(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.setAttribute('content', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (options.title) {
|
||||||
|
setTitle(options.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.description) {
|
||||||
|
setMetaTag('description', options.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ogTitle) {
|
||||||
|
setMetaTag('og:title', options.ogTitle, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ogDescription) {
|
||||||
|
setMetaTag('og:description', options.ogDescription, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.ogImage) {
|
||||||
|
setMetaTag('og:image', options.ogImage, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default Open Graph type
|
||||||
|
setMetaTag('og:type', 'website', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Restore original title
|
||||||
|
document.title = originalTitle
|
||||||
|
|
||||||
|
// Remove meta tags we added
|
||||||
|
metaElements.forEach(meta => {
|
||||||
|
if (meta.parentNode) {
|
||||||
|
meta.parentNode.removeChild(meta)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
setTitle,
|
||||||
|
setMetaTag
|
||||||
|
}
|
||||||
|
}
|
20
src/composables/useSiteConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from '@/composables/useI18n'
|
||||||
|
import { siteConfig as baseSiteConfig } from '@/config/site'
|
||||||
|
|
||||||
|
export function useSiteConfig() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const siteConfig = computed(() => ({
|
||||||
|
...baseSiteConfig,
|
||||||
|
title: t('seo.home.title'),
|
||||||
|
description: t('seo.home.description'),
|
||||||
|
contact: {
|
||||||
|
...baseSiteConfig.contact
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
siteConfig
|
||||||
|
}
|
||||||
|
}
|