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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user