mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 15:54:13 +01:00
Refactor: update project status values to use consistent capitalization; enhance app configuration for preview environment
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
container: {
|
||||
base: 'max-w-4xl'
|
||||
base: 'max-w-5xl'
|
||||
},
|
||||
colors: {
|
||||
primary: 'neutral',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function useProjectColors() {
|
||||
const statusColors: Record<string, string> = {
|
||||
'active': 'blue',
|
||||
'completed': 'green',
|
||||
'archived': 'neutral',
|
||||
'in progress': 'amber'
|
||||
'Active': 'blue',
|
||||
'Completed': 'green',
|
||||
'Archived': 'neutral',
|
||||
'In Progress': 'amber'
|
||||
}
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
|
||||
@@ -15,245 +15,251 @@ useSeoMeta({
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
|
||||
const selectedStatus = ref<string | null>(null)
|
||||
const selectedTags = ref<string[]>([])
|
||||
const { statusColors, typeColors } = useProjectColors()
|
||||
|
||||
const statuses = computed(() => {
|
||||
const allStatuses = new Set<string>()
|
||||
const selectedType = ref<string | null>(null)
|
||||
const selectedStatus = ref<string | null>(null)
|
||||
|
||||
const availableTypes = computed(() => {
|
||||
const types = new Set<string>()
|
||||
projects.value?.forEach((project) => {
|
||||
if (project.status) allStatuses.add(project.status)
|
||||
if (project.type) types.add(project.type)
|
||||
})
|
||||
return Array.from(allStatuses).sort()
|
||||
return Array.from(types).sort()
|
||||
})
|
||||
|
||||
const allTags = computed(() => {
|
||||
const tags = new Set<string>()
|
||||
const availableStatuses = computed(() => {
|
||||
const statuses = new Set<string>()
|
||||
projects.value?.forEach((project) => {
|
||||
project.tags?.forEach((tag: string) => tags.add(tag))
|
||||
if (project.status) statuses.add(project.status)
|
||||
})
|
||||
return Array.from(tags).sort()
|
||||
return Array.from(statuses).sort()
|
||||
})
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
if (!projects.value) return []
|
||||
|
||||
return projects.value.filter((project) => {
|
||||
const typeMatch = !selectedType.value || project.type === selectedType.value
|
||||
const statusMatch = !selectedStatus.value || project.status === selectedStatus.value
|
||||
const tagsMatch = selectedTags.value.length === 0
|
||||
|| selectedTags.value.some(tag => project.tags?.includes(tag))
|
||||
return statusMatch && tagsMatch
|
||||
return typeMatch && statusMatch
|
||||
})
|
||||
})
|
||||
|
||||
const { statusColors, typeColors } = useProjectColors()
|
||||
|
||||
function toggleTag(tag: string) {
|
||||
const index = selectedTags.value.indexOf(tag)
|
||||
if (index > -1) selectedTags.value.splice(index, 1)
|
||||
else selectedTags.value.push(tag)
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
selectedType.value = null
|
||||
selectedStatus.value = null
|
||||
selectedTags.value = []
|
||||
}
|
||||
|
||||
const hasActiveFilters = computed(() => !!selectedStatus.value || selectedTags.value.length > 0)
|
||||
const activeFilterCount = computed(() => (selectedStatus.value ? 1 : 0) + selectedTags.value.length)
|
||||
const hasActiveFilters = computed(() => !!selectedType.value || !!selectedStatus.value)
|
||||
const activeFilterCount = computed(() => (selectedType.value ? 1 : 0) + (selectedStatus.value ? 1 : 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="space-y-8 py-8">
|
||||
<main class="space-y-8 py-4">
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-neutral-900 dark:text-white font-mono tracking-tight">
|
||||
<span class="text-primary-500">/</span> projects
|
||||
Engineering & Research Labs
|
||||
</h1>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 overflow-x-auto flex-nowrap md:flex-wrap w-full whitespace-nowrap pb-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Status:</span>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2 overflow-x-auto w-full whitespace-nowrap pb-2">
|
||||
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300 mr-2 min-w-12.5">Type:</span>
|
||||
<UButton
|
||||
:variant="!selectedStatus ? 'solid' : 'ghost'"
|
||||
:variant="!selectedType ? 'solid' : 'ghost'"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
@click="selectedStatus = null"
|
||||
@click="selectedType = null"
|
||||
>
|
||||
All
|
||||
</UButton>
|
||||
<UButton
|
||||
v-for="status in statuses"
|
||||
:key="status"
|
||||
:variant="selectedStatus === status ? 'solid' : 'ghost'"
|
||||
:color="(statusColors[status] || 'neutral') as any"
|
||||
v-for="type in availableTypes"
|
||||
:key="type"
|
||||
:variant="selectedType === type ? 'subtle' : 'ghost'"
|
||||
:color="(typeColors[type] || 'neutral') as any"
|
||||
size="sm"
|
||||
@click="selectedStatus = selectedStatus === status ? null : status"
|
||||
class="transition-all duration-200"
|
||||
:class="selectedType === type ? 'ring-1 ring-inset' : ''"
|
||||
@click="selectedType = selectedType === type ? null : type"
|
||||
>
|
||||
{{ status }}
|
||||
{{ type }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<UButton
|
||||
v-if="hasActiveFilters"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
icon="i-ph-x-circle-duotone"
|
||||
aria-label="Clear filters"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Clear filters ({{ activeFilterCount }})
|
||||
</UButton>
|
||||
<div class="flex items-center gap-4 overflow-x-auto flex-nowrap md:flex-wrap w-full whitespace-nowrap pb-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Status:</span>
|
||||
<UButton
|
||||
:variant="!selectedStatus ? 'solid' : 'ghost'"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
@click="selectedStatus = null"
|
||||
>
|
||||
All
|
||||
</UButton>
|
||||
<UButton
|
||||
v-for="status in availableStatuses"
|
||||
:key="status"
|
||||
:variant="selectedStatus === status ? 'solid' : 'ghost'"
|
||||
:color="(statusColors[status] || 'neutral') as any"
|
||||
size="sm"
|
||||
@click="selectedStatus = selectedStatus === status ? null : status"
|
||||
>
|
||||
{{ status }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<UButton
|
||||
v-if="hasActiveFilters"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
icon="i-ph-x-circle-duotone"
|
||||
aria-label="Clear filters"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Clear filters ({{ activeFilterCount }})
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
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 pb-2"
|
||||
>
|
||||
<UBadge
|
||||
v-for="tag in allTags"
|
||||
:key="tag"
|
||||
:color="selectedTags.includes(tag) ? 'primary' : 'neutral'"
|
||||
:variant="selectedTags.includes(tag) ? 'solid' : 'subtle'"
|
||||
class="inline-flex items-center justify-center text-center cursor-pointer hover:scale-105 transition-transform shrink-0 snap-start"
|
||||
@click="toggleTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</UBadge>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<UCard
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.slug"
|
||||
class="relative hover:scale-102 transition-all duration-300 group"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-neutral-700 dark:text-neutral-300 shrink-0">
|
||||
<UIcon
|
||||
:name="project.icon || 'i-ph-code-duotone'"
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<UTooltip
|
||||
:text="project.title"
|
||||
:popper="{ placement: 'top-start' }"
|
||||
class="w-full relative z-10"
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<UCard
|
||||
v-for="project in filteredProjects"
|
||||
:key="project.slug"
|
||||
class="relative hover:shadow-sm transition-all duration-300 group"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="p-2 rounded-lg shrink-0 flex items-center justify-center"
|
||||
:class="project.favorite ? 'ring-2 ring-amber-400 text-amber-400' : 'bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300'"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="`/projects/${project.slug}`"
|
||||
class="block focus:outline-none"
|
||||
>
|
||||
<h3 class="text-lg font-bold truncate group-hover:text-neutral-900 text-neutral-500 dark:group-hover:text-white transition-colors duration-200">
|
||||
{{ project.title }}
|
||||
</h3>
|
||||
</NuxtLink>
|
||||
</UTooltip>
|
||||
<UIcon
|
||||
:name="project.icon || 'i-ph-code-duotone'"
|
||||
class="w-6 h-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-2 flex-wrap relative z-10">
|
||||
<UBadge
|
||||
v-if="project.type"
|
||||
:color="(typeColors[project.type] || 'neutral') as any"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
<div class="flex-1 min-w-0">
|
||||
<UTooltip
|
||||
:text="project.title"
|
||||
:popper="{ placement: 'top-start' }"
|
||||
class="w-full relative z-10"
|
||||
>
|
||||
{{ project.type }}
|
||||
</UBadge>
|
||||
<UBadge
|
||||
v-if="project.status"
|
||||
: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>
|
||||
<NuxtLink
|
||||
:to="`/projects/${project.slug}`"
|
||||
class="block focus:outline-none"
|
||||
>
|
||||
<h3 class="text-lg font-bold truncate group-hover:text-neutral-900 text-neutral-500 dark:group-hover:text-white transition-colors duration-200">
|
||||
{{ project.title }}
|
||||
</h3>
|
||||
</NuxtLink>
|
||||
</UTooltip>
|
||||
|
||||
<div class="flex items-center gap-2 mt-2 flex-wrap relative z-10">
|
||||
<UBadge
|
||||
v-if="project.type"
|
||||
:color="(typeColors[project.type] || 'neutral') as any"
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
>
|
||||
{{ project.type }}
|
||||
</UBadge>
|
||||
<UBadge
|
||||
v-if="project.status"
|
||||
: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>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-3 leading-relaxed">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-3 leading-relaxed">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<UBadge
|
||||
v-for="tag in project.tags?.slice(0, 3)"
|
||||
:key="tag"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
class="opacity-75"
|
||||
>
|
||||
{{ tag }}
|
||||
</UBadge>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<UBadge
|
||||
v-for="tag in project.tags?.slice(0, 3)"
|
||||
:key="tag"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
class="opacity-75"
|
||||
>
|
||||
{{ tag }}
|
||||
</UBadge>
|
||||
<span
|
||||
v-if="project.tags && project.tags.length > 3"
|
||||
class="text-xs text-neutral-400 font-mono ml-1 self-center"
|
||||
>
|
||||
+{{ project.tags.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="project.tags && project.tags.length > 3"
|
||||
class="text-xs text-neutral-400 font-mono ml-1 self-center"
|
||||
v-if="project.readingTime"
|
||||
class="text-xs text-neutral-400 font-mono flex items-center gap-1 shrink-0 ml-2"
|
||||
>
|
||||
+{{ project.tags.length - 3 }}
|
||||
<UIcon
|
||||
name="i-ph-hourglass-medium-duotone"
|
||||
class="w-3 h-3"
|
||||
/>
|
||||
{{ project.readingTime }}m
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="project.readingTime"
|
||||
class="text-xs text-neutral-400 font-mono flex items-center gap-1 shrink-0 ml-2"
|
||||
>
|
||||
<UIcon
|
||||
name="i-ph-hourglass-medium-duotone"
|
||||
class="w-3 h-3"
|
||||
/>
|
||||
{{ project.readingTime }}m
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<NuxtLink
|
||||
:to="`/projects/${project.slug}`"
|
||||
:aria-label="`Open project: ${project.title}`"
|
||||
class="absolute inset-0 z-0"
|
||||
/>
|
||||
</UCard>
|
||||
</div>
|
||||
<NuxtLink
|
||||
:to="`/projects/${project.slug}`"
|
||||
:aria-label="`Open project: ${project.title}`"
|
||||
class="absolute inset-0 z-0"
|
||||
/>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="filteredProjects.length === 0"
|
||||
class="text-center py-20 border border-dashed border-gray-200 dark:border-gray-800 rounded-xl"
|
||||
>
|
||||
<UIcon
|
||||
name="i-ph-flask-duotone"
|
||||
class="text-6xl text-neutral-300 dark:text-neutral-700 mb-4"
|
||||
/>
|
||||
<h3 class="text-lg font-medium text-neutral-900 dark:text-white">
|
||||
No experiments found
|
||||
</h3>
|
||||
<p class="text-neutral-500 dark:text-neutral-400 mb-6">
|
||||
No projects match the selected filters.
|
||||
</p>
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="soft"
|
||||
@click="clearFilters"
|
||||
<div
|
||||
v-if="filteredProjects.length === 0"
|
||||
class="text-center py-20 border border-dashed border-neutral-200 dark:border-neutral-800 rounded-xl"
|
||||
>
|
||||
Clear Filters
|
||||
</UButton>
|
||||
<UIcon
|
||||
name="i-ph-flask-duotone"
|
||||
class="text-6xl text-neutral-300 dark:text-neutral-700 mb-4"
|
||||
/>
|
||||
<h3 class="text-lg font-medium text-neutral-900 dark:text-white">
|
||||
No experiments found
|
||||
</h3>
|
||||
<p class="text-neutral-500 dark:text-neutral-400 mb-6">
|
||||
No projects match the selected filters.
|
||||
</p>
|
||||
<UButton
|
||||
color="primary"
|
||||
variant="soft"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Clear Filters
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ description: My personal space on the web — a portfolio, a blog, and a digital
|
||||
publishedAt: 2024-06-01
|
||||
readingTime: 1
|
||||
favorite: true
|
||||
status: active
|
||||
status: Active
|
||||
tags:
|
||||
- Vue.js
|
||||
- Nuxt
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Personal Project
|
||||
description: A customizable browser homepage that lets you organize all your favorite links in one place with categories, tabs, icons and colors.
|
||||
publishedAt: 2024-09-04
|
||||
readingTime: 1
|
||||
status: archived
|
||||
status: Archived
|
||||
tags:
|
||||
- Nuxt
|
||||
- Vue.js
|
||||
|
||||
@@ -6,7 +6,7 @@ description: A personal homelab environment where I deploy, test, and maintain s
|
||||
publishedAt: 2025-09-04
|
||||
readingTime: 1
|
||||
favorite: true
|
||||
status: active
|
||||
status: Active
|
||||
tags:
|
||||
- Docker
|
||||
- Proxmox
|
||||
|
||||
@@ -6,7 +6,7 @@ description: A curated collection of mathematics and data science projects devel
|
||||
publishedAt: 2023-09-01
|
||||
readingTime: 1
|
||||
favorite: true
|
||||
status: in progress
|
||||
status: In progress
|
||||
tags:
|
||||
- Python
|
||||
- R
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Academic Project
|
||||
description: Predicting the number of bikes rented in a bike-sharing system using Generalized Linear Models and various statistical techniques.
|
||||
publishedAt: 2025-01-24
|
||||
readingTime: 1
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- R
|
||||
- Statistics
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Academic Project
|
||||
description: Prediction of breast cancer presence by comparing several supervised classification models using machine learning techniques.
|
||||
publishedAt: 2025-06-06
|
||||
readingTime: 2
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- Python
|
||||
- Machine Learning
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Research Project
|
||||
description: TensorFlow/Keras implementation and reproduction of "Dropout Reduces Underfitting" (Liu et al., 2023). A comparative study of Early and Late Dropout strategies to optimize model convergence.
|
||||
publishedAt: 2024-12-10
|
||||
readingTime: 4
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- Python
|
||||
- TensorFlow
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Academic Project
|
||||
description: Predicting loan approval and default risk using machine learning classification techniques.
|
||||
publishedAt: 2025-01-24
|
||||
readingTime: 2
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- Python
|
||||
- Machine Learning
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Academic Project
|
||||
description: An implementation of different Monte Carlo methods and algorithms in R, including inverse CDF simulation, accept-reject methods, and stratification techniques.
|
||||
publishedAt: 2024-11-24
|
||||
readingTime: 3
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- R
|
||||
- Mathematics
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Academic Project
|
||||
description: A Python implementation of the Schelling Segregation Model using statistics and data visualization to analyze spatial segregation patterns.
|
||||
publishedAt: 2024-05-03
|
||||
readingTime: 4
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- Python
|
||||
- Data Visualization
|
||||
|
||||
@@ -5,7 +5,7 @@ type: Internship Project
|
||||
description: Summary of my internship as a Data Engineer at Sevetys, focusing on data quality, cleaning, standardization, and comprehensive data quality metrics.
|
||||
publishedAt: 2025-07-31
|
||||
readingTime: 2
|
||||
status: completed
|
||||
status: Completed
|
||||
tags:
|
||||
- Python
|
||||
- PySpark
|
||||
|
||||
@@ -49,5 +49,30 @@
|
||||
"head_sampling_rate": 1,
|
||||
"persist": true
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "preview.arthurdanjou.fr",
|
||||
"zone_name": "arthurdanjou.fr",
|
||||
"custom_domain": true
|
||||
}
|
||||
],
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_id": "d6e8c2ff-399a-4bec-b1b3-4bfe57d50ea8",
|
||||
"migrations_table": "_hub_migrations",
|
||||
"migrations_dir": ".output/server/db/migrations/"
|
||||
}
|
||||
],
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "CACHE",
|
||||
"id": "f0766ace1d24423ba6e5cac4fb8f2054"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user