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,33 +1,66 @@
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { projects } from '~/config/projects';
import { DEFAULT_IMAGES } from '~/config/paths';
import { ref, onMounted, onUnmounted } from 'vue';
import { handleImageError, handleIconError, handleLightboxImageError } from '~/assets/ts/error/handle';
import { TEXTS } from '~/config/content';
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 route = useRoute();
const error = ref<string | null>(null);
const lightboxOpen = ref(false);
const showLightbox = ref(false);
const currentImageIndex = ref(0);
const processedImages = ref<string[]>([]);
const currentProject = ref<Project | null>(null);
const currentProject = computed(() => {
const projectId = route.params.id as string;
return projects.find(p => String(p.id) === projectId) || null;
});
currentProject.value = projects.find((project) => project.id === id) || null;
const openLightbox = (index: number): void => {
currentImageIndex.value = index;
showLightbox.value = true;
};
const closeLightbox = (): void => {
showLightbox.value = false;
};
const projectImage = computed(() => {
return currentProject.value?.image || DEFAULT_IMAGES.PROJECT;
const nextImage = (): void => {
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(() => {
return currentProject.value?.gallery || [];
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
});
const getIconPath = (iconType: keyof typeof PATHS.ICONS) => {
return $theme.isDark.value
? PATHS.ICONS[iconType]?.DARK
return $theme.isDark.value
? PATHS.ICONS[iconType]?.DARK
: PATHS.ICONS[iconType]?.LIGHT;
};
@ -50,51 +83,17 @@ export const useProjectDetail = () => {
} : 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 {
currentProject,
projectImage,
projectGallery,
projectResources,
isLoading: ref(false),
error,
handleImageError,
handleIconError,
lightboxOpen,
showLightbox,
currentImageIndex,
openLightbox,
closeLightbox,
nextImage,
prevImage
prevImage,
handleImageError,
handleIconError,
handleLightboxImageError
};
};

@ -1,4 +1,4 @@
import { ref, onMounted } from 'vue';
import { ref } from 'vue';
const THEME_KEY = 'theme';
const THEMES = {
@ -6,11 +6,9 @@ const THEMES = {
DARK: 'dark'
} as const;
// Créer un état partagé pour le thème
const isDark = ref(false);
let initialized = false;
// Fonction pour définir le thème
const setTheme = (theme: typeof THEMES[keyof typeof THEMES]) => {
if (process.client) {
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 = () => {
if (process.client && !initialized) {
const savedTheme = localStorage.getItem(THEME_KEY) as typeof THEMES[keyof typeof THEMES] | null;
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
const theme = savedTheme || (prefersDark ? THEMES.DARK : THEMES.LIGHT);
setTheme(theme);
// Configurer un écouteur pour les changements de préférence
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)) {
setTheme(e.matches ? THEMES.DARK : THEMES.LIGHT);
}
});
initialized = true;
}
};
// Fonction pour basculer le thème
const toggleTheme = () => {
setTheme(isDark.value ? THEMES.LIGHT : THEMES.DARK);
};
export const useTheme = () => {
// Initialiser le thème lors du premier appel côté client
if (process.client) {
initializeTheme();
}

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

@ -14,6 +14,10 @@ export default defineNuxtConfig({
'~/assets/css/pages/cv.css'
],
imports: {
dirs: ['composables']
},
app: {
head: {
title: 'Portfolio',
@ -23,12 +27,12 @@ export default defineNuxtConfig({
{ name: 'description', content: 'Portfolio professionnel' }
],
link: [
{
rel: 'icon',
type: 'image/x-icon',
href: process.env.NODE_ENV === 'production'
? (process.env.BASE_PATH || '/containers/matheothierry-portfolio_nuxt') + '/favicon.ico'
: '/favicon.ico'
{
rel: 'icon',
type: 'image/x-icon',
href: process.env.NODE_ENV === 'production'
? (process.env.BASE_PATH || '/containers/matheothierry-portfolio_nuxt') + '/favicon.ico'
: '/favicon.ico'
}
],
},
@ -69,4 +73,4 @@ export default defineNuxtConfig({
basePath: process.env.BASE_PATH || (process.env.NODE_ENV === 'production' ? '/containers/matheothierry-portfolio_nuxt' : '')
}
}
})
})

@ -33,19 +33,19 @@
<h2>{{ TEXTS.PROJECTS.DESCRIPTION.RESOURCES }}</h2>
<div class="resources-grid">
<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>
</a>
<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>
</a>
<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>
</NuxtLink>
<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>
</a>
</div>
@ -68,7 +68,7 @@
</div>
<!-- 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-prev" @click="prevImage">&lt;</button>
<img v-if="projectGallery[currentImageIndex]" :src="projectGallery[currentImageIndex]" :alt="`${currentProject?.title} gallery image ${currentImageIndex + 1}`" class="lightbox-image" />
@ -78,26 +78,33 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useProjectDetail } from '~/composables/useProjectDetail';
import { TEXTS } from '~/config/content';
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 projectImage = computed(() => {return currentProject.value?.image || DEFAULT_IMAGES.PROJECT;});
const projectGallery = computed(() => {return currentProject.value?.gallery || [];});
const projectResources = computed(() => {return currentProject.value?.resources || [];});
const {
currentProject,
projectImage,
projectGallery,
projectResources,
handleImageError,
handleIconError,
lightboxOpen,
showLightbox,
currentImageIndex,
openLightbox,
closeLightbox,
nextImage,
prevImage
} = useProjectDetail();
} = useProjectDetail(projectId);
</script>
<style scoped>

@ -16,14 +16,4 @@ export interface Project {
status: 'completed' | 'in-progress' | 'planned';
resources: Resource[];
gallery?: readonly string[];
}
export interface ProjectResource {
url: string;
icon: string;
}
export interface ProcessedResources {
github?: ProjectResource;
demo?: ProjectResource;
}
Loading…
Cancel
Save