41683
continuous-integration/drone/push Build is passing Details

master
37b7 3 weeks ago
parent 8ba18f35b9
commit 20498d1ad5

@ -0,0 +1,16 @@
import { DEFAULT_IMAGES } from "~/config/paths";
export const handleLightboxImageError = (event: Event): void => {
const target = event.target as HTMLImageElement;
target.src = DEFAULT_IMAGES.PROJECT;
};
export const handleImageError = (event: Event): void => {
const target = event.target as HTMLImageElement;
target.src = DEFAULT_IMAGES.PROJECT;
};
export const handleIconError = (event: Event): void => {
const target = event.target as HTMLImageElement;
target.src = DEFAULT_IMAGES.ICON;
};

@ -1,28 +1,61 @@
import { ref, computed } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router'; import { handleImageError, handleIconError, handleLightboxImageError } from '~/assets/ts/error/handle';
import { projects } from '~/config/projects';
import { DEFAULT_IMAGES } from '~/config/paths';
import { TEXTS } from '~/config/content'; import { TEXTS } from '~/config/content';
import { PATHS } from '~/config/paths'; import { PATHS } from '~/config/paths';
import { projects } from '~/config/projects';
import type { Project } from '~/types/project';
export const useProjectDetail = () => { export const useProjectDetail = (id: number) => {
const { $theme } = useNuxtApp(); const { $theme } = useNuxtApp();
const route = useRoute(); const showLightbox = ref(false);
const error = ref<string | null>(null);
const lightboxOpen = ref(false);
const currentImageIndex = ref(0); const currentImageIndex = ref(0);
const processedImages = ref<string[]>([]);
const currentProject = ref<Project | null>(null);
const currentProject = computed(() => { currentProject.value = projects.find((project) => project.id === id) || null;
const projectId = route.params.id as string;
return projects.find(p => String(p.id) === projectId) || null; const openLightbox = (index: number): void => {
}); currentImageIndex.value = index;
showLightbox.value = true;
};
const closeLightbox = (): void => {
showLightbox.value = false;
};
const projectImage = computed(() => { const nextImage = (): void => {
return currentProject.value?.image || DEFAULT_IMAGES.PROJECT; if (!processedImages.value.length) return;
currentImageIndex.value = (currentImageIndex.value + 1) % processedImages.value.length;
};
const prevImage = (): void => {
if (!processedImages.value.length) return;
currentImageIndex.value = (currentImageIndex.value - 1 + processedImages.value.length) % processedImages.value.length;
};
const handleKeyDown = (event: KeyboardEvent): void => {
if (!showLightbox.value) return;
switch (event.key) {
case 'Escape':
closeLightbox();
break;
case 'ArrowLeft':
prevImage();
break;
case 'ArrowRight':
nextImage();
break;
}
};
onMounted(async () => {
window.addEventListener('keydown', handleKeyDown);
}); });
const projectGallery = computed(() => { onUnmounted(() => {
return currentProject.value?.gallery || []; window.removeEventListener('keydown', handleKeyDown);
}); });
const getIconPath = (iconType: keyof typeof PATHS.ICONS) => { const getIconPath = (iconType: keyof typeof PATHS.ICONS) => {
@ -50,51 +83,17 @@ export const useProjectDetail = () => {
} : null } : null
})); }));
const handleImageError = (event: Event) => {
const target = event.target as HTMLImageElement;
target.src = DEFAULT_IMAGES.PROJECT;
error.value = TEXTS.COMMON.ERROR.IMAGE_LOAD;
};
const handleIconError = (event: Event) => {
const target = event.target as HTMLImageElement;
target.src = DEFAULT_IMAGES.ICON;
error.value = TEXTS.COMMON.ERROR.ICON_LOAD;
};
const openLightbox = (index: number) => {
currentImageIndex.value = index;
lightboxOpen.value = true;
};
const closeLightbox = () => {
lightboxOpen.value = false;
};
const nextImage = () => {
if (projectGallery.value.length === 0) return;
currentImageIndex.value = (currentImageIndex.value + 1) % projectGallery.value.length;
};
const prevImage = () => {
if (projectGallery.value.length === 0) return;
currentImageIndex.value = (currentImageIndex.value - 1 + projectGallery.value.length) % projectGallery.value.length;
};
return { return {
currentProject, currentProject,
projectImage,
projectGallery,
projectResources, projectResources,
isLoading: ref(false), showLightbox,
error,
handleImageError,
handleIconError,
lightboxOpen,
currentImageIndex, currentImageIndex,
openLightbox, openLightbox,
closeLightbox, closeLightbox,
nextImage, nextImage,
prevImage prevImage,
handleImageError,
handleIconError,
handleLightboxImageError
}; };
}; };

