Refactor: clean up template structure and improve readability; enhance project card navigation

This commit is contained in:
2025-12-24 13:50:55 +01:00
parent bcf9bd599e
commit 00a5c34f36
2 changed files with 83 additions and 88 deletions

View File

@@ -46,7 +46,6 @@ const formattedDate = computed(() => {
v-if="project" v-if="project"
class="space-y-8" class="space-y-8"
> >
<!-- Back Button -->
<div> <div>
<UButton <UButton
to="/projects" to="/projects"
@@ -59,7 +58,6 @@ const formattedDate = computed(() => {
</UButton> </UButton>
</div> </div>
<!-- Project Header -->
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<UIcon <UIcon
@@ -119,7 +117,6 @@ const formattedDate = computed(() => {
</div> </div>
</div> </div>
<!-- Tags -->
<div <div
v-if="project.tags && project.tags.length > 0" v-if="project.tags && project.tags.length > 0"
class="flex flex-wrap gap-2 pt-2" class="flex flex-wrap gap-2 pt-2"
@@ -137,7 +134,6 @@ const formattedDate = computed(() => {
<USeparator /> <USeparator />
<!-- Project Content -->
<ContentRenderer <ContentRenderer
v-if="projectWithBody" v-if="projectWithBody"
:value="projectWithBody" :value="projectWithBody"

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
const { data: projects } = await useAsyncData('projects', () => { const { data: projects } = await useAsyncData('projects', () => {
return queryCollection('projects').where('extension', '=', 'md').order('favorite', 'DESC').order('publishedAt', 'DESC').all() return queryCollection('projects')
.where('extension', '=', 'md')
.order('favorite', 'DESC')
.order('publishedAt', 'DESC')
.all()
}) })
useSeoMeta({ useSeoMeta({
@@ -45,12 +49,8 @@ const { statusColors, typeColors } = useProjectColors()
function toggleTag(tag: string) { function toggleTag(tag: string) {
const index = selectedTags.value.indexOf(tag) const index = selectedTags.value.indexOf(tag)
if (index > -1) { if (index > -1) selectedTags.value.splice(index, 1)
selectedTags.value.splice(index, 1) else selectedTags.value.push(tag)
}
else {
selectedTags.value.push(tag)
}
} }
function clearFilters() { function clearFilters() {
@@ -63,18 +63,17 @@ const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + select
</script> </script>
<template> <template>
<main class="space-y-8"> <main class="space-y-8 py-8">
<div class="space-y-4"> <div class="space-y-4">
<h1 class="text-3xl sm:text-4xl font-bold text-neutral-900 dark:text-white"> <h1 class="text-3xl sm:text-4xl font-bold text-neutral-900 dark:text-white font-mono tracking-tight">
Engineering & Research Labs <span class="text-primary-500">/</span> projects
</h1> </h1>
<p class="text-lg text-neutral-600 dark:text-neutral-400"> <p class="max-w-3xl leading-relaxed">
Bridging the gap between theoretical models and production systems. Explore my experimental labs, open-source contributions, and engineering work. Bridging the gap between theoretical models and production systems. Explore my experimental labs, open-source contributions, and engineering work.
</p> </p>
</div> </div>
<!-- Filters --> <div class="flex items-center gap-4 overflow-x-auto flex-nowrap md:flex-wrap w-full whitespace-nowrap pb-2">
<div class="flex items-center gap-4 overflow-x-auto flex-nowrap md:flex-wrap w-full whitespace-nowrap">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Status:</span> <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Status:</span>
<UButton <UButton
@@ -97,7 +96,6 @@ const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + select
</UButton> </UButton>
</div> </div>
<!-- Ajout: bouton Clear à droite -->
<div class="ml-auto"> <div class="ml-auto">
<UButton <UButton
v-if="hasActiveFilters" v-if="hasActiveFilters"
@@ -113,10 +111,9 @@ const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + select
</div> </div>
</div> </div>
<!-- Tags Filter -->
<div <div
v-if="allTags.length > 0" v-if="allTags.length > 0"
class="grid grid-flow-col grid-rows-3 auto-cols-max gap-2 overflow-x-auto w-full snap-x snap-mandatory md:flex md:flex-wrap md:overflow-visible" class="grid grid-flow-col grid-rows-3 auto-cols-max gap-2 overflow-x-auto w-full snap-x snap-mandatory md:flex md:flex-wrap md:overflow-visible pb-2"
> >
<UBadge <UBadge
v-for="tag in allTags" v-for="tag in allTags"
@@ -128,73 +125,70 @@ const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + select
> >
{{ tag }} {{ tag }}
</UBadge> </UBadge>
<!-- Lien Clear mobile -->
<UButton
v-if="hasActiveFilters"
class="md:hidden justify-self-end"
variant="ghost"
color="neutral"
size="xs"
icon="i-ph-x-circle-duotone"
aria-label="Clear filters"
@click="clearFilters"
>
Clear
</UButton>
</div> </div>
<!-- Projects Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UCard <UCard
v-for="project in filteredProjects" v-for="project in filteredProjects"
:key="project.slug" :key="project.slug"
class="relative hover:scale-[1.02] transition-transform cursor-pointer" class="relative hover:scale-102 transition-all duration-300 group"
> >
<template #header> <template #header>
<div class="flex items-start justify-between gap-4"> <div class="flex items-start gap-4">
<div class="flex items-start gap-3 flex-1"> <div class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-neutral-700 dark:text-neutral-300 shrink-0">
<UIcon <UIcon
v-if="project.icon" :name="project.icon || 'i-ph-code-duotone'"
:name="project.icon" class="w-6 h-6"
class="text-3xl text-neutral-700 dark:text-neutral-300 shrink-0"
/> />
<div class="flex-1 min-w-0"> </div>
<h3 class="text-lg font-semibold text-neutral-900 dark:text-white truncate">
{{ project.title }} <div class="flex-1 min-w-0">
</h3> <UTooltip
<div class="flex items-center gap-2 mt-1 flex-wrap"> :text="project.title"
<UBadge :popper="{ placement: 'top-start' }"
v-if="project.type" class="w-full relative z-10"
:color="(typeColors[project.type] || 'neutral') as any" >
variant="subtle" <NuxtLink
size="xs" :to="`/projects/${project.slug}`"
> class="block focus:outline-none"
{{ project.type }} >
</UBadge> <h3 class="text-lg font-bold truncate group-hover:text-neutral-900 text-neutral-500 dark:group-hover:text-white transition-colors duration-200">
<UBadge {{ project.title }}
v-if="project.status" </h3>
:color="(statusColors[project.status] || 'neutral') as any" </NuxtLink>
variant="subtle" </UTooltip>
size="xs"
> <div class="flex items-center gap-2 mt-2 flex-wrap relative z-10">
{{ project.status }} <UBadge
</UBadge> v-if="project.type"
<UBadge :color="(typeColors[project.type] || 'neutral') as any"
v-if="project.favorite" variant="subtle"
color="amber" size="xs"
variant="subtle" >
size="xs" {{ project.type }}
> </UBadge>
Favorite <UBadge
</UBadge> v-if="project.status"
</div> :color="(statusColors[project.status] || 'neutral') as any"
variant="subtle"
size="xs"
>
{{ project.status }}
</UBadge>
<UBadge
v-if="project.favorite"
color="amber"
variant="subtle"
size="xs"
>
</UBadge>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<p class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-3"> <p class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-3 leading-relaxed">
{{ project.description }} {{ project.description }}
</p> </p>
@@ -207,50 +201,55 @@ const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + select
color="neutral" color="neutral"
variant="outline" variant="outline"
size="xs" size="xs"
class="opacity-75"
> >
{{ tag }} {{ tag }}
</UBadge> </UBadge>
<UBadge <span
v-if="project.tags && project.tags.length > 3" v-if="project.tags && project.tags.length > 3"
color="neutral" class="text-xs text-neutral-400 font-mono ml-1 self-center"
variant="outline"
size="xs"
> >
+{{ project.tags.length - 3 }} +{{ project.tags.length - 3 }}
</UBadge> </span>
</div> </div>
<span <span
v-if="project.readingTime" v-if="project.readingTime"
class="text-xs text-neutral-500 dark:text-neutral-400" class="text-xs text-neutral-400 font-mono flex items-center gap-1 shrink-0 ml-2"
> >
{{ project.readingTime }} min read <UIcon
name="i-ph-hourglass-medium-duotone"
class="w-3 h-3"
/>
{{ project.readingTime }}m
</span> </span>
</div> </div>
</template> </template>
<!-- Full-card link overlay for navigation -->
<NuxtLink <NuxtLink
:to="`/projects/${project.slug}`" :to="`/projects/${project.slug}`"
:aria-label="`Open project: ${project.title}`" :aria-label="`Open project: ${project.title}`"
class="absolute inset-0" class="absolute inset-0 z-0"
/> />
</UCard> </UCard>
</div> </div>
<!-- Empty State -->
<div <div
v-if="filteredProjects.length === 0" v-if="filteredProjects.length === 0"
class="text-center py-12" class="text-center py-20 border border-dashed border-gray-200 dark:border-gray-800 rounded-xl"
> >
<UIcon <UIcon
name="i-ph-folder-open-duotone" name="i-ph-flask-duotone"
class="text-6xl text-neutral-400 dark:text-neutral-600 mb-4" class="text-6xl text-neutral-300 dark:text-neutral-700 mb-4"
/> />
<p class="text-lg text-neutral-600 dark:text-neutral-400"> <h3 class="text-lg font-medium text-neutral-900 dark:text-white">
No projects found with the selected filters. No experiments found
</h3>
<p class="text-neutral-500 dark:text-neutral-400 mb-6">
No projects match the selected filters.
</p> </p>
<UButton <UButton
class="mt-4" color="primary"
variant="soft"
@click="clearFilters" @click="clearFilters"
> >
Clear Filters Clear Filters