mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-31 01:28:42 +01:00
Refactor: clean up template structure and improve readability; enhance project card navigation
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user