@ -1,4 +1,4 @@
import { ref, onMounted } from 'vue'; import { ref } from 'vue';
const THEME_KEY = 'theme'; const THEME_KEY = 'theme';
const THEMES = { const THEMES = {
@ -6,11 +6,9 @@ const THEMES = {
DARK: 'dark' DARK: 'dark'
} as const; } as const;
// Créer un état partagé pour le thème
const isDark = ref(false); const isDark = ref(false);
let initialized = false; let initialized = false;
// Fonction pour définir le thème
const setTheme = (theme: typeof THEMES[keyof typeof THEMES]) => { const setTheme = (theme: typeof THEMES[keyof typeof THEMES]) => {
if (process.client) { if (process.client) {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
@ -19,34 +17,26 @@ const setTheme = (theme: typeof THEMES[keyof typeof THEMES]) => {
} }
}; };
// Initialiser le thème uniquement côté client et une seule fois
const initializeTheme = () => { const initializeTheme = () => {
if (process.client && !initialized) { if (process.client && !initialized) {
const savedTheme = localStorage.getItem(THEME_KEY) as typeof THEMES[keyof typeof THEMES] | null; const savedTheme = localStorage.getItem(THEME_KEY) as typeof THEMES[keyof typeof THEMES] | null;
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
const theme = savedTheme || (prefersDark ? THEMES.DARK : THEMES.LIGHT); const theme = savedTheme || (prefersDark ? THEMES.DARK : THEMES.LIGHT);
setTheme(theme); setTheme(theme);
// Configurer un écouteur pour les changements de préférence
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener('change', (e) => { window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// Ne mettre à jour automatiquement que si l'utilisateur n'a pas défini de préférence
if (!localStorage.getItem(THEME_KEY)) { if (!localStorage.getItem(THEME_KEY)) {
setTheme(e.matches ? THEMES.DARK : THEMES.LIGHT); setTheme(e.matches ? THEMES.DARK : THEMES.LIGHT);
} }
}); });
initialized = true; initialized = true;
} }
}; };
// Fonction pour basculer le thème
const toggleTheme = () => { const toggleTheme = () => {
setTheme(isDark.value ? THEMES.LIGHT : THEMES.DARK); setTheme(isDark.value ? THEMES.LIGHT : THEMES.DARK);
}; };
export const useTheme = () => { export const useTheme = () => {
// Initialiser le thème lors du premier appel côté client
if (process.client) { if (process.client) {
initializeTheme(); initializeTheme();
} }

@ -49,15 +49,15 @@ export const PATHS = {
GITHUB: { GITHUB: {
LIGHT: withBasePath('/assets/icons/github-black.png'), LIGHT: withBasePath('/assets/icons/github-black.png'),
DARK: withBasePath('/assets/icons/github-white.png') DARK: withBasePath('/assets/icons/github-white.png')
}, } as Icons,
DEMO: { DEMO: {
LIGHT: withBasePath('/assets/icons/demo-black.png'), LIGHT: withBasePath('/assets/icons/demo-black.png'),
DARK: withBasePath('/assets/icons/demo-white.png') DARK: withBasePath('/assets/icons/demo-white.png')
}, } as Icons,
DOCUMENTATION: { DOCUMENTATION: {
LIGHT: withBasePath('/assets/icons/documentation-black.png'), LIGHT: withBasePath('/assets/icons/documentation-black.png'),
DARK: withBasePath('/assets/icons/documentation-white.png') DARK: withBasePath('/assets/icons/documentation-white.png')
}, } as Icons,
YOUTUBE: { YOUTUBE: {
LIGHT: withBasePath('/assets/icons/youtube-black.png'), LIGHT: withBasePath('/assets/icons/youtube-black.png'),
DARK: withBasePath('/assets/icons/youtube-white.png') DARK: withBasePath('/assets/icons/youtube-white.png')
@ -76,7 +76,6 @@ export const PATHS = {
PORTFOLIO: { PORTFOLIO: {
GITHUB: 'https://github.com/37b7/portfolio_nuxt', GITHUB: 'https://github.com/37b7/portfolio_nuxt',
DEMO: 'https://votre-portfolio.com', DEMO: 'https://votre-portfolio.com',
DOCUMENTATION: 'https://votre-portfolio.com/docs'
}, },
IRIS: { IRIS: {
GITHUB: 'https://github.com/votre-username/todo-app', GITHUB: 'https://github.com/votre-username/todo-app',

@ -14,6 +14,10 @@ export default defineNuxtConfig({
'~/assets/css/pages/cv.css' '~/assets/css/pages/cv.css'
], ],
imports: {
dirs: ['composables']
},
app: { app: {
head: { head: {
title: 'Portfolio', title: 'Portfolio',

@ -33,19 +33,19 @@
<h2>{{ TEXTS.PROJECTS.DESCRIPTION.RESOURCES }}</h2> <h2>{{ TEXTS.PROJECTS.DESCRIPTION.RESOURCES }}</h2>
<div class="resources-grid"> <div class="resources-grid">
<a v-if="currentProject.github" :href="currentProject.github" target="_blank" rel="noopener noreferrer" class="resource-link"> <a v-if="currentProject.github" :href="currentProject.github" target="_blank" rel="noopener noreferrer" class="resource-link">
<img v-if="projectResources.github?.icon" :src="projectResources.github?.icon" :alt="TEXTS.PROJECTS.RESOURCES.GITHUB" class="resource-icon" @error="handleIconError" loading="lazy" /> <img v-if="PATHS.ICONS.GITHUB" :src="PATHS.ICONS.GITHUB" :alt="TEXTS.PROJECTS.RESOURCES.GITHUB" class="resource-icon" @error="handleIconError" loading="lazy" />
<span>{{ TEXTS.PROJECTS.RESOURCES.GITHUB }}</span> <span>{{ TEXTS.PROJECTS.RESOURCES.GITHUB }}</span>
</a> </a>
<a v-if="currentProject.demo" :href="currentProject.demo" target="_blank" rel="noopener noreferrer" class="resource-link" > <a v-if="currentProject.demo" :href="currentProject.demo" target="_blank" rel="noopener noreferrer" class="resource-link" >
<img v-if="projectResources.demo?.icon" :src="projectResources.demo?.icon" :alt="TEXTS.PROJECTS.RESOURCES.DEMO" class="resource-icon" @error="handleIconError" loading="lazy" /> <img v-if="PATHS.ICONS.DEMO" :src="PATHS.ICONS.DEMO" :alt="TEXTS.PROJECTS.RESOURCES.DEMO" class="resource-icon" @error="handleIconError" loading="lazy" />
<span>{{ TEXTS.PROJECTS.RESOURCES.DEMO }}</span> <span>{{ TEXTS.PROJECTS.RESOURCES.DEMO }}</span>
</a> </a>
<NuxtLink :to="`${projectsPath}/${currentProject.id}/docs`" class="resource-link"> <NuxtLink :to="`${projectsPath}/${currentProject.id}/docs`" class="resource-link">
<img v-if="projectResources.documentation?.icon" :src="projectResources.documentation?.icon" :alt="TEXTS.PROJECTS.RESOURCES.DOCUMENTATION" class="resource-icon" @error="handleIconError" loading="lazy" /> <img v-if="PATHS.ICONS.DOCUMENTATION" :src="PATHS.ICONS.DOCUMENTATION" :alt="TEXTS.PROJECTS.RESOURCES.DOCUMENTATION" class="resource-icon" @error="handleIconError" loading="lazy" />
<span>{{ TEXTS.PROJECTS.RESOURCES.DOCUMENTATION }}</span> <span>{{ TEXTS.PROJECTS.RESOURCES.DOCUMENTATION }}</span>
</NuxtLink> </NuxtLink>
<a v-if="currentProject.youtube" :href="currentProject.youtube" target="_blank" rel="noopener noreferrer" class="resource-link" > <a v-if="currentProject.youtube" :href="currentProject.youtube" target="_blank" rel="noopener noreferrer" class="resource-link" >
<img v-if="projectResources.youtube?.icon" :src="projectResources.youtube?.icon" :alt="TEXTS.PROJECTS.RESOURCES.YOUTUBE" class="resource-icon" @error="handleIconError" loading="lazy" /> <img v-if="PATHS.ICONS.YOUTUBE" :src="PATHS.ICONS.YOUTUBE" :alt="TEXTS.PROJECTS.RESOURCES.YOUTUBE" class="resource-icon" @error="handleIconError" loading="lazy" />
<span>{{ TEXTS.PROJECTS.RESOURCES.YOUTUBE }}</span> <span>{{ TEXTS.PROJECTS.RESOURCES.YOUTUBE }}</span>
</a> </a>
</div> </div>
@ -68,7 +68,7 @@
</div> </div>
<!-- Lightbox --> <!-- Lightbox -->
<div v-if="lightboxOpen" class="lightbox" @click.self="closeLightbox"> <div v-if="showLightbox" class="lightbox" @click.self="closeLightbox">
<button class="lightbox-close" @click="closeLightbox">&times;</button> <button class="lightbox-close" @click="closeLightbox">&times;</button>
<button class="lightbox-prev" @click="prevImage">&lt;</button> <button class="lightbox-prev" @click="prevImage">&lt;</button>
<img v-if="projectGallery[currentImageIndex]" :src="projectGallery[currentImageIndex]" :alt="`${currentProject?.title} gallery image ${currentImageIndex + 1}`" class="lightbox-image" /> <img v-if="projectGallery[currentImageIndex]" :src="projectGallery[currentImageIndex]" :alt="`${currentProject?.title} gallery image ${currentImageIndex + 1}`" class="lightbox-image" />
@ -78,26 +78,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue';
import { useProjectDetail } from '~/composables/useProjectDetail'; import { useProjectDetail } from '~/composables/useProjectDetail';
import { TEXTS } from '~/config/content'; import { TEXTS } from '~/config/content';
import { navigationItems } from '~/config/navigation'; import { navigationItems } from '~/config/navigation';
import { useRoute } from 'nuxt/app';
import { projects } from '~/config/projects';
import { DEFAULT_IMAGES, PATHS } from '~/config/paths';
const route = useRoute();
const projectId = Number(route.params.id);
const currentProject = ref(projects.find(p => p.id === projectId) || null);
const projectsPath = navigationItems.find((item) => item.finder === 'projects')?.path || '/projects'; const projectsPath = navigationItems.find((item) => item.finder === 'projects')?.path || '/projects';
const projectImage = computed(() => {return currentProject.value?.image || DEFAULT_IMAGES.PROJECT;});
const projectGallery = computed(() => {return currentProject.value?.gallery || [];});
const projectResources = computed(() => {return currentProject.value?.resources || [];});
const { const {
currentProject,
projectImage,
projectGallery,
projectResources,
handleImageError, handleImageError,
handleIconError, handleIconError,
lightboxOpen, showLightbox,
currentImageIndex, currentImageIndex,
openLightbox, openLightbox,
closeLightbox, closeLightbox,
nextImage, nextImage,
prevImage prevImage
} = useProjectDetail(); } = useProjectDetail(projectId);
</script> </script>
<style scoped> <style scoped>

@ -17,13 +17,3 @@ export interface Project {
resources: Resource[]; resources: Resource[];
gallery?: readonly string[]; gallery?: readonly string[];
} }
export interface ProjectResource {
url: string;
icon: string;
}
export interface ProcessedResources {
github?: ProjectResource;
demo?: ProjectResource;
}
Loading…
Cancel
Save