Refactor code structure for improved readability and maintainability

This commit is contained in:
2025-12-22 23:09:21 +01:00
parent c04bf9f82b
commit e0589826bb
31 changed files with 407 additions and 180 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
useHead({
link: [{ rel: 'icon', type: 'image/webp', href: '/favicon.webp' }]
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
})
</script>

View File

@@ -84,6 +84,7 @@ const socialsList = [
/>
</UTooltip>
</UDropdownMenu>
<ThemeSwitcher />
</nav>
</header>
</template>

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
const { education } = await useContent()
const formatDate = (iso?: string) => {
if (!iso) return 'Present'
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return useDateFormat(d, 'MMM YYYY', { locales: 'en-US' }).value
}
</script>
<template>
<section
v-if="education && education.length"
class="my-8 space-y-6"
aria-labelledby="education-title"
>
<h2
id="education-title"
class="sr-only"
>
Education
</h2>
<div class="grid gap-4 grid-cols-1">
<UCard
v-for="item in education"
:key="item.id"
variant="outline"
color="neutral"
>
<div>
<h3 class="text-lg md:text-xl font-semibold tracking-tight">
{{ item.degree ?? item.title }}
</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-2">
{{ item.institution }}
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex flex-wrap items-center gap-2">
<span class="font-medium">Dates:</span>
<span>{{ formatDate(item.startDate) }} {{ formatDate(item.endDate) }}</span>
<span
v-if="item.duration"
class="text-neutral-500"
>({{ item.duration }})</span>
</p>
<p
class="text-sm text-neutral-700 dark:text-neutral-300 mt-1"
>
<span class="font-medium">Location:</span> {{ item.location }}
</p>
<p
class="text-sm text-neutral-700 dark:text-neutral-300 mt-3"
>
{{ item.description }}
</p>
</div>
</UCard>
</div>
</section>
</template>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
const { experiences } = await useContent()
const formatDate = (iso?: string) => {
if (!iso) return 'Present'
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return useDateFormat(d, 'MMM YYYY', { locales: 'en-US' }).value
}
</script>
<template>
<section
v-if="experiences && experiences.length"
class="my-8 space-y-6"
aria-labelledby="experiences-title"
>
<h2
id="experiences-title"
class="sr-only"
>
Experiences
</h2>
<div class="grid gap-4 grid-cols-1">
<UCard
v-for="item in experiences"
:key="item.id"
variant="outline"
color="neutral"
>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div
v-if="item.emoji"
class="text-2xl leading-none"
>
{{ item.emoji }}
</div>
<div class="flex-1">
<h3 class="text-lg md:text-xl font-semibold tracking-tight">
{{ item.title }}<span
v-if="item.type"
class="text-md text-neutral-500 font-normal"
> · {{ item.type }}</span>
</h3>
<p class="text-sm text-neutral-700 dark:text-neutral-300">
<span class="font-medium mr-2">Company:</span>
<span
v-if="item.companyUrl"
class="underline decoration-neutral-400/70 underline-offset-4 hover:decoration-neutral-600 dark:hover:decoration-neutral-300"
>
<NuxtLink
:to="item.companyUrl"
target="_blank"
rel="noreferrer"
>{{ item.company }}</NuxtLink>
</span>
<span v-else>{{ item.company }}</span>
</p>
</div>
</div>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex flex-wrap items-center gap-2">
<span class="font-medium">Dates:</span>
<span>{{ formatDate(item.startDate) }} {{ formatDate(item.endDate) }}</span>
<span
v-if="item.duration"
class="text-neutral-500"
>({{ item.duration }})</span>
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300 flex items-center gap-2">
<span class="font-medium">Location:</span>
<span>{{ item.location }}</span>
</p>
<p class="text-sm text-neutral-700 dark:text-neutral-300">
{{ item.description }}
</p>
<div
v-if="item.tags?.length"
class="flex flex-wrap gap-2"
>
<UBadge
v-for="tag in item.tags"
:key="tag"
size="sm"
variant="soft"
color="primary"
>
{{ tag }}
</UBadge>
</div>
</div>
</UCard>
</div>
</section>
</template>

View File

@@ -7,7 +7,7 @@
alt="Avatar"
class="hover:rotate-360 duration-500 transform-gpu rounded-full"
size="xl"
src="/favicon.webp"
src="/arthur.webp"
/>
</UTooltip>
</ClientOnly>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
const { skills } = await useContent()
</script>
<template>
<section
v-if="skills"
class="my-8 space-y-6"
aria-labelledby="skills-title"
>
<h2
id="skills-title"
class="sr-only"
>
Skills
</h2>
<div
v-for="skill in skills.body"
:key="skill.id"
>
<div class>
<h3 class="text-xl md:text-2xl font-semibold tracking-tight mb-4">
{{ skill.name }}
</h3>
<div class="flex flex-wrap gap-2">
<UBadge
v-for="item in skill.items"
:key="item.name"
:icon="item.icon"
variant="soft"
color="primary"
class="transition-colors duration-200 hover:opacity-80"
:aria-label="item.name"
>
{{ item.name }}
</UBadge>
</div>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,8 @@
export async function useContent() {
const skills = await queryCollection('skills').where('extension', '=', 'json').first()
const projects = await queryCollection('projects').where('extension', '=', 'md').order('publishedAt', 'DESC').all()
const education = await queryCollection('education').where('extension', '=', 'md').order('startDate', 'DESC').all()
const experiences = await queryCollection('experiences').where('extension', '=', 'md').order('startDate', 'DESC').all()
return { skills, projects, education, experiences }
}

13
app/pages/activity.vue Normal file
View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>

13
app/pages/ecosystem.vue Normal file
View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>

15
app/pages/hobbies.vue Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
const { data: page } = await useAsyncData('hobbies', () => {
return queryCollection('hobbies').first()
})
</script>
<template>
<main>
<ContentRenderer
v-if="page"
:value="page"
class="mt-8 md:mt-16"
/>
</main>
</template>

View File

@@ -16,6 +16,9 @@ const { data: page } = await useAsyncData('index', () => {
:value="page"
class="mt-8 md:mt-16"
/>
<HomeSkills />
<HomeEducation />
<HomeExperiences />
<HomeStats />
<HomeActivity />
<HomeQuote />

View File

@@ -1,13 +1,12 @@
<script lang="ts" setup>
const { data: projects } = await useAsyncData('projects', () => {
return queryCollection('projects').all()
})
</script>
<template>
<div>
PROJECTS PAGE
{{ projects }}
</div>
</template>
<style scoped>
</style>

15
app/pages/uses.vue Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
const { data: page } = await useAsyncData('uses', () => {
return queryCollection('uses').first()
})
</script>
<template>
<main>
<ContentRenderer
v-if="page"
:value="page"
class="mt-8 md:mt-16"
/>
</main>
</template>

View File

@@ -24,6 +24,7 @@
},
"devDependencies": {
"@iconify-json/devicon": "1.2.54",
"@iconify-json/file-icons": "^1.2.2",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/twemoji": "1.2.5",
@@ -236,6 +237,8 @@
"@iconify-json/devicon": ["@iconify-json/devicon@1.2.54", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-I188YZ/t+SF2bfZnrRjwmdr/nzUNXuc/S3uvsZF0LG6atOuGtKk7KcmK/NUaTB2JAIeM1hsD+7wieYBObI8O4Q=="],
"@iconify-json/file-icons": ["@iconify-json/file-icons@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-ajk44wYGTiu79EAyrfNHNaxZ1/2Z1xuSOAvYUT/pAPWHc+P6n9TUZP/ccnPG18kx0WMBxmkHHQ9/zkm6ENhk+Q=="],
"@iconify-json/logos": ["@iconify-json/logos@1.2.10", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qxaXKJ6fu8jzTMPQdHtNxlfx6tBQ0jXRbHZIYy5Ilh8Lx9US9FsAdzZWUR8MXV8PnWTKGDFO4ZZee9VwerCyMA=="],
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.73", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-++HFkqDNu4jqG5+vYT+OcVj9OiuPCw9wQuh8G5QWQnBRSJ9eKwSStiU8ORgOoK07xJsm/0VIHySMniXUUXP9Gw=="],

View File

@@ -30,8 +30,7 @@ export const collections = {
type: 'data',
source: 'skills.json',
schema: z.object({
description: z.string(),
skills: z.array(z.object({
body: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
@@ -80,10 +79,9 @@ export const collections = {
type: 'data',
source: 'contact.json',
schema: z.object({
contact: z.array(z.object({
body: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
category: z.string().optional(),
icon: z.string().optional(),
value: z.string().url(),
@@ -100,7 +98,7 @@ export const collections = {
type: 'data',
source: 'languages.json',
schema: z.object({
languages: z.array(z.object({
body: z.array(z.object({
name: z.string(),
level: z.string(),
proficiency: z.string()

View File

@@ -1,77 +1,69 @@
{
"contact": [
{
"id": "personal-email",
"name": "Email Personnel",
"description": "Contactez-moi pour des questions personnelles",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-perso",
"priority": 1
},
{
"id": "professional-email",
"name": "Email Professionnel",
"description": "Pour les opportunités professionnelles et collaborations",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-pro",
"priority": 1
},
{
"id": "linkedin",
"name": "LinkedIn",
"description": "Profil professionnel et réseau",
"category": "social",
"icon": "i-ph:linkedin-logo-duotone",
"value": "https://go.arthurdanjou.fr/linkedin",
"priority": 2
},
{
"id": "github",
"name": "GitHub",
"description": "Projets open-source et portefeuille technique",
"category": "social",
"icon": "i-ph:github-logo-duotone",
"value": "https://go.arthurdanjou.fr/github",
"username": "ArthurDanjou",
"priority": 1
},
{
"id": "twitter",
"name": "Twitter / X",
"description": "Actualités tech et partages d'idées",
"category": "social",
"icon": "i-ph:x-logo-duotone",
"value": "https://go.arthurdanjou.fr/twitter",
"priority": 3
},
{
"id": "discord",
"name": "Discord",
"description": "Discussions en temps réel et communauté",
"category": "communication",
"icon": "i-ph:discord-logo-duotone",
"value": "https://go.arthurdanjou.fr/discord",
"priority": 2
},
{
"id": "personal-website",
"name": "Site Personnel",
"description": "Accueil et portefeuille complet",
"category": "web",
"icon": "i-ph:globe-duotone",
"value": "https://arthurdanjou.fr",
"priority": 1
},
{
"id": "status-page",
"name": "Statut des Services",
"description": "État et disponibilité des services",
"category": "infrastructure",
"icon": "i-ph:fire-duotone",
"value": "https://go.arthurdanjou.fr/status",
"priority": 3
}
]
"body": [
{
"id": "personal-email",
"name": "Email Personnel",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-perso",
"priority": 1
},
{
"id": "professional-email",
"name": "Email Professionnel",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-pro",
"priority": 1
},
{
"id": "linkedin",
"name": "LinkedIn",
"category": "social",
"icon": "i-ph:linkedin-logo-duotone",
"value": "https://go.arthurdanjou.fr/linkedin",
"priority": 2
},
{
"id": "github",
"name": "GitHub",
"category": "social",
"icon": "i-ph:github-logo-duotone",
"value": "https://go.arthurdanjou.fr/github",
"username": "ArthurDanjou",
"priority": 1
},
{
"id": "twitter",
"name": "Twitter / X",
"category": "social",
"icon": "i-ph:x-logo-duotone",
"value": "https://go.arthurdanjou.fr/twitter",
"priority": 3
},
{
"id": "discord",
"name": "Discord",
"category": "communication",
"icon": "i-ph:discord-logo-duotone",
"value": "https://go.arthurdanjou.fr/discord",
"priority": 2
},
{
"id": "personal-website",
"name": "Site Personnel",
"category": "web",
"icon": "i-ph:globe-duotone",
"value": "https://arthurdanjou.fr",
"priority": 1
},
{
"id": "status-page",
"name": "Statut des Services",
"category": "infrastructure",
"icon": "i-ph:fire-duotone",
"value": "https://go.arthurdanjou.fr/status",
"priority": 3
}
]
}

View File

@@ -0,0 +1,19 @@
---
title: Hackathon CND - Machine Learning for Cybersecurity
type: Hackathon
company: Commissariat au numérique de défense (CND), French Armies ministry
companyUrl: https://www.defense.gouv.fr/cnd
location: Fort du Mont-Valérien, Suresnes, France
startDate: 2025-11
endDate: 2025-11
duration: 3 days
description: Developed a Python ML pipeline during the CND hackathon to classify system logs for bug and attack detection. Implemented feature extraction and preprocessing, trained and evaluated models (tree-based and lightweight neural), tuned thresholds to favor recall, and delivered a realtime prototype with visualization and reproducible code in collaboration with CND engineers. Implemented a Streamlit application to test the classifier interactively and used an LLM to generate contextual help explaining the likely origin and indicators of detected bugs or attacks for end users.
tags:
- Python
- Machine Learning
- AI
- Cybersecurity
- Streamlit
- LLM
emoji: 🔒
---

View File

@@ -1,19 +0,0 @@
---
title: Hackathon CND - Machine Learning for Cybersecurity
type: Hackathon
company: Commissariat au numérique de défense (CND), French Armies ministry
companyUrl: https://www.defense.gouv.fr/cnd
location: Fort du Mont-Valérien, Suresnes, France
startDate: 2025-11
endDate: 2025-11
duration: 3 days
description: Developed a Python ML pipeline during the Dirisi hackathon to classify system logs for bug and attack detection. Implemented feature extraction and preprocessing, trained and evaluated models (tree-based and lightweight neural), tuned thresholds to favor recall, and delivered a realtime prototype with visualization and reproducible code in collaboration with CND engineers. Implemented a Streamlit application to test the classifier interactively and used an LLM to generate contextual help explaining the likely origin and indicators of detected bugs or attacks for end users.
tags:
- Python
- Machine Learning
- AI
- Cybersecurity
- Streamlit
- LLM
emoji: 🔒
---

View File

@@ -1,5 +1,5 @@
---
title: Data Analyst Intern
title: Data Engineer Intern
type: Internship
company: Sevetys
companyUrl: https://sevetys.fr
@@ -7,7 +7,7 @@ location: Paris, France
startDate: 2025-06
endDate: 2025-07
duration: 2 months
description: At Sevetys, I worked as a Data Analyst on topics related to client and patient data. My responsibilities included Python development using PySpark on Microsoft Azure, data modeling based on business needs, and ensuring data quality. This experience allowed me to deepen my data engineering skills while working autonomously in a demanding cloud-based environment.
description: At Sevetys, I worked as a Data Engineer on topics related to client and patient data. My responsibilities included Python development using PySpark on Microsoft Azure, data modeling based on business needs, and ensuring data quality. This experience allowed me to deepen my data engineering skills while working autonomously in a demanding cloud-based environment.
tags:
- Python
- PySpark

View File

@@ -32,4 +32,4 @@ These tools allow me to go from :hover-text{hover="Exploration, cleaning, reshap
I'm :hover-text{hover="As tech is always evolving, I need to be up-to-date 🖥️" position="top" text="constantly"} learning new things, from technology to finance and entrepreneurship. I love :hover-text{hover="I love sharing my knowledge and helping others 🫂" text="sharing"} my knowledge and learning new theorems and technologies. I'm a :hover-text{hover="I'm constantly looking to discover new things" text="curious"} person and eager to continue learning and growing throughout my life.
As well as programming, I enjoy :hover-text{hover="Sport allows me to burn off energy 🏋️‍♂️" text="sport"} and :hover-text{hover="Travelling frees me and gets me away from it all ✈️" text="travelling"} . My passion, commitment and eagerness to learn and progress are the qualities that enable me to succeed in my :hover-text{hover="Career already begun and far from over 😎" text="career"} and :hover-text{hover="Only 2 years of study left 💪" text="studies"} .
As well as programming, I enjoy :hover-text{hover="Sport allows me to burn off energy 🏋️‍♂️" text="sport"} and :hover-text{hover="Travelling frees me and gets me away from it all ✈️" text="travelling"} . My passion, commitment and eagerness to learn and progress are the qualities that enable me to succeed in my :hover-text{hover="Career already begun and far from over 😎" text="career"} and :hover-text{hover="Only 2 years of study left 💪" text="studies"} .

View File

@@ -1,5 +1,5 @@
{
"languages": [
"body": [
{
"name": "French",
"level": "Native",

View File

@@ -1,6 +1,5 @@
{
"description": "Master's student in Applied Mathematics (M280 - Paris Dauphine) specializing in Data Science and AI. My profile sits at the intersection of theoretical research and software engineering. I leverage my strong background in probability, statistics, and optimization to design robust Deep Learning architectures, while using my engineering skills (MLOps, Infrastructure) to deploy them efficiently. Currently looking for a research-oriented final year internship (April 2026) leading to a PhD.",
"skills": [
"body": [
{
"id": "scientific-computing",
"name": "Scientific Computing & AI",
@@ -31,9 +30,13 @@
"icon": "i-devicon-scikitlearn"
},
{
"name": "Pandas & Numpy",
"name": "Pandas",
"icon": "i-devicon-pandas"
},
{
"name": "NumPy",
"icon": "i-logos-numpy"
},
{
"name": "MatPlotLib",
"icon": "i-devicon-matplotlib"
@@ -76,6 +79,10 @@
{
"name": "Apache Spark (PySpark)",
"icon": "i-logos-apache-spark"
},
{
"name": "Cloudflare",
"icon": "i-logos-cloudflare-icon"
}
]
},
@@ -103,6 +110,10 @@
{
"name": "AdonisJs",
"icon": "i-logos-adonisjs-icon"
},
{
"name": "Gradio",
"icon": "i-logos-gradio-icon"
}
]
}

View File

@@ -33,6 +33,7 @@ This page documents all the tools, equipment and services I use daily for my wor
### IDEs
- **Visual Studio Code** - My main development environment. Flexible, performant and lightweight. Supports Python, JavaScript, TypeScript, SQL and much more. I especially appreciate the extensions and AI integrations
- **Cursor** - A VSCode fork with AI-powered code completions and suggestions to boost productivity
- **JetBrains Suite** (IntelliJ IDEA Ultimate, PyCharm Professional, WebStorm, DataGrip) - Which I've been using for 7 years. The best IDEs for Java, Python, JavaScript, SQL and other languages
### Theme and Fonts
@@ -68,12 +69,11 @@ This page documents all the tools, equipment and services I use daily for my wor
### Self-Hosted Services
I maintain several services:
- **Monitoring & Infrastructure**: Uptime Kuma, Beszel, Traefik, Portainer
- **Security & Privacy**: Cloudflare, AdGuard Home, Vaultwarden, Tailscale
- **Storage & Media**: Minio, Immich
- **Smart Home**: Home Assistant
- **Other Utilities**: MySpeed, Palmr, Cap.so
---
*This list is constantly updated as I experiment with new tools and equipment.*

View File

@@ -33,6 +33,13 @@ export default defineNuxtConfig({
fallback: 'light'
},
content: {
database: {
type: 'd1',
bindingName: 'DB'
}
},
mdc: {
headings: {
anchorLinks: false
@@ -83,9 +90,11 @@ export default defineNuxtConfig({
nitro: {
preset: 'cloudflare_module',
experimental: {
openAPI: true
},
prerender: {
routes: ['/'],
crawlLinks: true
}
},

View File

@@ -30,6 +30,7 @@
},
"devDependencies": {
"@iconify-json/devicon": "1.2.54",
"@iconify-json/file-icons": "^1.2.2",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/twemoji": "1.2.5",

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -5,6 +5,8 @@ export default defineCachedEventHandler(async (event) => {
.where('extension', '=', 'json')
.first()
console.log(await queryCollection(event, 'contact').all())
if (!result) {
throw createError({ statusCode: 404, statusMessage: 'Contact information not found' })
}

View File

@@ -1,15 +0,0 @@
import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'hobbies')
.where('extension', '=', 'md')
.first()
if (!result) {
throw createError({ statusCode: 404, statusMessage: 'Hobbies not found' })
}
return result.body
}, {
maxAge: 60 * 60 * 24,
name: 'hobbies'
})

View File

@@ -1,16 +0,0 @@
import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'profile')
.where('extension', '=', 'md')
.first()
if (!result) {
throw createError({ statusCode: 404, statusMessage: 'Profile not found' })
}
return result
}, {
maxAge: 60 * 60 * 24,
name: 'profile'
})

View File

@@ -1,33 +0,0 @@
import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const categories = await queryCollection(event, 'usesCategories')
.where('extension', '=', 'md')
.all()
if (categories.length === 0) {
throw createError({ statusCode: 404, statusMessage: 'Uses categories not found' })
}
const uses = await queryCollection(event, 'uses')
.where('extension', '=', 'md')
.all()
if (uses.length === 0) {
throw createError({ statusCode: 404, statusMessage: 'Uses not found' })
}
const uses_by_categories = []
for (const category of categories) {
uses_by_categories.push({
category: category,
uses: uses.filter((use: { category: unknown }) => use.category === category.slug)
})
}
return uses_by_categories
},
{
maxAge: 60 * 60 * 24,
name: 'uses'
})

View File

@@ -59,8 +59,12 @@ interface Nav {
export const navs: readonly Nav[] = [
{ label: 'home', to: '/', icon: 'house-duotone' },
{ label: 'uses', to: '/uses', icon: 'tree-evergreen-duotone' },
{ label: 'projects', to: '/projects', icon: 'folder-duotone' },
{ label: 'hobbies', to: '/hobbies', icon: 'game-controller-duotone' },
{ label: 'stats', to: '/stats', icon: 'chart-bar-duotone' },
{ label: 'activity', to: '/activity', icon: 'activity-duotone' },
{ label: 'ecosystem', to: '/ecosystem', icon: 'graph-duotone' },
{
label: 'resume',
icon: 'address-book-duotone',