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
This commit is contained in:
Mr¤KayJayDee
2025-06-22 15:00:35 +02:00
commit cc7368b550
122 changed files with 11938 additions and 0 deletions
+49
View 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
View 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
View 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
View 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
}
}
+68
View File
@@ -0,0 +1,68 @@
import { ref, watch, onMounted } from 'vue'
export type Theme = 'light' | 'dark'
const isDark = ref<boolean>(false)
export function useTheme() {
const toggleTheme = () => {
isDark.value = !isDark.value
}
const setTheme = (theme: Theme) => {
isDark.value = theme === 'dark'
}
const getTheme = (): Theme => {
return isDark.value ? 'dark' : 'light'
}
// Apply theme to document
const applyTheme = () => {
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', isDark.value)
document.documentElement.setAttribute('data-theme', getTheme())
}
}
// Save theme to localStorage
const saveTheme = () => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', getTheme())
}
}
// Load theme from localStorage or system preference
const loadTheme = () => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme) {
setTheme(savedTheme)
} else {
// Use system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setTheme(prefersDark ? 'dark' : 'light')
}
}
}
// Watch for theme changes
watch(isDark, () => {
applyTheme()
saveTheme()
})
// Initialize theme on mount
onMounted(() => {
loadTheme()
applyTheme()
})
return {
isDark,
toggleTheme,
setTheme,
getTheme
}
}