Compare commits

15 Commits

Author SHA1 Message Date
c04bf9f82b feat: ajouter une notification Discord pour le statut de déploiement 2025-12-22 20:56:03 +01:00
4030bdb693 feat: simplifier les commandes de déploiement et de prévisualisation dans la configuration 2025-12-22 20:41:11 +01:00
db44535528 feat: mettre à jour le preset Nitro pour utiliser 'cloudflare_module' 2025-12-22 19:55:59 +01:00
986fd2b791 feat: mettre à jour la configuration de déploiement pour Cloudflare Workers et ajouter des paramètres d'observabilité 2025-12-22 19:51:59 +01:00
5f74bec60a feat: mettre à jour les variables d'environnement pour le déploiement sur Cloudflare 2025-12-22 19:47:06 +01:00
17306539fe feat: ajouter les CV en anglais et en français 2025-12-22 19:40:14 +01:00
14bed2c651 feat: ajouter les pages de projets et de statistiques 2025-12-22 19:40:11 +01:00
e57a0cbb1f feat: supprimer les outils inutilisés dans le dossier shared/utils/tools 2025-12-22 19:40:06 +01:00
0e7e3f5a37 feat: refactor Stats and Nav interfaces to simplify structure and remove unused code 2025-12-22 19:39:59 +01:00
a33bcb8bfc feat: refactor components to remove i18n and update static text 2025-12-22 19:39:53 +01:00
c39830803e feat: simplifier le footer et l'en-tête, supprimer le sélecteur de langue et mettre à jour le commutateur de thème 2025-12-22 19:39:44 +01:00
ba91408b6d feat: Add personal profile, projects, and skills documentation
- Created index.md for personal introduction and interests.
- Added languages.json to specify language proficiencies.
- Developed profile.md detailing academic background, skills, and career goals.
- Introduced multiple project markdown files showcasing personal and academic projects, including ArtChat, ArtHome, and various data science initiatives.
- Implemented skills.json to outline technical skills and competencies.
- Compiled uses.md to document hardware and software tools utilized for development and personal projects.
2025-12-22 19:39:36 +01:00
adea7fe35e feat: ajouter de nouveaux gestionnaires d'événements pour les API et supprimer les fichiers obsolètes 2025-12-22 19:39:10 +01:00
ed8ffedde5 feat: ajouter de nouvelles collections et modifier la configuration de Nuxt 2025-12-22 19:38:49 +01:00
cffa2e20fe chore: update dependencies and devDependencies in package.json
- Removed outdated dependencies: @ai-sdk/mcp and @ai-sdk/vue
- Updated @iconify-json/devicon to 1.2.54
- Updated @iconify-json/twemoji to 1.2.5
- Updated vue-tsc to 3.2.1
2025-12-22 19:38:40 +01:00
72 changed files with 1761 additions and 995 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy to Cloudflare Pages
name: Deploy to Cloudflare Workers
on:
push:
@@ -35,19 +35,42 @@ jobs:
- name: Build
run: bun run build
env:
NUXT_PUBLIC_I18N_BASE_URL: ${{ vars.NUXT_PUBLIC_I18N_BASE_URL }}
NUXT_API_URL: ${{ vars.NUXT_API_URL }}
STUDIO_GITHUB_CLIENT_ID: ${{ vars.STUDIO_GITHUB_CLIENT_ID }}
STUDIO_GITHUB_CLIENT_SECRET: ${{ vars.STUDIO_GITHUB_CLIENT_SECRET }}
NUXT_DISCORD_USER_ID: ${{ secrets.NUXT_DISCORD_USER_ID }}
- name: Publish to Cloudflare Pages
NUXT_WAKATIME_CODING: ${{ secrets.NUXT_WAKATIME_CODING }}
NUXT_WAKATIME_EDITORS: ${{ secrets.NUXT_WAKATIME_EDITORS }}
NUXT_WAKATIME_LANGUAGES: ${{ secrets.NUXT_WAKATIME_LANGUAGES }}
NUXT_WAKATIME_OS: ${{ secrets.NUXT_WAKATIME_OS }}
NUXT_WAKATIME_USER_ID: ${{ secrets.NUXT_WAKATIME_USER_ID }}
NUXT_STATUS_PAGE: ${{ secrets.NUXT_STATUS_PAGE }}
STUDIO_GITHUB_CLIENT_ID: ${{ secrets.STUDIO_GITHUB_CLIENT_ID }}
STUDIO_GITHUB_CLIENT_SECRET: ${{ secrets.STUDIO_GITHUB_CLIENT_SECRET }}
- name: Publish to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy --branch=${{ env.BRANCH_NAME }}
command: deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
- name: Discord Notification
uses: sarisia/actions-status-discord@v1
if: always() # S'exécute même si le build plante
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: "Déploiement Portfolio"
description: |
Build terminé sur la branche **${{ github.ref_name }}**.
Commit: `${{ github.sha }}` par ${{ github.actor }}.
nofail: false
nodetail: false
image: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
username: "GitHub Actions"

View File

@@ -1,9 +1,5 @@
<script setup lang="ts">
import { socials } from '~~/types'
const { t } = useI18n({
useScope: 'local'
})
</script>
<template>
@@ -17,7 +13,7 @@ const { t } = useI18n({
</div>
<div class="space-y-4">
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<h1>{{ t('find') }}</h1>
<h1>Find me on:</h1>
<div class="flex gap-2 flex-wrap">
<HomeLink
v-for="social in [...socials].sort((a, b) => a.label.localeCompare(b.label))"
@@ -30,7 +26,7 @@ const { t } = useI18n({
</div>
</div>
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<h1>{{ t('email') }}</h1>
<h1>Or send me an email:</h1>
<div class="flex">
<HomeLink
blanked
@@ -41,31 +37,7 @@ const { t } = useI18n({
</div>
</div>
<div class="mt-8 w-full flex justify-center text-xs">
{{
t('copyright', {
date: new Date().getFullYear()
})
}}
© {{ new Date().getFullYear() }} Arthur Danjou. All rights reserved.
</div>
</footer>
</template>
<i18n lang="json">
{
"en": {
"find": "Find me on:",
"email": "Or send me an email:",
"copyright": "© {date} Arthur Danjou. All rights reserved."
},
"fr": {
"find": "Retrouvez-moi sur :",
"email": "Ou envoyez-moi un email :",
"copyright": "© {date} Arthur Danjou. Tous droits réservés."
},
"es": {
"find": "Encuéntrame en :",
"email": "O envíame un mail",
"copyright": "2024 Arthur Danjour. Todos los derechos reservados."
}
}
</i18n>

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import { navs, socials } from '~~/types'
const { locale, t } = useI18n()
const openContactDrawer = ref(false)
const router = useRouter()
defineShortcuts({
@@ -22,17 +20,17 @@ const socialsList = [
<template>
<header class="flex md:items-center justify-between my-8 gap-2">
<NuxtLinkLocale
<NuxtLink
class="handwriting text-xl sm:text-3xl text-nowrap gap-2 font-bold duration-300 text-neutral-600 hover:text-black dark:text-neutral-400 dark:hover:text-white"
to="/"
>
Arthur Danjou
</NuxtLinkLocale>
</NuxtLink>
<nav class="flex gap-2 items-center justify-end flex-wrap">
<UTooltip
v-for="nav in navs"
:key="nav.label.en"
:text="nav.label[locale]"
:key="nav.label"
:text="nav.label"
:delay-duration="4"
>
<UButton
@@ -47,7 +45,7 @@ const socialsList = [
</UTooltip>
<UTooltip
:delay-duration="4"
:text="t('status')"
text="Status Page"
>
<UButton
icon="i-ph-warning-duotone"
@@ -73,7 +71,7 @@ const socialsList = [
>
<UTooltip
:kbds="['C']"
:text="t('contact.button')"
text="Contact Me"
:delay-duration="4"
class="cursor-pointer"
>
@@ -86,11 +84,6 @@ const socialsList = [
/>
</UTooltip>
</UDropdownMenu>
<USeparator
orientation="vertical"
class="h-6"
/>
<LangSwitcher />
</nav>
</header>
</template>
@@ -122,29 +115,3 @@ const socialsList = [
}
}
</style>
<i18n lang="json">
{
"en": {
"status": "status page",
"contact": {
"button": "contact me",
"title": "Contact me"
}
},
"fr": {
"status": "page de statut",
"contact": {
"button": "me contacter",
"title": "Me contacter"
}
},
"es": {
"status": "página de estado",
"contact": {
"button": "contactame",
"title": "Contactame"
}
}
}
</i18n>

View File

@@ -1,66 +0,0 @@
<script lang="ts" setup>
const openSelectMenu = ref(false)
const { locale, setLocale, locales, t } = useI18n()
const currentLocale = computed(() => locales.value.filter(l => l.code === locale.value)[0])
const lang = ref(locale.value)
watch(lang, () => changeLocale(lang.value))
async function changeLocale(newLocale: string) {
document.body.style.animation = 'switch-on .2s'
await new Promise(resolve => setTimeout(resolve, 200))
await setLocale(newLocale as 'en' | 'fr' | 'es')
document.body.style.animation = 'switch-off .5s'
await new Promise(resolve => setTimeout(resolve, 200))
document.body.style.animation = ''
}
defineShortcuts({
l: () => lang.value = currentLocale.value!.code === 'en' ? 'fr' : currentLocale.value!.code === 'fr' ? 'es' : 'en'
})
</script>
<template>
<ClientOnly>
<ThemeSwitcher />
<UTooltip
:kbds="['L']"
:text="t('language')"
class="cursor-pointer"
:delay-duration="4"
:content="{
align: 'center',
side: 'right',
sideOffset: 8
}"
>
<USelect
v-model="lang"
v-model:open="openSelectMenu"
:items="locales"
size="sm"
:leading-icon="(currentLocale!.icon as string)"
label-key="label"
value-key="code"
variant="soft"
@update:model-value="changeLocale"
/>
</UTooltip>
</ClientOnly>
</template>
<i18n lang="json">
{
"en": {
"language": "change language"
},
"fr": {
"language": "changer de langue"
},
"es": {
"language": "cambiar idioma"
}
}
</i18n>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
const { t } = useI18n()
const colorMode = useColorMode()
const nextTheme = computed(() => (colorMode.value === 'dark' ? 'light' : 'dark'))
@@ -58,7 +57,7 @@ defineShortcuts({
<template>
<UTooltip
:kbds="['T']"
:text="t('theme')"
text="switch theme"
class="cursor-pointer"
:delay-duration="4"
>
@@ -73,20 +72,6 @@ defineShortcuts({
</UTooltip>
</template>
<i18n lang="json">
{
"en": {
"theme": "switch theme"
},
"fr": {
"theme": "changer de thème"
},
"es": {
"theme": "cambiar tema"
}
}
</i18n>
<style>
::view-transition-old(root),
::view-transition-new(root) {

View File

@@ -1,9 +1,6 @@
<script lang="ts" setup>
import type { UseTimeAgoMessages } from '@vueuse/core'
import type { Activity } from '~~/types'
import { activityMessages, IDEs } from '~~/types'
const { locale, locales, t } = useI18n({ useScope: 'local' })
import { IDEs } from '~~/types'
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'),
{ lazy: true }
@@ -25,8 +22,6 @@ const codingActivity = computed(() => {
: codingActivities.value[0]
})
const currentLocale = computed(() => locales.value.find((l: { code: string }) => l.code === locale.value)?.code ?? 'en')
const isActive = computed(() => {
const act = codingActivity.value
if (!act) return false
@@ -59,13 +54,11 @@ const formattedActivity = computed<FormattedActivity>(() => {
? (details.charAt(0).toUpperCase() + details.slice(1).replace('Workspace:', '').trim())
: ''
const stateWord = (state && state.split(' ').length >= 2 ? state.split(' ')[1] : t('secret')) as string
const ago = useTimeAgo(timestamps.start, {
messages: activityMessages[locale.value as keyof typeof activityMessages] as UseTimeAgoMessages
}).value
const stateWord = (state && state.split(' ').length >= 2 ? state.split(' ')[1] : 'Secret project') as string
const ago = useTimeAgo(timestamps.start).value
const formatDate = (date: number, format: string) =>
useDateFormat(date, format, { locales: currentLocale.value }).value
useDateFormat(date, format).value
return {
name,
@@ -93,7 +86,7 @@ const editorIcon = computed(() => {
v-if="formattedActivity"
class="flex items-start gap-2 mt-4"
>
<UTooltip :text="isActive ? t('tooltip.online') : t('tooltip.idling')">
<UTooltip :text="isActive ? 'I\'m online 👋' : 'I\'m sleeping 😴'">
<div class="relative flex h-3 w-3 mt-2">
<div
v-if="isActive"
@@ -106,42 +99,33 @@ const editorIcon = computed(() => {
</div>
</UTooltip>
<i18n-t
<div
v-if="isActive"
keypath="working"
tag="div"
>
<template #state>
<strong>{{ formattedActivity.state.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ') }}</strong>
</template>
<template #project>
<i>{{ formattedActivity.project.replace('Editing', '') }}</i>
</template>
<template #editor>
<span class="space-x-1">
<UIcon
:name="editorIcon"
size="16"
/>
<strong>{{ formattedActivity.name }}</strong>
</span>
</template>
<template #start>
<strong>{{ formattedActivity.start.ago }}</strong>
</template>
<template #format>
<strong>{{ formattedActivity.start.formatted.date }}</strong> {{ t('separator') }}
<strong>{{ formattedActivity.start.formatted.time }}</strong>
</template>
</i18n-t>
<i18n-t
v-else
keypath="idling"
tag="div"
class="space-x-1"
>
<template #editor>
<span>
I'm actually working on
<strong>{{ formattedActivity.state.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ') }}</strong>,
editing <i>{{ formattedActivity.project.replace('Editing', '') }}</i>, using
<span class="space-x-1">
<UIcon
:name="editorIcon"
size="16"
/>
<strong>{{ formattedActivity.name }}</strong>
</span>.
I've started <strong>{{ formattedActivity.start.ago }}</strong>, on
<strong>{{ formattedActivity.start.formatted.date }}</strong>
at <strong>{{ formattedActivity.start.formatted.time }}</strong>.
</span>
</div>
<div
v-else
class="space-x-1"
>
<span>
I'm idling on my computer with
<span class="space-x-1">
<UIcon
:name="editorIcon"
@@ -149,72 +133,23 @@ const editorIcon = computed(() => {
/>
<strong>{{ formattedActivity.name }}</strong>
</span>
</template>
</i18n-t>
running in background.
</span>
</div>
</div>
<div
v-else
class="my-5 flex md:items-start gap-2"
>
<UTooltip :text="t('tooltip.offline')">
<UTooltip text="I'm offline 🫥">
<div class="relative flex h-3 w-3 mt-2">
<div class="relative cursor-not-allowed inline-flex rounded-full h-3 w-3 bg-red-500" />
</div>
</UTooltip>
<i18n-t
keypath="offline"
tag="p"
class="not-prose"
>
<template #maths>
<i>{{ t('maths') }}</i>
</template>
</i18n-t>
<p class="not-prose">
I'm currently offline. Come back later to see what I'm working on. <i>I am probably doing some maths or sleeping.</i>
</p>
</div>
</ClientOnly>
</template>
<i18n lang="json">
{
"en": {
"offline": "I'm currently offline. Come back later to see what I'm working on. {maths}",
"working": "I'm actually working on {state}, editing {project}, using {editor}. I've started {start}, the {format}.",
"idling": "I'm idling on my computer with {editor} running in background.",
"maths": "I am probably doing some maths or sleeping.",
"tooltip": {
"online": "I'm online 👋",
"offline": "I'm offline 🫥",
"idling": "I'm sleeping 😴"
},
"separator": "at",
"secret": "Secret Project"
},
"fr": {
"offline": "Je suis actuellement hors ligne. Revenez plus tard pour voir sur quoi je travaille. {maths}",
"working": "Je travaille actuellement sur {state}, éditant {project}, en utilisant {editor}. J'ai commencé {start}, le {format}.",
"idling": "Je suis en veille sur mon ordinateur avec {editor} en arrière-plan.",
"maths": "Je suis probablement en train de faire des maths ou en train de dormir.",
"tooltip": {
"online": "Je suis connecté 👋",
"offline": "Je suis déconnecté 🫥",
"idling": "Je dors 😴"
},
"separator": "à",
"secret": "Projet Secret"
},
"es": {
"offline": "Ahora mismo estoy desconectado. Vuelve más tarde para ver en lo que estoy trabajando. {maths}",
"working": "Estoy trabajando en {state}, editando {project}, y utilizando {editor}. He empezado {start}, el {format}.",
"idling": "Estoy en reposo en mi ordenador con {editor} en segundo plano.",
"maths": "Estoy probablemente haciendo matemáticas o durmiendo.",
"tooltip": {
"online": "Estoy conectado 👋",
"offline": "Estoy desconectado 🫥",
"idling": "Estoy durmiendo 😴"
},
"separator": "a",
"secret": "Proyecto Secreto"
}
}
</i18n>

View File

@@ -1,8 +1,5 @@
<script lang="ts" setup>
const { width } = useWindowSize()
const { t } = useI18n({
useScope: 'local'
})
</script>
<template>
@@ -15,21 +12,7 @@ const { t } = useI18n({
class="transform -rotate-12 duration-300 animate-wave"
name="i-ph-hand-pointing-duotone"
/>
<p>{{ t('quote') }}</p>
<p>Hover the bold texts to find out more about me.</p>
</div>
</ClientOnly>
</template>
<i18n lang="json">
{
"en": {
"quote": "Hover the bold texts to find out more about me."
},
"fr": {
"quote": "Survolez les textes en gras pour en savoir plus sur moi."
},
"es": {
"quote": "Pase el cursor sobre los textos en negrita para obtener más información sobre mí."
}
}
</i18n>

View File

@@ -1,9 +1,3 @@
<script lang="ts" setup>
const { t } = useI18n({
useScope: 'local'
})
</script>
<template>
<div class="mt-4">
<div class="float-left flex items-center mr-2 mt-1">
@@ -19,21 +13,7 @@ const { t } = useI18n({
</ClientOnly>
</div>
<p class="not-prose">
{{ t('quote') }}
Hello everyone! Thanks for visiting my portfolio. Please leave whatever you like to say, such as suggestions, appreciations, questions or anything!
</p>
</div>
</template>
<i18n lang="json">
{
"en": {
"quote": "Hello everyone! Thanks for visiting my portfolio. Please leave whatever you like to say, such as suggestions, appreciations, questions or anything!"
},
"fr": {
"quote": "Bonjour tout le monde ! Merci de visiter mon portfolio. N'hésitez pas à laisser ce que vous avez à dire, comme des suggestions, des appréciations, des questions ou autre chose !"
},
"es": {
"quote": "Hola a todos ! Muchas gracias por visitar mi portfolio. No dudes en dejar cualquier comentario, como sugerencias, apreciaciones. preguntas, o cualquier cosa !"
}
}
</i18n>

View File

@@ -2,83 +2,40 @@
import type { Stats } from '~~/types'
import { usePrecision } from '@vueuse/math'
const { locale, locales, t } = useI18n({
useScope: 'local'
})
const currentLocale = computed(() => locales.value.find(l => l.code === locale.value))
const { data: stats } = await useAsyncData<Stats>('stats', () => $fetch('/api/stats'))
const time = useTimeAgo(new Date(stats.value!.coding.data.range.start)).value.split(' ')[0]
const date = useDateFormat(new Date(stats.value!.coding.data.range.start), 'DD MMMM YYYY', { locales: currentLocale.value?.code ?? 'en' })
const hours = usePrecision(stats.value!.coding.data.grand_total.total_seconds_including_other_language / 3600, 0)
const time = useTimeAgo(new Date(stats.value?.coding.range.start ?? 0)).value.split(' ')[0]
const date = useDateFormat(new Date(stats.value?.coding.range.start ?? 0), 'DD MMMM YYYY')
const hours = usePrecision((stats.value?.coding.grand_total.total_seconds_including_other_language ?? 0) / 3600, 0)
const editors = computed(() => stats.value?.editors.slice(0, 3).map(editor => `${editor.name} (${editor.percent}%)`).join(', '))
const os = computed(() => stats.value?.os.slice(0, 2).map(os => `${os.name} (${os.percent}%)`).join(', '))
const languages = computed(() => stats.value?.languages.slice(0, 3).map(language => `${language.name} (${language.percent}%)`).join(', '))
</script>
<template>
<ClientOnly>
<i18n-t
v-if="stats && stats.coding && stats.editors && stats.os && stats.languages && time && date && hours"
keypath="stats"
tag="p"
<div
v-if="time && date && hours && stats"
class="space-y-1"
>
<template #time>
{{ time }}
</template>
<template #date>
<p>
I collect some data for {{ time }} years, started the
<HoverText
:hover="t('tooltip.date')"
hover="That was so long ago 🫣"
:text="date"
/>
</template>
<template #hours>
/>.
I've coded for a total of
<HoverText
:hover="t('tooltip.hours')"
hover="That's a lot 😮"
:text="hours"
/>
</template>
<template #editors>
{{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(t('separator')) }}
</template>
<template
v-if="stats.os.data[0]"
#os
>
{{ stats.os.data[0].name }} ({{ stats.os.data[0].percent }}%)
</template>
<template #languages>
{{
stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(t('separator'))
}}
</template>
</i18n-t>
hours.
</p>
<p>
My best editors are {{ editors || 'N/A' }}. My best OS is {{ os || 'N/A' }}. My top languages are
{{ languages || 'N/A' }}.
</p>
</div>
</ClientOnly>
</template>
<i18n lang="json">
{
"en": {
"stats": "I collect some data for {time} years, started the {date}. I've coded for a total of {hours} hours. My best editors are {editors}. My best OS is {os}. My top languages are {languages}.",
"separator": " and ",
"tooltip": {
"date": "That was so long ago 🫣",
"hours": "That's a lot 😮"
}
},
"fr": {
"stats": "Je collecte des données depuis {time} ans, commencé le {date}. J'ai codé un total de {hours} heures. Mes meilleurs éditeurs sont {editors}. Mon meilleur OS est {os}. Mes langages préférés sont {languages}.",
"separator": " et ",
"tooltip": {
"date": "C'était il y a si longtemps 🫣",
"hours": "C'est beaucoup 😮"
}
},
"es": {
"stats": "Recopilo datos desde hace {time} años, empecé el {date}. He programado durante un total de {hours} horas. Mis mejores editores son {editors}. Mi mejor OS es {os}. Y mis lenguajes favoritos son {languages}.",
"separator": " y ",
"tooltip": {
"date": "hace tato tiempo…🫣",
"hours": "es mucho 😮"
}
}
}
</i18n>

View File

@@ -1,148 +0,0 @@
<script lang="ts" setup>
import { getTextFromMessage } from '@nuxt/ui/utils/ai'
import { DefaultChatTransport } from 'ai'
import { Chat } from '@ai-sdk/vue'
const toast = useToast()
const input = ref('')
const { t, locale } = useI18n({ useScope: 'local' })
const chat = new Chat({
transport: new DefaultChatTransport({
api: '/api/chat',
body: () => ({
messages: chat.messages,
lang: locale.value
})
}),
onError(error) {
toast.add({
title: 'Error',
description: error.message,
color: 'red'
})
}
})
function handleSubmit(e: Event) {
e.preventDefault()
if (input.value.trim()) {
chat.sendMessage({ text: input.value })
input.value = ''
}
}
const clipboard = useClipboard()
const actions = ref([
{
label: 'Copy to clipboard',
icon: 'i-lucide-copy',
onClick: (message: never) => clipboard.copy(getTextFromMessage(message))
}
])
</script>
<template>
<main>
<UDashboardPanel
:ui="{ body: 'p-0 sm:p-0' }"
class="max-h-[calc(100vh-28rem)]! min-h-[calc(100vh-28rem)]! border-none"
:resizable="false"
>
<template #body>
<UContainer>
<UChatMessages
:actions
:messages="chat.messages"
:status="chat.status"
:user="{
variant: 'solid'
}"
:assistant="{
variant: 'naked'
}"
:auto-scroll="{
color: 'neutral',
variant: 'outline'
}"
>
<template #indicator>
<UButton
class="px-0"
color="neutral"
variant="link"
loading
loading-icon="i-lucide-loader"
label="Thinking..."
/>
</template>
<template #content="{ message }">
<MDC
:value="getTextFromMessage(message)"
:cache-key="message.id"
class="*:first:mt-0 *:last:mb-0"
/>
</template>
</UChatMessages>
</UContainer>
</template>
<template #footer>
<UContainer>
<ClientOnly>
<UCard
variant="outline"
class="rounded-xl"
:ui="{ body: 'p-2 sm:p-2' }"
>
<UChatPrompt
v-model="input"
:placeholder="t('placeholder')"
:error="chat.error"
@submit="handleSubmit"
>
<UChatPromptSubmit
:status="chat.status"
color="neutral"
submitted-color="neutral"
submitted-variant="subtle"
submitted-icon="i-lucide-square"
streaming-color="neutral"
streaming-variant="subtle"
streaming-icon="i-lucide-square"
error-color="red"
error-variant="soft"
error-icon="i-lucide-rotate-ccw"
@stop="chat.stop()"
@reload="chat.regenerate()"
/>
</UChatPrompt>
</UCard>
</ClientOnly>
</UContainer>
</template>
</UDashboardPanel>
</main>
</template>
<style scoped>
</style>
<i18n lang="json">
{
"en": {
"placeholder": "Type your message...",
"thinking": "Thinking..."
},
"fr": {
"placeholder": "Tapez votre message...",
"thinking": "Réflexion..."
},
"es": {
"placeholder": "Escribe tu mensaje...",
"thinking": "Pensando..."
}
}
</i18n>

View File

@@ -1,15 +1,11 @@
<script lang="ts" setup>
const { locale } = useI18n()
useSeoMeta({
title: 'Arthur Danjou - AI enjoyer and Maths student',
description: 'Developer enjoying Artificial Intelligence and Machine Learning. Mathematics Student at Paris Dauphine-PSL University specialised in Statistics'
})
const { data: page } = await useAsyncData(`/home/${locale.value}`, () => {
return queryCollection('main').path(`/home/${locale.value}`).first()
}, {
watch: [locale]
const { data: page } = await useAsyncData('index', () => {
return queryCollection('index').first()
})
</script>

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

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

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

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

View File

@@ -5,8 +5,6 @@
"": {
"name": "artsite",
"dependencies": {
"@ai-sdk/mcp": "^0.0.12",
"@ai-sdk/vue": "^2.0.115",
"@libsql/client": "^0.15.15",
"@modelcontextprotocol/sdk": "^1.25.1",
"@nuxt/content": "3.9.0",
@@ -16,42 +14,30 @@
"@nuxtjs/mdc": "^0.19.1",
"@vueuse/core": "^14.1.0",
"@vueuse/math": "^14.1.0",
"ai": "5.0.115",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-alpha.4",
"vue": "3.5.26",
"vue-router": "4.6.4",
"workers-ai-provider": "^2.0.0",
"zod": "^4.2.1",
},
"devDependencies": {
"@iconify-json/devicon": "1.2.53",
"@iconify-json/devicon": "1.2.54",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/twemoji": "^1.2.4",
"@iconify-json/twemoji": "1.2.5",
"@iconify-json/vscode-icons": "1.2.37",
"@types/node": "25.0.3",
"@vueuse/nuxt": "14.1.0",
"eslint": "9.39.2",
"typescript": "^5.9.3",
"vue-tsc": "3.1.8",
"vue-tsc": "3.2.1",
"wrangler": "4.56.0",
},
},
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6fHjDfCbjfj4vyMExuLei7ir2///E5sNwNZaobdJsJIxJjDSsjzSLGO/aUI7p9eOnB8XctDrDSF5ilwDGpi6eg=="],
"@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hyf31U2CmgGexqOLgLfno525pjbqidJLu9pU+XcEwW/PkMcfTFuRq1iD3wbqtAmURRW0qJITiKV+in1B4I23gA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/vue": ["@ai-sdk/vue@2.0.115", "", { "dependencies": { "@ai-sdk/provider-utils": "3.0.19", "ai": "5.0.115", "swrv": "^1.0.4" }, "peerDependencies": { "vue": "^3.3.4", "zod": "^3.25.76 || ^4.1.8" }, "optionalPeers": ["vue", "zod"] }, "sha512-6ozPym9+VLNpw+NgJGBb4C6cTGWUSQhdOYCCG9EZe7UAOE9e5pY+5FcfS0qqxhwh9xRVgJ/fJvfN3F1vhyMi6A=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
@@ -248,7 +234,7 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iconify-json/devicon": ["@iconify-json/devicon@1.2.53", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-uMBNP2Xmyre5w0XmQ3Sye64d6NokSiee+zb51AMqREjnUfM0s47bA+PO9Sy4M2GBjs6NIovi0rJQucWMpylMcQ=="],
"@iconify-json/devicon": ["@iconify-json/devicon@1.2.54", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-I188YZ/t+SF2bfZnrRjwmdr/nzUNXuc/S3uvsZF0LG6atOuGtKk7KcmK/NUaTB2JAIeM1hsD+7wieYBObI8O4Q=="],
"@iconify-json/logos": ["@iconify-json/logos@1.2.10", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qxaXKJ6fu8jzTMPQdHtNxlfx6tBQ0jXRbHZIYy5Ilh8Lx9US9FsAdzZWUR8MXV8PnWTKGDFO4ZZee9VwerCyMA=="],
@@ -256,7 +242,7 @@
"@iconify-json/ph": ["@iconify-json/ph@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw=="],
"@iconify-json/twemoji": ["@iconify-json/twemoji@1.2.4", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-REYJeXhzaLktNe32DxJJf3t65sYC5KO9K0Jh+RApXRBAo1/IB+jBqd8rny2sXci+wtQLBEfD4z4AGCLBrTMGWA=="],
"@iconify-json/twemoji": ["@iconify-json/twemoji@1.2.5", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-uKpuIEV0v6K5BW3Mjdyl+XKFVAbbcPxAgifKvEMtZoUZB5+YiY5zaMm2uNNCxyXzAWU9yNLlj41WU6/mvgALsw=="],
"@iconify-json/vscode-icons": ["@iconify-json/vscode-icons@1.2.37", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-HLRdU6nZks4N8x3JYz6j+b3+hcUCvYvlTLwGzM3xyXfTJyDSA2cAdWcEXfoA4hQMJGA+zCDSPAWFelFptH5Kbw=="],
@@ -898,17 +884,15 @@
"@vercel/nft": ["@vercel/nft@0.30.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="],
"@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.53" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w=="],
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5", "@rolldown/pluginutils": "^1.0.0-beta.50", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, "sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A=="],
"@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="],
"@volar/language-core": ["@volar/language-core@2.4.27", "", { "dependencies": { "@volar/source-map": "2.4.27" } }, "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ=="],
"@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="],
"@volar/source-map": ["@volar/source-map@2.4.27", "", {}, "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg=="],
"@volar/typescript": ["@volar/typescript@2.4.26", "", { "dependencies": { "@volar/language-core": "2.4.26", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA=="],
"@volar/typescript": ["@volar/typescript@2.4.27", "", { "dependencies": { "@volar/language-core": "2.4.27", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg=="],
"@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="],
@@ -934,7 +918,7 @@
"@vue/devtools-shared": ["@vue/devtools-shared@8.0.5", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg=="],
"@vue/language-core": ["@vue/language-core@3.1.8", "", { "dependencies": { "@volar/language-core": "2.4.26", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw=="],
"@vue/language-core": ["@vue/language-core@3.2.1", "", { "dependencies": { "@volar/language-core": "2.4.27", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A=="],
"@vue/reactivity": ["@vue/reactivity@3.5.26", "", { "dependencies": { "@vue/shared": "3.5.26" } }, "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ=="],
@@ -976,8 +960,6 @@
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ai": ["ai@5.0.115", "", { "dependencies": { "@ai-sdk/gateway": "2.0.22", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aVuHx0orGxXvhyL7oXUyW8TnWQE6Al8f3Bl6VZjz0WHMV+WaACHPkSyvQ3wje2QCUGzdl5DBF5d+OaXyghPQyg=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
@@ -1670,8 +1652,6 @@
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="],
"json-schema-to-typescript-lite": ["json-schema-to-typescript-lite@15.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^14.1.1", "@types/json-schema": "^7.0.15" } }, "sha512-5mMORSQm9oTLyjM4mWnyNBi2T042Fhg1/0gCIB6X8U/LVpM2A+Nmj2yEyArqVouDmFThDxpEXcnTgSrjkGJRFA=="],
@@ -2418,8 +2398,6 @@
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
"swrv": ["swrv@1.1.0", "", { "peerDependencies": { "vue": ">=3.2.26 < 4" } }, "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ=="],
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
@@ -2608,7 +2586,7 @@
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
"vue-tsc": ["vue-tsc@3.1.8", "", { "dependencies": { "@volar/typescript": "2.4.26", "@vue/language-core": "3.1.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA=="],
"vue-tsc": ["vue-tsc@3.2.1", "", { "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
@@ -2630,8 +2608,6 @@
"workerd": ["workerd@1.20251217.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251217.0", "@cloudflare/workerd-darwin-arm64": "1.20251217.0", "@cloudflare/workerd-linux-64": "1.20251217.0", "@cloudflare/workerd-linux-arm64": "1.20251217.0", "@cloudflare/workerd-windows-64": "1.20251217.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-s3mHDSWwHTduyY8kpHOsl27ZJ4ziDBJlc18PfBvNMqNnhO7yBeemlxH7bo7yQyU1foJrIZ6IENHDDg0Z9N8zQA=="],
"workers-ai-provider": ["workers-ai-provider@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.7" } }, "sha512-AoGGy8aOR3lzCzRouSxA6mgrCKuZfrnzxvOHHy9kOzwz4Mm4Hb55a/9G8zz+v0I/mn8bYLs/4I6JABSl/zXJ7w=="],
"wrangler": ["wrangler@4.56.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.7.13", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20251217.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251217.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251217.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Nqi8duQeRbA+31QrD6QlWHW3IZVnuuRxMy7DEg46deUzywivmaRV/euBN5KKXDPtA24VyhYsK7I0tkb7P5DM2w=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -2678,8 +2654,6 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -2844,9 +2818,7 @@
"@vue/devtools-core/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.24", "", { "dependencies": { "@vue/compiler-core": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw=="],
"@vue/language-core/@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
"@vue/language-core/@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
"@vue/reactivity/@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
@@ -3028,6 +3000,8 @@
"unplugin-vue-components/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"unplugin-vue-router/@vue/language-core": ["@vue/language-core@3.1.8", "", { "dependencies": { "@volar/language-core": "2.4.26", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw=="],
"unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
@@ -3048,6 +3022,10 @@
"vue/@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
"vue-component-meta/@volar/typescript": ["@volar/typescript@2.4.26", "", { "dependencies": { "@volar/language-core": "2.4.26", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA=="],
"vue-component-meta/@vue/language-core": ["@vue/language-core@3.1.8", "", { "dependencies": { "@volar/language-core": "2.4.26", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw=="],
"youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -3298,8 +3276,6 @@
"@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
"@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -3528,6 +3504,12 @@
"unplugin-vue-components/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"unplugin-vue-router/@vue/language-core/@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="],
"unplugin-vue-router/@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.24", "", { "dependencies": { "@vue/compiler-core": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw=="],
"unplugin-vue-router/@vue/language-core/@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
"unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"vaul-vue/@vueuse/core/@types/web-bluetooth": ["@types/web-bluetooth@0.0.20", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
@@ -3610,6 +3592,14 @@
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"vue-component-meta/@volar/typescript/@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="],
"vue-component-meta/@vue/language-core/@volar/language-core": ["@volar/language-core@2.4.26", "", { "dependencies": { "@volar/source-map": "2.4.26" } }, "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.24", "", { "dependencies": { "@vue/compiler-core": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw=="],
"vue-component-meta/@vue/language-core/@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
"@nuxt/devtools-kit/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"@nuxt/devtools-wizard/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
@@ -3630,8 +3620,6 @@
"@nuxt/nitro-server/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A=="],
"@vue/language-core/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"clipboardy/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"nuxt-studio/@nuxtjs/mdc/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="],
@@ -3678,6 +3666,10 @@
"reka-ui/@vueuse/shared/vue/@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
"unplugin-vue-router/@vue/language-core/@volar/language-core/@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="],
"unplugin-vue-router/@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig=="],
"vaul-vue/reka-ui/@tanstack/vue-virtual/@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
"vaul-vue/reka-ui/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
@@ -3750,6 +3742,12 @@
"vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="],
"vue-component-meta/@volar/typescript/@volar/language-core/@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="],
"vue-component-meta/@vue/language-core/@volar/language-core/@volar/source-map": ["@volar/source-map@2.4.26", "", {}, "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig=="],
"@nuxt/eslint/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
"reka-ui/@vueuse/core/vue/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.24", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig=="],
@@ -3784,8 +3782,12 @@
"reka-ui/@vueuse/shared/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.24", "", { "dependencies": { "@vue/compiler-dom": "3.5.24", "@vue/shared": "3.5.24" } }, "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg=="],
"unplugin-vue-router/@vue/language-core/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"vaul-vue/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@nuxt/eslint/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"reka-ui/@vueuse/core/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],

View File

@@ -1,8 +1,114 @@
import { defineCollection } from '@nuxt/content'
import { defineCollection, z } from '@nuxt/content'
export const collections = {
main: defineCollection({
index: defineCollection({
type: 'page',
source: 'home/*.md'
source: 'index.md'
}),
projects: defineCollection({
type: 'data',
source: 'projects/*.md',
schema: z.object({
slug: z.string(),
title: z.string(),
type: z.string().optional(),
description: z.string(),
publishedAt: z.string(),
readingTime: z.number().optional(),
tags: z.array(z.string()),
cover: z.string().optional(),
favorite: z.boolean().optional(),
status: z.string().optional(),
emoji: z.string().optional()
})
}),
uses: defineCollection({
type: 'page',
source: 'uses.md'
}),
skills: defineCollection({
type: 'data',
source: 'skills.json',
schema: z.object({
description: z.string(),
skills: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
items: z.array(z.object({
name: z.string(),
icon: z.string().optional()
}))
}))
})
}),
experiences: defineCollection({
type: 'data',
source: 'experiences/*.md',
schema: z.object({
title: z.string(),
type: z.string().optional(),
company: z.string(),
companyUrl: z.string().url().optional(),
startDate: z.string(),
endDate: z.string().optional(),
duration: z.string().optional(),
location: z.string(),
description: z.string(),
tags: z.array(z.string()).optional(),
emoji: z.string().optional()
})
}),
education: defineCollection({
type: 'data',
source: 'education/*.md',
schema: z.object({
title: z.string(),
type: z.string().optional(),
degree: z.string().optional(),
institution: z.string(),
startDate: z.string(),
endDate: z.string().optional(),
duration: z.string().optional(),
location: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
emoji: z.string().optional()
})
}),
contact: defineCollection({
type: 'data',
source: 'contact.json',
schema: z.object({
contact: 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(),
username: z.string().optional(),
priority: z.number().optional()
}))
})
}),
hobbies: defineCollection({
type: 'page',
source: 'hobbies.md'
}),
languages: defineCollection({
type: 'data',
source: 'languages.json',
schema: z.object({
languages: z.array(z.object({
name: z.string(),
level: z.string(),
proficiency: z.string()
}))
})
}),
profile: defineCollection({
type: 'page',
source: 'profile.md'
})
}

77
content/contact.json Normal file
View File

@@ -0,0 +1,77 @@
{
"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
}
]
}

View File

@@ -0,0 +1,15 @@
---
title: Bachelor's Degree in Mathematics
type: Bachelor
institution: Paris-Saclay University
location: Paris, France
startDate: 2021-09
endDate: 2024-06
duration: 3 years
description: Comprehensive study of pure and applied mathematics, providing a strong foundation in mathematical theory and problem-solving.
tags:
- Mathematics
- Physics
- Computer Science
emoji: 🎓
---

16
content/education/m1.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: Master's Degree in Applied Mathematics (Year 1)
type: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2023-09
endDate: 2024-06
duration: 1 year
description: First year of specialized study in applied mathematics, combining theoretical knowledge with practical applications in data science, optimization, and machine learning.
tags:
- Applied Mathematics
- Data Science
- Machine Learning
- Optimization
emoji: 📚
---

16
content/education/m2.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: Master's Degree in Applied Mathematics (Year 2)
type: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2024-09
endDate: 2025-10
duration: 1 year
description: Second year of advanced study in applied mathematics with focus on specialized topics, research projects, and professional applications in industry and research.
tags:
- Applied Mathematics
- Advanced Machine Learning
- Data Engineering
- Research
emoji: 🏆
---

View File

@@ -0,0 +1,18 @@
---
title: Freelancer
type: Freelance
company: ArtDanjProduction
companyUrl: https://go.arthurdanjou.fr/website
location: Paris, France
startDate: 2022-02
endDate: null
duration: 3+ years
description: As a freelancer, I designed, developed, and maintained various personal projects, exploring new programming languages and technologies. I also write documentation and articles related to my projects, fix bugs, and ensure their smooth operation in production. Additionally, I manage my Proxmox and Docker-based homelab, hosting multiple services, and set up network infrastructure to optimize performance and stability.
tags:
- Java
- TypeScript
- HomeLab
- Docker
- Self-Hosted
emoji: 💼
---

View File

@@ -0,0 +1,18 @@
---
title: Junior Developer
type: Employment
company: Erisium
companyUrl: https://x.com/Erisium
location: Remote, France
startDate: 2021-09
endDate: 2022-09
duration: 1 year
description: At Erisium, one of the most popular French-speaking Minecraft servers, I worked as a Junior Java Developer. I developed mini-games designed by the game design team, and worked on backend infrastructure optimizations to handle several thousand concurrent players. This experience allowed me to solve a wide range of complex bugs and to grow within a collaborative, high-performance technical environment.
tags:
- Java
- Docker
- Minecraft
- Backend
- Game Development
emoji: 🎮
---

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 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

@@ -0,0 +1,17 @@
---
title: Sales Assistant II
type: Student Job
company: Picard Surgelés
companyUrl: https://picard.fr
location: Paris, France
startDate: 2022-09
endDate: 2024-10
duration: 2+ years
description: As part of my student job at Picard, I welcomed and advised customers while handling product restocking and in-store deliveries. I placed orders according to overall stock, monitored the cold chain, and maintained freezer cleanliness, ensuring product quality and safety.
tags:
- Sales
- Customer Service
- Retail
- Team Work
emoji: 🛒
---

View File

@@ -0,0 +1,18 @@
---
title: Data Analyst Intern
type: Internship
company: Sevetys
companyUrl: https://sevetys.fr
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.
tags:
- Python
- PySpark
- Microsoft Azure
- Data Engineering
- Data Quality
emoji: 📊
---

28
content/hobbies.md Normal file
View File

@@ -0,0 +1,28 @@
---
title: "Balance and Drive: Beyond the Data"
description: Exploring my passions outside of data science and machine learning engineering that fuel my creativity and performance.
---
While my passion for data science and machine learning engineering is at the core of what I do, I am convinced that personal balance is the key to performance and creativity. Outside of my technical projects, I nurture this balance through several key interests.
## ⚽ Sports & Team Dynamics
**Rugby and volleyball** are fundamental to my equilibrium. These team sports have taught me the importance of collective strategy, communication, and physical commitment. The dynamics of working under pressure with diverse personalities mirrors the collaborative nature of ML projects.
As a long-time supporter of **PSG** in football, I appreciate the tactical analysis, performance management, and data-driven decision-making that occurs at the highest level of sport—much like optimizing complex systems.
## 🎵 Music & Creative Problem-Solving
**Music** is my creative outlet and a tool for different thinking patterns. Training my ear to recognize harmony and structure translates directly to identifying elegant solutions in system design, architecture, and mathematical modeling. It reinforces my belief that great engineering, like great music, requires both technical precision and artistic intuition.
## 🌍 Travel & Cultural Adaptation
**Travel** provides essential perspective and adaptability. Having discovered highly diverse cultures since childhood—from Egypt and South Africa to Thailand and the United States—has profoundly shaped my curiosity and flexibility. This exposure to different ways of thinking and problem-solving is crucial in a constantly evolving field like AI, where understanding multiple perspectives can lead to breakthrough insights.
## 🏎️ Motorsports & Optimization
As a **Formula 1 enthusiast**, I'm fascinated by the pursuit of pure performance and optimization under constraints. F1 represents the pinnacle of real-time, data-driven strategy, where decisions are made in milliseconds based on telemetry, weather, and tactical considerations. This mirrors my approach to machine learning: extracting maximum value from available resources and constraints.
## 🎯 Balance as Performance
These passions are not escapes—they're integral to my professional success. They reinforce my commitment to continuous improvement, adaptability, and the drive to progress. It is this balance that allows me to approach every new challenge with motivation, energy, and fresh perspective.

View File

@@ -1,34 +0,0 @@
---
title: Arthur Danjou • Mathematics Lover and IA Enthusiast
description: I'm Arthur, a Mathematics lover and IA enthusiast. I'm currently
studying at the University of Paris Dauphine-PSL. I'm passionate about
Mathematics, Computer Science, and Artificial Intelligence.
---
Hola ! Soy :home-name, estudiante de matemáticas especializado en Estadística en la Universidad Paris-Dauphine, en Francia.
Con una :hover-text{hover="la tecnología avanza demasiado rápido 🤯" position="top" text="comprensión profunda"} de las tecnologías emergentes, me sitúo en el centro de un ámbito en plena expansión. Mi formación en :hover-text{hover="las matemáticas son mi mayor pasión Σ" position="right" text="matemáticas"} me permite comprender ampliamente los conceptos y las teorías que gobiernan las dichas :hover-text{hover="mi segunda pasión 📲" text="tecnologías"} y también poder concebirlas de manera eficaz.
Como ingeniero de software y estudiante de matemáticas, mi :hover-text{hover="mi mochila de conocimientos 🎒" text="conocimientos"} cubre
:prose-icon[TypeScript]{color="blue" icon="i-logos:typescript-icon"},
:prose-icon[Vue]{color="green" icon="i-logos:vue"},
:prose-icon[Nuxt]{color="emerald" icon="i-logos:nuxt-icon"},
:prose-icon[Adonis]{color="purple" icon="i-logos:adonisjs-icon"},
:prose-icon[Java]{color="red" icon="i-logos:java"},
:prose-icon[Python]{color="amber" icon="i-logos:python"},
:prose-icon[R]{color="blue" icon="i-logos:r-lang"},
esto me permite :hover-text{hover="entender rápidamente la complejidad de los proyectos 🏎️" text="comprender"} las diferentes necesidades de los proyectos matemáticos, y proponer las mejores soluciones.
Utilizo herramientas como
:prose-icon[scikit-learn]{color="orange" icon="devicon-scikitlearn"} para el aprendizaje supervisado,
:prose-icon[pandas]{color="blue" icon="i-logos:pandas-icon"} para la manipulación eficiente de datos,
:prose-icon[NumPy]{color="indigo" icon="i-logos:numpy"} para el cálculo científico, y
:prose-icon[TensorFlow]{color="orange" icon="i-logos:tensorflow"} así como :prose-icon[PyTorch]{color="orange" icon="i-logos:pytorch-icon"} para construir y entrenar modelos de aprendizaje profundo. También he aprendido otras tecnologías importantes como :prose-icon[Docker]{color="sky" icon="i-logos:docker-icon"},
:prose-icon[Redis]{color="red" icon="i-logos:redis"},
:prose-icon[MySQL]{color="zinc" icon="i-logos:mysql-icon"} y
:prose-icon[Git]{color="orange" icon="i-logos:git-icon"} que :hover-text{hover="todas estas tecnologías se complementan 📎" text="completan"} mis competencias.
Estas herramientas me permiten ir desde :hover-text{hover="Exploración, limpieza, reorganización…" text="preparación de datos"} hasta :hover-text{hover="Entrenamiento, evaluación, despliegue" text="despliegue"} de modelos en entornos reales, siempre con rigurosidad estadística y un enfoque en el rendimiento. Me apasiona la IA y la :hover-text{hover="La IA es el futuro de la tecnología 🤖" text="ciencia de datos"} .
Estoy :hover-text{hover="me gusta estar siempre al día 🖥️" position="top" text="constantemente"} aprendiendo cosas nuevas, desde la tecnología hasta las finanzas, pasando por el emprendimiento. Me gusta :hover-text{hover="me encanta compartir y ayudar a los demás 🫂" text="compartir"} mis conocimientos y aprender nuevos teoremas y tecnologías. Soy una persona :hover-text{hover="busco cosas nuevas que descubrir 🔍" text="curiosa"} y con el deseo de seguir aprendiendo y creciendo a lo largo de toda mi vida.
Aparte de la programación, me gusta el :hover-text{hover="el deporte me permite gastar mi energía 🏋️‍♂️" text="deporte"} y :hover-text{hover="los viajes me permiten desconectar ✈️" text="viajar"} . Mi pasión, mi compromiso y mis ganas de aprender y mejorar son las cualidades que me permiten triunfar en mi :hover-text{hover="carrera que ya he empezado, y le queda mucho para terminar 😎" text="carrera"} y en mis :hover-text{hover="solo me quedan 2 años de estudios 💪" text="estudios"} .

View File

@@ -1,46 +0,0 @@
---
title: Arthur Danjou • Mathematics Lover and IA Enthusiast
description: I'm Arthur, a Mathematics lover and IA enthusiast. I'm currently
studying at the University of Paris Dauphine-PSL. I'm passionate about
Mathematics, Computer Science, and Artificial Intelligence.
---
Salut, je suis :home-name, étudiant en mathématiques spécialisé en Statistiques à l'Université Paris-Dauphine en France.
Avec une :hover-text{hover="La technologie évolue beaucoup trop vite 🤯" position="top" text="compréhension profonde"} des technologies émergentes, je suis au cœur d'un domaine en pleine expansion. Ma formation en :hover-text{hover="Les
mathématiques sont ma principale passion ∑" position="right" text="mathématiques"} me donne une longueur d'avance pour
comprendre les concepts et les théories qui sous-tendent ces :hover-text{hover="Ma deuxième passion 📱" text="technologies"} et à les concevoir efficacement.
En tant qu'ingénieur logiciel et étudiant en mathématiques, mon :hover-text{hover="Mon sac de connaissances 🎒" text="expertise"} couvre
:prose-icon[TypeScript]{color="blue" icon="i-logos:typescript-icon"},
:prose-icon[Vue]{color="green" icon="i-logos:vue"},
:prose-icon[Nuxt]{color="emerald" icon="i-logos:nuxt-icon"},
:prose-icon[Adonis]{color="purple" icon="i-logos:adonisjs-icon"},
:prose-icon[Java]{color="red" icon="i-logos:java"},
:prose-icon[Python]{color="amber" icon="i-logos:python"},
:prose-icon[R]{color="blue" icon="i-logos:r-lang"},
ce qui me permet de :hover-text{hover="Comprendre rapidement la complexité des projets 🏎️" text="comprendre"} les
différents besoins des projets mathématiques et de proposer les meilleures solutions.
J'utilise des outils comme
:prose-icon[scikit-learn]{color="orange" icon="i-devicon-scikitlearn"} pour l'apprentissage supervisé,
:prose-icon[pandas]{color="blue" icon="i-logos:pandas-icon"} pour la manipulation efficace de données,
:prose-icon[NumPy]{color="indigo" icon="i-logos:numpy"} pour le calcul scientifique, et
:prose-icon[TensorFlow]{color="orange" icon="i-logos:tensorflow"} ainsi que :prose-icon[PyTorch]{color="orange" icon="i-logos:pytorch-icon"} pour la création et l'entraînement de modèles profonds.
J'ai également appris d'autres technologies importantes, telles que
:prose-icon[Docker]{color="sky" icon="i-logos:docker-icon"},
:prose-icon[Redis]{color="red" icon="i-logos:redis"},
:prose-icon[MySQL]{color="zinc" icon="i-logos:mysql-icon"} et
:prose-icon[Git]{color="orange" icon="i-logos:git-icon"} pour :hover-text{hover="Toutes ces technologies se complètent 🔗" text="compléter"} mes connaissances.
Ma maîtrise de ces bibliothèques me permet de passer de la :hover-text{hover="Exploration, nettoyage, mise en forme…" text="préparation des données"} jusqu'au :hover-text{hover="Entraînement, évaluation, déploiement" text="déploiement"} de modèles dans des environnements réels, toujours avec rigueur statistique et souci de performance.
Je suis passionné par l'IA et la :hover-text{hover="L'IA est l'avenir de la technologie 🤖" text="science des données"} .
Je suis :hover-text{hover="Je dois toujours chercher à être à jour 🖥️" position="top" text="constamment"} dans
l'apprentissage de nouvelles choses, de la technologie à la finance en passant par l'entrepreneuriat. J'aime
:hover-text{hover="J'aime partager et aider les autres 🫂" text="partager"} mes connaissances et apprendre de nouveaux
théorèmes et technologies. Je suis une personne :hover-text{hover="Je cherche à découvrir de nouvelles choses" text="curieuse"} et désireuse de continuer à apprendre et à grandir tout au long de ma vie.
Outre la programmation, j'aime le :hover-text{hover="Le sport me permet de dépenser de l'énergie 🏋️‍♂️" text="sport"} et :hover-text{hover="Les voyages me libèrent et m'évadent ✈️" text="voyager"} .
Ma passion, mon engagement et mon envie d'apprendre et de progresser sont les qualités qui me permettent de réussir dans
ma :hover-text{hover="Carrière déjà commencée et loin d'être terminée 😎" text="carrière"} et mes :hover-text{hover="Il
ne me reste que 2 ans d'études 💪" text="études"} .

19
content/languages.json Normal file
View File

@@ -0,0 +1,19 @@
{
"languages": [
{
"name": "French",
"level": "Native",
"proficiency": "C2"
},
{
"name": "English",
"level": "Fluent",
"proficiency": "C1"
},
{
"name": "Spanish",
"level": "Intermediate",
"proficiency": "A2"
}
]
}

98
content/profile.md Normal file
View File

@@ -0,0 +1,98 @@
---
title: Arthur Danjou - Data Science & Applied AI Student.
description: Profile of Arthur Danjou, a Data Science and Applied AI student at Paris Dauphine-PSL University, highlighting his skills, experience, and career aspirations.
---
# Arthur Danjou
**Data Science & Applied AI**
Rigorous, curious, and motivated, I put my skills in statistics, machine learning, and applied artificial intelligence to work on concrete and innovative projects[cite: 9]. Passionate about mathematical modelling and the deployment of AI solutions, I enjoy transforming theory into measurable results[cite: 10].
## 📞 Contact & Links
* **Email:** `arthur.danjou@dauphine.eu`
* **Portfolio:** `go.arthurdanjou.fr/portfolio`
* **GitHub:** `go.arthurdanjou.fr/github`
* **LinkedIn:** `go.arthurdanjou.fr/linkedin`
## 📍 Location
* **Current:** Paris, France
* **Timezone:** Europe/Paris (CET/CEST)
* **Remote:** Open (confirmed by "REMOTE" experience)
## 🗓️ Availability
* **Status:** Available for a final-year internship.
* **Start Date:** **April 2026**.
* **Contract Types:** Internship (priority).
## 🎯 Career Goals
* To join a team of Data Scientists or AI Researchers to deepen my knowledge.
* Contribute to high-impact projects.
* **Prepare for a future doctorate in artificial intelligence**.
* Become an expert in Machine Learning Engineering and MLOps.
* Combine mathematical rigor (from education) with practical engineering solutions (from experience).
## 💼 Work Preferences
* **Target Roles:** Data Scientist, AI Researcher.
* **Industries:** AI/ML, Data Science, Health, DevOps.
* **Work Style:** Remote, Hybrid.
* **Company Size:** Startup, Scale-up, Enterprise.
## 🏆 Notable Achievements
* Administration of a personal home lab (servers, databases, storage, backups) for MLOps experimentation since 2022.
* Implemented daily data cleaning pipelines on Azure with PySpark, enhancing data quality by 20-30% at Sevetys.
* Design, development, and maintenance of web, data, and cloud projects (Python, TypeScript, Nuxt 3) via ArtDanj Production.
* Developed an automated monthly data quality report (completeness, consistency) for business teams.
## 📚 Education
* **Master's in Applied Mathematics** (M280) - Paris Dauphine-PSL University (2023-2025)
- Specialization: Data Science & Applied Artificial Intelligence
- Focus: Deep Learning, Probabilistic Models, Statistical Learning Theory
* **Bachelor's in Applied Mathematics** - Paris Dauphine-PSL University (2020-2023)
- Foundation in linear algebra, probability, statistics, and numerical analysis
## 🔐 Certifications & Competencies
* Advanced Python & Data Science practices
* MLOps & Cloud Infrastructure (Azure, Docker, Kubernetes)
* Full-stack Web Development (Nuxt 3, TypeScript, Vue.js)
* Linux System Administration & Networking
## 🎓 Research & Academic Interests
* Machine Learning Engineering and deployment pipelines
* Probabilistic inference and Bayesian methods
* Statistical learning theory and generalization bounds
* Deep Learning architectures for structured data
* Data quality and governance in production systems
* Former rugby team captain, participating in the French school championships.
## 📚 Education & Core Competencies
### Paris Dauphine-PSL University (MSc)
* **Dual Expertise:** Theory & Practice in Advanced Data Science and AI Systems.
* **Core Theoretical Focus:** Strong background in statistical modeling and advanced AI principles (Advanced Machine Learning, Learning Theory, Clustering, Deep Learning, Climate Risk Modeling).
* **Practical Skills:** Hands-on experience in NLP, Reinforcement Learning, Generative AI, Data Quality, and Data Visualisation.
* **Key Courses (M1/M2):** Supervised Statistical Learning, Generalised Linear Models (GLMs), Monte Carlo Methods, Data Analysis, Non-parametric Statistics, Time Series, Numerical Optimisation.
* **Active Engagement:** Scheduled participation in the Natixis and DIRISI Hackathons.
### Technical Skillset
* **Programming:** Python, R, TypeScript, Java, Git, LaTeX.
* **Libraries & Frameworks:** NumPy, Pandas, Scikit-learn, PyTorch, Matplotlib, Seaborn.
* **Databases:** SQL, Redis.
* **Cloud & DevOps:** Proxmox, Docker, Azure, Linux, SysAdmin.
### Statistical & AI Modeling
* **Multivariate Data Analysis:** Principal Component Analysis (PCA), Correspondence Analysis (CA), clustering techniques, correlation analysis.
* **Supervised Learning:** k-NN, linear and logistic regression, CNN, Naive Bayes.
* **Unsupervised Learning:** Clustering, dimensionality reduction, k-means, CNN.
* **IA & Modern Models:** Natural Language Processing, Transformers, Large Language Models, AI agents, embeddings, and fine-tuning.

View File

@@ -0,0 +1,38 @@
---
slug: artchat
title: ArtChat - Portfolio & Blog
type: Personal Project
description: My personal space on the web — a portfolio, a blog, and a digital lab where I showcase my projects, write about topics I care about, and experiment with design and web technologies.
publishedAt: 2024-06-01
readingTime: 1
cover: artchat/cover.png
favorite: true
status: Active
tags:
- Vue.js
- Nuxt
- TypeScript
- Tailwind CSS
- Web
emoji: 🌍
---
[**ArtChat**](https://go.arthurdanjou.fr/website) is my personal space on the web — a portfolio, a blog, and a digital lab where I showcase my projects, write about topics I care about, and experiment with design and web technologies.
It's designed to be fast, accessible, and fully responsive. The site also serves as a playground to explore and test modern frontend tools.
## ⚒️ Tech Stack
- **UI** → [Vue.js](https://vuejs.org/): A progressive JavaScript framework for building interactive interfaces.
- **Framework** → [Nuxt](https://nuxt.com/): A powerful full-stack framework built on Vue, perfect for modern web apps.
- **Content System** → [Nuxt Content](https://content.nuxtjs.org/): File-based CMS to manage blog posts and pages using Markdown.
- **Design System** → [Nuxt UI](https://nuxtui.com/): Fully styled, customizable UI components tailored for Nuxt.
- **CMS & Editing** → [Nuxt Studio](https://nuxt.studio): Visual editing and content management integrated with Nuxt Content.
- **Language** → [TypeScript](https://www.typescriptlang.org/): A statically typed superset of JavaScript.
- **Styling** → [Sass](https://sass-lang.com/) & [Tailwind CSS](https://tailwindcss.com/): Utility-first CSS framework enhanced with SCSS flexibility.
- **Deployment** → [NuxtHub](https://hub.nuxt.com/): Cloudflare-powered platform for fast, scalable Nuxt app deployment.
- **Package Manager** → [pnpm](https://pnpm.io/): A fast, disk-efficient package manager for JavaScript/TypeScript projects.
- **Linter** → [ESLint](https://eslint.org/): A tool for identifying and fixing problems in JavaScript/TypeScript code.
- **ORM** → [Drizzle ORM](https://orm.drizzle.team/): A lightweight, type-safe ORM for TypeScript.
- **Validation** → [Zod](https://zod.dev/): A TypeScript-first schema declaration and validation library with full static type inference.
- **Deployment** → [NuxtHub](https://hub.nuxt.com/): A platform to deploy and scale Nuxt apps globally with minimal latency and full-stack capabilities.

View File

@@ -0,0 +1,30 @@
---
slug: arthome
title: ArtHome - Browser Homepage
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
cover: arthome/cover.png
status: Active
tags:
- Nuxt
- Vue.js
- Web
- Productivity
emoji: 🏡
---
[ArtHome](https://go.arthurdanjou.fr/arthome) is a customizable browser homepage that lets you organize all your favorite links in one place.
Create categories and tabs to group your shortcuts, personalize them with icons and colors, and make the page private if you want to keep your links just for yourself. The interface is clean, responsive, and works across all modern browsers.
## 🛠️ Built with
- [Nuxt](https://nuxt.com): An open-source framework for building performant, full-stack web applications with Vue.
- [NuxtHub](https://hub.nuxt.com): A Cloudflare-powered platform to deploy and scale Nuxt apps globally with minimal latency and full-stack capabilities.
- [NuxtUI](https://ui.nuxt.com): A sleek and flexible component library that helps create beautiful, responsive UIs for Nuxt applications.
- [ESLint](https://eslint.org): A linter that identifies and fixes problems in your JavaScript/TypeScript code.
- [Drizzle ORM](https://orm.drizzle.team/): A lightweight, type-safe ORM built for TypeScript, designed for simplicity and performance.
- [Zod](https://zod.dev/): A TypeScript-first schema declaration and validation library with full static type inference.
- and a lot of ❤️

View File

@@ -0,0 +1,46 @@
---
slug: artlab
title: ArtLab - Personal HomeLab
type: Infrastructure Project
description: A personal homelab environment where I deploy, test, and maintain self-hosted services with privacy-focused networking through VPN and Cloudflare Tunnels.
publishedAt: 2025-09-04
readingTime: 1
cover: artlab/cover.png
favorite: true
status: Active
tags:
- Docker
- Proxmox
- HomeLab
- Self-Hosted
- Infrastructure
emoji: 🏡
---
[**ArtLab**](https://go.arthurdanjou.fr/status) is my personal homelab, where I experiment with self-hosting and automation.
My homelab is a self-hosted environment where I deploy, test, and maintain personal services. Everything is securely exposed **only through a private VPN** using [Tailscale](https://tailscale.com/), ensuring encrypted, access-controlled connections across all devices.
For selected services, I also use **Cloudflare Tunnels** to enable secure external access without opening ports or exposing my public IP.
## 🛠️ Running Services
- **MinIO**: S3-compatible object storage for static files and backups.
- **Immich**: Self-hosted photo management platform — a private alternative to Google Photos.
- **Jellyfin**: Media server for streaming movies, shows, and music.
- **Portainer & Docker**: Container orchestration and service management.
- **Traefik**: Reverse proxy and automatic HTTPS with Let's Encrypt.
- **Homepage**: A sleek dashboard to access and monitor all services.
- **Proxmox**: Virtualization platform used to manage VMs and containers.
- **Uptime Kuma**: Self-hosted uptime monitoring.
- **Home Assistant**: Smart home automation and device integration.
- **AdGuard Home**: Network-wide ad and tracker blocking via DNS.
- **Beszel**: Self-hosted, lightweight alternative to Notion for notes and knowledge management.
- **Palmr**: Personal logging and journaling tool.
## 🖥️ Hardware
- **Beelink EQR6**: AMD Ryzen mini PC, main server host.
- **TP-Link 5-port Switch**: Network connectivity for all devices.
- **UGREEN NASync DXP4800 Plus**: 4-bay NAS, currently populated with 2 × 8TB drives for storage and backups.
This homelab is a sandbox for DevOps experimentation, infrastructure reliability, and privacy-respecting digital autonomy.

View File

@@ -0,0 +1,75 @@
---
slug: artstudies
title: ArtStudies - Academic Projects Collection
type: Academic Project
description: A curated collection of mathematics and data science projects developed during my academic journey, spanning Bachelor's and Master's studies.
publishedAt: 2023-09-01
readingTime: 1
favorite: true
status: Active
tags:
- Python
- R
- Data Science
- Machine Learning
- Mathematics
emoji: 🎓
---
# ArtStudies
[ArtStudies Projects](https://github.com/ArthurDanjou/artstudies) is a curated collection of academic projects completed throughout my mathematics studies. The repository showcases work in both _Python_ and _R_, focusing on mathematical modeling, data analysis, and numerical methods.
The projects are organized into two main sections:
- **L3** Third year of the Bachelor's degree in Mathematics
- **M1** First year of the Master's degree in Mathematics
- **M2** Second year of the Master's degree in Mathematics
## 📁 File Structure
- `L3`
- `Analyse Matricielle`
- `Analyse Multidimensionnelle`
- `Calculs Numériques`
- `Équations Différentielles`
- `Méthodes Numériques`
- `Probabilités`
- `Projet Numérique`
- `Statistiques`
- `M1`
- `Data Analysis`
- `General Linear Models`
- `Monte Carlo Methods`
- `Numerical Methods`
- `Numerical Optimization`
- `Portfolio Management`
- `Statistical Learning`
- `M2`
- `Data Visualisation`
- `Deep Learning`
- `Linear Models`
- `Machine Learning`
- `VBA`
- `SQL`
## 🛠️ Technologies & Tools
- [Python](https://www.python.org): A high-level, interpreted programming language, widely used for data science, machine learning, and scientific computing.
- [R](https://www.r-project.org): A statistical computing environment, perfect for data analysis and visualization.
- [Jupyter](https://jupyter.org): Interactive notebooks combining code, results, and rich text for reproducible research.
- [Pandas](https://pandas.pydata.org): A data manipulation library providing data structures and operations for manipulating numerical tables and time series.
- [NumPy](https://numpy.org): Core package for numerical computing with support for large, multi-dimensional arrays and matrices.
- [SciPy](https://www.scipy.org): A library for advanced scientific computations including optimization, integration, and signal processing.
- [Scikit-learn](https://scikit-learn.org): A robust library offering simple and efficient tools for machine learning and statistical modeling, including classification, regression, and clustering.
- [TensorFlow](https://www.tensorflow.org): A comprehensive open-source framework for building and deploying machine learning and deep learning models.
- [Keras](https://keras.io): A high-level neural networks API, running on top of TensorFlow, designed for fast experimentation.
- [Matplotlib](https://matplotlib.org): A versatile plotting library for creating high-quality static, animated, and interactive visualizations in Python.
- [Plotly](https://plotly.com): An interactive graphing library for creating dynamic visualizations in Python and R.
- [Seaborn](https://seaborn.pydata.org): A statistical data visualization library built on top of Matplotlib, providing a high-level interface for drawing attractive and informative graphics.
- [RMarkdown](https://rmarkdown.rstudio.com): A dynamic tool for combining code, results, and narrative into high-quality documents and presentations.
- [FactoMineR](https://factominer.free.fr/): An R package focused on multivariate exploratory data analysis (e.g., PCA, MCA, CA).
- [ggplot2](https://ggplot2.tidyverse.org): A grammar-based graphics package for creating complex and elegant visualizations in R.
- [RShiny](https://shiny.rstudio.com): A web application framework for building interactive web apps directly from R.
- and my 🧠.

View File

@@ -0,0 +1,57 @@
---
slug: bikes-glm
title: Generalized Linear Models for Bikes Prediction
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
tags:
- R
- Statistics
- Data Analysis
- GLM
- Mathematics
emoji: 🚲
---
# Generalized Linear Models for Bikes Prediction
## Overview
This project was completed as part of the **Generalized Linear Models** course at Paris-Dauphine PSL University. The objective was to develop and compare statistical models to predict the number of bicycle rentals in a bike-sharing system based on various environmental and temporal characteristics.
## 📊 Project Objectives
- Determine the best predictive model for bicycle rental counts
- Analyze the impact of various features (temperature, humidity, wind speed, seasonality, etc.)
- Apply and evaluate different generalized linear modeling techniques
- Validate model assumptions and performance metrics
## 🔍 Methodology
The study employs rigorous statistical approaches including:
- **Exploratory Data Analysis (EDA)** - Understanding feature distributions and relationships
- **Model Comparison** - Testing multiple GLM families (Poisson, Negative Binomial, Gaussian)
- **Feature Selection** - Identifying the most influential variables
- **Model Diagnostics** - Validating assumptions and checking residuals
- **Cross-validation** - Ensuring robust performance estimates
## 📁 Key Findings
The analysis identified critical factors influencing bike-sharing demand:
- Seasonal patterns and weather conditions
- Temperature and humidity effects
- Holiday and working day distinctions
- Time-based trends and cyclical patterns
## 📚 Resources
- **Code Repository**: [GLM Bikes Code](https://go.arthurdanjou.fr/glm-bikes-code)
- **Full Report**: See embedded PDF below
## 📄 Detailed Report
<iframe src="/projects/bikes-glm/Report.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -0,0 +1,47 @@
---
slug: breast-cancer
title: Breast Cancer Detection
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
tags:
- Python
- Machine Learning
- Data Science
- Classification
- Healthcare
emoji: 💉
---
The project was carried out as part of the `Statistical Learning` course at Paris-Dauphine PSL University. Its objective is to identify the most effective model for predicting or explaining the presence of breast cancer based on a set of biological and clinical features.
This project aims to develop and evaluate several supervised classification models to predict the presence of breast cancer based on biological features extracted from the Breast Cancer Coimbra dataset, provided by the UCI Machine Learning Repository.
The dataset contains 116 observations divided into two classes:
- 1: healthy individuals (controls)
- 2: patients diagnosed with breast cancer
There are 9 explanatory variables, including clinical measurements such as age, insulin levels, leptin, insulin resistance, among others.
The project follows a comparative approach between several algorithms:
- Logistic Regression
- k-Nearest Neighbors (k-NN)
- Naive Bayes
- Artificial Neural Network (MLP with a 16-8-1 architecture)
Model evaluation is primarily based on the F1-score, which is more suitable in a medical context where identifying positive cases is crucial. Particular attention was paid to stratified cross-validation and to handling class imbalance, notably through the use of class weights and regularization techniques (L2, early stopping).
This project illustrates a concrete application of data science techniques to a public health issue, while implementing a rigorous methodology for supervised modeling.
You can find the code here: [Breast Cancer Detection](https://go.arthurdanjou.fr/breast-cancer-detection-code)
<iframe src="/projects/breast-cancer/report.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -0,0 +1,157 @@
---
slug: dropout-reduces-underfitting
title: Dropout Reduces Underfitting
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: Active
tags:
- Python
- TensorFlow
- Machine Learning
- Deep Learning
- Research
emoji: 🔬
---
📉 [Dropout Reduces Underfitting](https://github.com/arthurdanjou/dropoutreducesunderfitting): Reproduction & Analysis
![TensorFlow](https://img.shields.io/badge/TensorFlow-2.x-orange.svg)
![Python](https://img.shields.io/badge/Python-3.8%2B-blue.svg)
![License](https://img.shields.io/badge/License-MIT-green.svg)
> **Study and reproduction of the paper:** Liu, Z., et al. (2023). *Dropout Reduces Underfitting*. arXiv:2303.01500.
The paper is available at: [https://arxiv.org/abs/2303.01500](https://arxiv.org/abs/2303.01500)
This repository contains a robust and modular implementation in **TensorFlow/Keras** of **Early Dropout** and **Late Dropout** strategies. The goal is to verify the hypothesis that dropout, traditionally used to reduce overfitting, can also combat underfitting when applied solely during the initial training phase.
## 🎯 Scientific Objectives
The study aims to validate the three operating regimes of Dropout described in the paper:
1. **Early Dropout** (Targeting Underfitting): Active only during the initial phase to reduce gradient variance and align their direction, allowing for better final optimization.
2. **Late Dropout** (Targeting Overfitting): Disabled at the start to allow rapid learning, then activated to regularize final convergence.
3. **Standard Dropout**: Constant rate throughout training (Baseline).
4. **No Dropout**: Control experiment without dropout.
## 🛠️ Technical Architecture
Unlike naive Keras callback implementations, this project uses a **dynamic approach via the TensorFlow graph** to ensure the dropout rate is properly updated on the GPU without model recompilation.
### Key Components
* **`DynamicDropout`**: A custom layer inheriting from `keras.layers.Layer` that reads its rate from a shared `tf.Variable`.
* **`DropoutScheduler`**: A Keras `Callback` that drives the rate variable based on the current epoch and the chosen strategy (`early`, `late`, `standard`).
* **`ExperimentPipeline`**: An orchestrator class that handles data loading (MNIST, CIFAR-10, Fashion MNIST), model creation (Dense or CNN), and execution of comparative benchmarks.
## File Structure
```
.
├── README.md # This documentation file
├── Dropout reduces underfitting.pdf # Original research paper
├── pipeline.py # Main experiment pipeline
├── pipeline.ipynb # Jupyter notebook for experiments
├── pipeline_mnist.ipynb # Jupyter notebook for MNIST experiments
├── pipeline_cifar10.ipynb # Jupyter notebook for CIFAR-10 experiments
├── pipeline_cifar100.ipynb # Jupyter notebook for CIFAR-100 experiments
├── pipeline_fashion_mnist.ipynb # Jupyter notebook for Fashion MNIST experiments
├── requirements.txt # Python dependencies
├── .python-version # Python version specification
└── uv.lock # Dependency lock file
```
## 🚀 Installation
```bash
# Clone the repository
git clone https://github.com/arthurdanjou/dropoutreducesunderfitting.git
cd dropoutreducesunderfitting
```
## Install dependencies
```bash
pip install tensorflow numpy matplotlib seaborn scikit-learn
```
## 📊 Usage
The main notebook pipeline.ipynb contains all necessary code. Here is how to run a typical experiment via the pipeline API.
1. Initialization
Choose your dataset (cifar10, fashion_mnist, mnist) and architecture (cnn, dense).
```python
from pipeline import ExperimentPipeline
# Fashion MNIST is recommended to observe underfitting/overfitting nuances
exp = ExperimentPipeline(dataset_name="fashion_mnist", model_type="cnn")
```
2. Learning Curves Comparison
Compare training dynamics (Loss & Accuracy) of the three strategies.
```python
exp.compare_learning_curves(
modes=["standard", "early", "late"],
switch_epoch=10, # The epoch where dropout state changes
rate=0.4, # Dropout rate
epochs=30
)
```
3. Ablation Studies
Study the impact of the "Early" phase duration or Dropout intensity.
```python
# Impact of the switch epoch on final performance
exp.compare_switch_epochs(
switch_epochs=[5, 10, 15, 20],
modes=["early"],
rate=0.4,
epochs=30
)
# Impact of the dropout rate
exp.compare_drop_rates(
rates=[0.2, 0.4, 0.6],
modes=["standard", "early"],
switch_epoch=10,
epochs=25
)
```
4. Data Regimes (Data Scarcity)
Verify the paper's hypothesis that Early Dropout shines on large datasets (or limited models) while Standard Dropout protects small datasets.
```python
# Training on 10%, 50% and 100% of the dataset
exp.run_dataset_size_comparison(
fractions=[0.1, 0.5, 1.0],
modes=["standard", "early"],
rate=0.3,
switch_epoch=10
)
```
## 📈 Expected Results
According to the paper, you should observe:
- Early Dropout: Higher initial Loss, followed by a sharp drop after the switch_epoch, often reaching a lower minimum than Standard Dropout (reduction of underfitting).
- Late Dropout: Rapid rise in accuracy at the start (potential overfitting), then stabilized by the activation of dropout.
## 📝 Authors
- [Arthur Danjou](https://github.com/ArthurDanjou)
- [Alexis Mathieu](https://github.com/Alex6535)
- [Axelle Meric](https://github.com/AxelleMeric)
- [Philippine Quellec](https://github.com/Philippine35890)
- [Moritz Von Siemens](https://github.com/MoritzSiem)
M.Sc. Statistical and Financial Engineering (ISF) - Data Science Track at Université Paris-Dauphine PSL
Based on the work of Liu, Z., et al. (2023). Dropout Reduces Underfitting.

View File

@@ -0,0 +1,54 @@
---
slug: loan-ml
title: Machine Learning for Loan Prediction
type: Academic Project
description: Predicting loan approval and default risk using machine learning classification techniques.
publishedAt: 2025-01-24
readingTime: 2
status: Completed
tags:
- Python
- Machine Learning
- Classification
- Data Science
- Finance
emoji: 💰
---
# Machine Learning for Loan Prediction
## Overview
This project focuses on building machine learning models to predict loan approval outcomes and assess default risk. The objective is to develop robust classification models that can effectively identify creditworthy applicants.
## 📊 Project Objectives
- Build and compare multiple classification models for loan prediction
- Identify key factors influencing loan approval decisions
- Evaluate model performance using appropriate metrics
- Optimize model parameters for better predictive accuracy
## 🔍 Methodology
The study employs various machine learning approaches:
- **Exploratory Data Analysis (EDA)** - Understanding applicant characteristics and patterns
- **Feature Engineering** - Creating meaningful features from raw data
- **Model Comparison** - Testing multiple algorithms (Logistic Regression, Random Forest, Gradient Boosting, etc.)
- **Hyperparameter Tuning** - Optimizing model performance
- **Cross-validation** - Ensuring robust generalization
## 📁 Key Findings
[To be completed with your findings]
## 📚 Resources
- **Code Repository**: [Add link to your code]
- **Dataset**: [Add dataset information]
- **Full Report**: See embedded PDF below
## 📄 Detailed Report
<iframe src="/projects/loan-ml/Report.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -0,0 +1,31 @@
---
slug: monte-carlo-project
title: Monte Carlo Methods Project
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
tags:
- R
- Mathematics
- Statistics
- Monte Carlo
- Numerical Methods
emoji: 💻
---
This is the report for the Monte Carlo Methods Project. The project was done as part of the course `Monte Carlo Methods` at the Paris-Dauphine University. The goal was to implement different methods and algorithms using Monte Carlo methods in R.
Methods and algorithms implemented:
- Plotting graphs of functions
- Inverse c.d.f. Random Variation simulation
- Accept-Reject Random Variation simulation
- Random Variable simulation with stratification
- Cumulative density function
- Empirical Quantile Function
You can find the code here: [Monte Carlo Project Code](https://go.arthurdanjou.fr/monte-carlo-code)
<iframe src="/projects/monte-carlo-project/Report.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -0,0 +1,23 @@
---
slug: schelling-segregation-model
title: Schelling Segregation Model
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
tags:
- Python
- Data Visualization
- Statistics
- Modeling
- Mathematics
emoji: 📊
---
This is the French version of the report for the Schelling Segregation Model project. The project was done as part of the course `Projet Numérique` at the Paris-Saclay University. The goal was to implement the Schelling Segregation Model in Python and analyze the results using statistics and data visualization.
You can find the code here: [Schelling Segregation Model Code](https://go.arthurdanjou.fr/schelling-code)
<iframe src="/projects/schelling/Projet.pdf" width="100%" height="1000px">
</iframe>

View File

@@ -0,0 +1,31 @@
---
slug: sevetys
title: Data Engineer Internship at Sevetys
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
tags:
- Python
- PySpark
- Data Engineering
- Azure
- Big Data
emoji: 🐶
---
[Sevetys](https://sevetys.fr) is a leading French network of over 200 veterinary clinics, employing more than 1,300 professionals. Founded in 2017, the group provides comprehensive veterinary care for companion animals, exotic pets, and livestock, with services ranging from preventive medicine and surgery to cardiology, dermatology, and 24/7 emergency care.
Committed to digital innovation, Sevetys leverages centralized data systems to optimize clinic operations, improve patient data management, and enhance the overall client experience. This combination of medical excellence and operational efficiency supports veterinarians in delivering the highest quality care nationwide.
During my two-month internship as a Data Engineer, I focused primarily on cleaning and standardizing customer and patient data — a critical task, as this data is extensively used by clinics, Marketing, and Performance teams. Ensuring data quality was therefore essential to the company's operations.
Additionally, I took charge of revising and enhancing an existing data quality report designed to evaluate the effectiveness of my cleaning processes. The report encompassed 47 detailed metrics assessing data completeness and consistency, providing valuable insights that helped maintain high standards across the organization.
## ⚙️ Stack
- [Microsoft Azure Cloud](https://azure.microsoft.com/)
- [PySpark](https://spark.apache.org/docs/latest/api/python/)
- [Python](https://www.python.org/)
- [GitLab]()

110
content/skills.json Normal file
View File

@@ -0,0 +1,110 @@
{
"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": [
{
"id": "scientific-computing",
"name": "Scientific Computing & AI",
"description": "Core expertise in mathematics, statistics, and machine learning. Building and training neural networks, statistical models, and data science solutions.",
"items": [
{
"name": "Python",
"icon": "i-logos-python"
},
{
"name": "PyTorch",
"icon": "i-logos-pytorch-icon"
},
{
"name": "R Lang",
"icon": "i-logos-r-lang"
},
{
"name": "LaTeX",
"icon": "i-file-icons-latex"
},
{
"name": "Tensorflow",
"icon": "i-logos-tensorflow"
},
{
"name": "Scikit-Learn",
"icon": "i-devicon-scikitlearn"
},
{
"name": "Pandas & Numpy",
"icon": "i-devicon-pandas"
},
{
"name": "MatPlotLib",
"icon": "i-devicon-matplotlib"
}
]
},
{
"id": "data-engineering-mlops",
"name": "Data Engineering & MLOps",
"description": "Infrastructure, data pipelines, and production deployment. Managing databases, containerization, and scalable systems for ML models.",
"items": [
{
"name": "PostgreSQL",
"icon": "i-logos-postgresql"
},
{
"name": "MySQL",
"icon": "i-logos-mysql-icon"
},
{
"name": "Docker",
"icon": "i-logos-docker-icon"
},
{
"name": "Linux",
"icon": "i-logos-linux-tux"
},
{
"name": "Git",
"icon": "i-logos-git-icon"
},
{
"name": "Proxmox",
"icon": "i-devicon-proxmox-wordmark"
},
{
"name": "Redis",
"icon": "i-logos-redis"
},
{
"name": "Apache Spark (PySpark)",
"icon": "i-logos-apache-spark"
}
]
},
{
"id": "software-development",
"name": "Fullstack Development",
"description": "Web and backend development with modern frameworks. Building responsive UIs and scalable server-side applications.",
"items": [
{
"name": "TypeScript",
"icon": "i-logos-typescript-icon"
},
{
"name": "Vue.js & Nuxt",
"icon": "i-logos-nuxt-icon"
},
{
"name": "Java",
"icon": "i-logos-java"
},
{
"name": "TailwindCSS",
"icon": "i-logos-tailwindcss-icon"
},
{
"name": "AdonisJs",
"icon": "i-logos-adonisjs-icon"
}
]
}
]
}

79
content/uses.md Normal file
View File

@@ -0,0 +1,79 @@
---
title: What I Use
description: My favorite equipment, tools and software for productivity and development
---
# What I Use
This page documents all the tools, equipment and services I use daily for my work as a developer and my personal projects.
## 🖥️ Hardware
### Computers
- **Apple MacBook Pro 13'** - My main workstation with Apple M1 Chip and 16GB RAM, running macOS Sonoma
- **Custom Built Gaming PC** - A customized computer for gaming with Intel Core i5-10400F, 16GB DDR4, RTX 2060 and Windows 11
### Peripherals and Accessories
- **Apple AirPods Pro** - Probably my most used accessory after my phone and laptop. Excellent sound quality and very convenient
- **Apple iPad Air** - For reading books, watching movies, browsing the web, taking notes and writing equations during classes
- **Apple iPhone 14 Pro** - The best phone on the market in my opinion
- **SteelSeries Apex 9 TKL** - A compact and powerful TKL keyboard perfect for gamers and developers
- **Logitech G203 LightSync Black** - A classic gaming mouse with simple and clean design
### Apple Suite
- Using Mail, Calendar, Notes, Music and Reminders for my daily organization
---
## 💻 Development
### 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
- **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
- **Theme**: Catppuccin Macchiato - A community-driven pastel theme that strikes the balance between low and high contrast
- **Font**: JetBrains Mono
---
## 🛠️ Software and Tools
### Communication and Collaboration
- **Discord** - For chatting with my friends, clients and community members
- **Notion & Notion Calendar** - My all-in-one tool for note-taking, kanban boards, wikis, and drafts. Notion Calendar allows me to sync my databases with my calendar
### Navigation and System Tools
- **Firefox Browser** - My main browser for browsing, developer tools and the extension marketplace
- **Raycast** - An extensible launcher replacing Apple Spotlight. Allows me to complete tasks, calculate, share common links, and much more thanks to extensions
- **Warp** - A modern, Rust-based terminal reimagined with AI and collaborative tools for better productivity
---
## 🏠 Personal HomeLab
### Hardware Infrastructure
- **Beelink EQR6 AMD Ryzen** - The main server in my homelab running Proxmox to host self-hosted services, run Docker containers and test open-source tools
- **5-Port TP-Link Switch** - To connect my network devices to the main server and ensure fast, stable local communication
- **UGREEN NASync DXP4800 Plus** - A 4-bay NAS to store and manage my data centrally. Currently equipped with 2 8TB hard drives, with the possibility to add 2 more in the future
### 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

@@ -7,7 +7,6 @@ export default defineNuxtConfig({
'@nuxthub/core',
'@nuxt/eslint',
'@vueuse/nuxt',
'@nuxtjs/i18n',
'nuxt-studio'
],
@@ -63,28 +62,17 @@ export default defineNuxtConfig({
},
runtimeConfig: {
api: {
url: ''
discord: {
userId: '',
id: ''
},
public: {
i18n: {
baseUrl: ''
}
}
},
routeRules: {
'/api/activity': {
proxy: `${process.env.NUXT_API_URL}/api/activity`
},
'/api/stats': {
proxy: `${process.env.NUXT_API_URL}/api/stats`
},
'/api/uses': {
proxy: `${process.env.NUXT_API_URL}/api/uses`
},
'/api/': {
proxy: `${process.env.NUXT_API_URL}/api/`
statusPage: '',
wakatime: {
userId: '',
coding: '',
editors: '',
languages: '',
os: ''
}
},
@@ -94,7 +82,7 @@ export default defineNuxtConfig({
compatibilityDate: '2025-12-13',
nitro: {
preset: 'cloudflare_pages',
preset: 'cloudflare_module',
prerender: {
routes: ['/'],
@@ -116,30 +104,6 @@ export default defineNuxtConfig({
}
},
i18n: {
strategy: 'no_prefix',
locales: [
{
label: 'English',
code: 'en',
language: 'en-EN',
icon: 'i-twemoji-flag-united-kingdom'
},
{
label: 'Français',
code: 'fr',
language: 'fr-FR',
icon: 'i-twemoji-flag-france'
},
{
label: 'Español',
code: 'es',
language: 'es-ES',
icon: 'i-twemoji-flag-spain'
}
],
defaultLocale: 'en'
},
studio: {
route: '/studio',
repository: {

View File

@@ -4,15 +4,13 @@
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"preview": "bun run build && wrangler pages dev",
"preview": "bun run build && wrangler dev",
"postinstall": "nuxt prepare",
"lint": "eslint .",
"deploy": "bun run cf-typegen && bun run build && wrangler pages deploy",
"deploy": "bun run cf-typegen && bun run build && wrangler deploy",
"cf-typegen": "wrangler types"
},
"dependencies": {
"@ai-sdk/mcp": "^0.0.12",
"@ai-sdk/vue": "^2.0.115",
"@libsql/client": "^0.15.15",
"@modelcontextprotocol/sdk": "^1.25.1",
"@nuxt/content": "3.9.0",
@@ -22,27 +20,25 @@
"@nuxtjs/mdc": "^0.19.1",
"@vueuse/core": "^14.1.0",
"@vueuse/math": "^14.1.0",
"ai": "5.0.115",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-alpha.4",
"vue": "3.5.26",
"vue-router": "4.6.4",
"workers-ai-provider": "^2.0.0",
"zod": "^4.2.1"
},
"devDependencies": {
"@iconify-json/devicon": "1.2.53",
"@iconify-json/devicon": "1.2.54",
"@iconify-json/logos": "^1.2.10",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/twemoji": "^1.2.4",
"@iconify-json/twemoji": "1.2.5",
"@iconify-json/vscode-icons": "1.2.37",
"@types/node": "25.0.3",
"@vueuse/nuxt": "14.1.0",
"eslint": "9.39.2",
"typescript": "^5.9.3",
"vue-tsc": "3.1.8",
"vue-tsc": "3.2.1",
"wrangler": "4.56.0"
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
export default defineEventHandler(async (event) => {
const { discord } = useRuntimeConfig(event)
return await $fetch(`https://api.lanyard.rest/v1/users/${discord.userId}`)
})

View File

@@ -1,89 +0,0 @@
import { defineEventHandler, readValidatedBody } from 'h3'
import { z } from 'zod'
import {
convertToModelMessages,
createUIMessageStream,
createUIMessageStreamResponse,
streamText
} from 'ai'
import type { UIMessage } from 'ai'
import { createWorkersAI } from 'workers-ai-provider'
import { statsTool } from '~~/shared/utils/tools/stats'
import { activityTool } from '~~/shared/utils/tools/activity'
import { resumesTool } from '~~/shared/utils/tools/read-resume'
import { resourceTool } from '~~/shared/utils/tools/read-resources'
import { statusPageTool } from '~~/shared/utils/tools/status-page'
import { usesByCategoryTool } from '~~/shared/utils/tools/uses-by-category'
export default defineEventHandler(async (event) => {
const { messages, lang } = await readValidatedBody(event, z.object({
messages: z.array(z.custom<UIMessage>()),
lang: z.string().optional()
}).parse)
const { AI } = event.context.cloudflare.env || {}
if (!AI) {
throw createError({
statusCode: 500,
statusMessage: 'Cloudflare AI Binding not found. Check wrangler.json.'
})
}
const validCategories = ['contact', 'education', 'experiences', 'hobbies', 'languages', 'profile', 'projects', 'skills', 'uses'].join(', ')
const workersAI = createWorkersAI({ binding: AI })
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: workersAI('@cf/meta/llama-3-8b-instruct'),
system: `You are Arthur Danjou's personal portfolio assistant (Data Science Student).
### TOOL USAGE GUIDE - FOLLOW STRICTLY:
1. **readResources(category)**: YOUR PRIMARY BRAIN.
- Use this for ANY question about Arthur's life, background, or work.
- Categories: ${validCategories}.
- Example: "What are his skills?" -> call readResources('skills').
- Example: "Where did he study?" -> call readResources('education').
2. **readResume()**: THE FILE DISPENSER.
- **WARNING:** This tool ONLY returns a direct DOWNLOAD LINK (URL). It does NOT contain text.
- **WHEN TO USE:** ONLY when the user explicitly asks to "download", "get", "see", or "have" the CV/Resume file.
- **DO NOT USE:** If the user asks "What is on his resume?" or "Describe his experience", DO NOT use this. Use 'readResources' instead.
3. **stats()**: GITHUB METRICS.
- Use this for questions about coding volume, commit streaks, languages used percentages, or GitHub activity.
4. **activity()**: LIVE STATUS.
- Use this to know what Arthur is doing RIGHT NOW (e.g., "Is he coding?", "What is he listening to on Spotify?").
5. **statusPage()**: INFRASTRUCTURE HEALTH.
- Use this if the user asks about the website's uptime, server status, or if services are down.
6. **usesByCategory()**: TECH STACK & GEAR.
- Use this for specific questions about his hardware (Mac vs PC), software, developer tools, or desk setup.
### GENERAL RULES:
- If you don't call a tool, you know NOTHING. Do not hallucinate.
- Always answer in ${lang || 'English'}.
- Be concise and professional.
`,
tools: {
stats: statsTool,
activity: activityTool,
readResume: resumesTool,
readResources: resourceTool,
statusPage: statusPageTool,
usesByCategory: usesByCategoryTool
},
messages: await convertToModelMessages(messages)
})
writer.merge(result.toUIMessageStream())
}
})
return createUIMessageStreamResponse({ stream })
})

16
server/api/contact.get.ts Normal file
View File

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

View File

@@ -0,0 +1,24 @@
import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'education')
.where('extension', '=', 'md')
.all()
if (result.length === 0) {
throw createError({ statusCode: 404, statusMessage: 'Education records not found' })
}
return result
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
.map(edu => ({
degree: edu.degree,
institution: edu.institution,
startDate: edu.startDate,
endDate: edu.endDate,
location: edu.location
}))
}, {
maxAge: 60 * 60 * 24,
name: 'education'
})

View File

@@ -0,0 +1,27 @@
import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'experiences')
.where('extension', '=', 'md')
.all()
if (result.length === 0) {
throw createError({ statusCode: 404, statusMessage: 'Experience records not found' })
}
return result
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
.map(exp => ({
title: exp.title,
company: exp.company,
companyUrl: exp.companyUrl,
startDate: exp.startDate,
endDate: exp.endDate,
location: exp.location,
description: exp.description
}))
},
{
maxAge: 60 * 60 * 24,
name: 'experiences'
})

15
server/api/hobbies.get.ts Normal file
View File

@@ -0,0 +1,15 @@
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

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

16
server/api/profile.get.ts Normal file
View File

@@ -0,0 +1,16 @@
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

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

16
server/api/skills.get.ts Normal file
View File

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

57
server/api/stats.get.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { H3Event } from 'h3'
const cachedWakatimeCoding = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.coding}.json`)
}, {
maxAge: 24 * 60 * 60,
name: 'wakatime',
getKey: () => 'coding'
})
const cachedWakatimeEditors = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.editors}.json`)
}, {
maxAge: 24 * 60 * 60,
name: 'wakatime',
getKey: () => 'editors'
})
const cachedWakatimeOs = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.os}.json`)
}, {
maxAge: 24 * 60 * 60,
name: 'wakatime',
getKey: () => 'os'
})
const cachedWakatimeLanguages = defineCachedFunction(async (event: H3Event) => {
const config = useRuntimeConfig(event)
return await $fetch<{ data: unknown[] }>(`https://wakatime.com/share/${config.wakatime.userId}/${config.wakatime.languages}.json`)
}, {
maxAge: 24 * 60 * 60,
name: 'wakatime',
getKey: () => 'languages'
})
export default defineEventHandler(async (event) => {
const [coding, editors, os, languages] = await Promise.all([
cachedWakatimeCoding(event),
cachedWakatimeEditors(event),
cachedWakatimeOs(event),
cachedWakatimeLanguages(event)
])
return {
coding: coding.data,
editors: editors.data,
os: os.data,
languages: languages.data
}
})

View File

@@ -0,0 +1,7 @@
export default defineCachedEventHandler(async (event) => {
const { statusPage } = useRuntimeConfig(event)
return await $fetch(statusPage)
}, {
maxAge: 60 * 60,
name: 'status-page'
})

33
server/api/uses.get.ts Normal file
View File

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,6 @@
export default defineCachedEventHandler(async (event) => {
return sendRedirect(event, '/resumes/CV 2026 EN.pdf', 302)
}, {
maxAge: 60 * 60 * 24,
name: 'resume_en'
})

View File

@@ -0,0 +1,6 @@
export default defineCachedEventHandler(async (event) => {
return sendRedirect(event, '/resumes/CV 2026 FR.pdf', 302)
}, {
maxAge: 60 * 60 * 24,
name: 'resume_fr'
})

View File

@@ -1,6 +0,0 @@
export * from './tools/activity'
export * from './tools/read-resources'
export * from './tools/read-resume'
export * from './tools/stats'
export * from './tools/status-page'
export * from './tools/uses-by-category'

View File

@@ -1,12 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
import type { Activity } from '~~/types'
export type ActivityUIToolInvocation = UIToolInvocation<typeof activityTool>
export const activityTool = tool({
description: 'Real-time current activity and status of Arthur Danjou, including what he\'s currently working on',
execute: async () => {
return await $fetch<Activity>('/api/activity')
}
})

View File

@@ -1,25 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
import { z } from 'zod'
export type resourceUIToolInvocation = UIToolInvocation<typeof resourceTool>
export const resourceTool = tool({
description: 'Read a resource from the server API',
inputSchema: z.object({
resource: z.enum([
'contact',
'education',
'experiences',
'hobbies',
'languages',
'profile',
'projects',
'skills',
'uses'
]).describe('resource name')
}),
execute: async ({ resource }) => {
return await $fetch(`/api/${resource}`)
}
})

View File

@@ -1,15 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
import { z } from 'zod'
export type ResumesUIToolInvocation = UIToolInvocation<typeof resumesTool>
export const resumesTool = tool({
description: 'Retrieves a direct download link to Arthur Danjou\'s professional resume in the specified language. Supports both English and French versions.',
inputSchema: z.object({
lang: z.enum(['en', 'fr']).describe('The language for the resume: \'en\' for English or \'fr\' for French.')
}),
execute: async ({ lang }) => {
return `/api/resumes/${lang}`
}
})

View File

@@ -1,12 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
import type { Stats } from '~~/types'
export type StatsUIToolInvocation = UIToolInvocation<typeof statsTool>
export const statsTool = tool({
description: 'Detailed coding statistics and analytics from WakaTime, including programming languages, time spent coding, and productivity metrics',
execute: async () => {
return await $fetch<Stats>('/api/stats')
}
})

View File

@@ -1,11 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
export type StatusPageUIToolInvocation = UIToolInvocation<typeof statusPageTool>
export const statusPageTool = tool({
description: 'Real-time status, uptime monitoring, and incident reports for Arthur Danjou\'s homelab infrastructure, powered by UptimeKuma',
execute: async () => {
return await $fetch('/api/status-page')
}
})

View File

@@ -1,17 +0,0 @@
import type { UIToolInvocation } from 'ai'
import { tool } from 'ai'
import { z } from 'zod'
export type UsesByCategoryUIToolInvocation = UIToolInvocation<typeof usesByCategoryTool>
export const usesByCategoryTool = tool({
description: 'Retrieves a filtered list of tools, software, and hardware used by Arthur Danjou based on a specific category. Available categories: homelab, IDE, hardware, and software.',
inputSchema: z.object({
categoryName: z.enum(['homelab', 'ide', 'hardware', 'software']).describe('The category to filter by: \'homelab\', \'ide\', \'hardware\', or \'software\'.')
}),
execute: async ({ categoryName }: { categoryName: 'homelab' | 'ide' | 'hardware' | 'software' }) => {
const uses = await $fetch<{ category: string }[]>('/api/uses')
return uses.filter(use => use.category === categoryName)
}
})

View File

@@ -5,24 +5,16 @@ interface WakatimeData {
export interface Stats {
coding: {
data: {
grand_total: {
total_seconds_including_other_language: number
}
range: {
start: string
}
grand_total: {
total_seconds_including_other_language: number
}
range: {
start: string
}
}
editors: {
data: WakatimeData[]
}
os: {
data: WakatimeData[]
}
languages: {
data: WakatimeData[]
}
editors: WakatimeData[]
os: WakatimeData[]
languages: WakatimeData[]
}
interface LanyardActivity {
@@ -51,78 +43,6 @@ export const IDEs = [
{ name: 'Cursor', icon: 'i-vscode-icons-file-type-cursorrules' }
] as const
type TimeUnit = (n: number, past?: boolean) => string
type TimeFormatter = (n: string) => string
interface ActivityMessages {
justNow: string
past: TimeFormatter
future: TimeFormatter
month: TimeUnit
year: TimeUnit
day: TimeUnit
week: TimeUnit
hour: TimeUnit
minute: TimeUnit
second: TimeUnit
invalid: string
}
function createTimeUnit(singular: string, plural: string, pastSingular?: string, futureSingular?: string): TimeUnit {
return (n: number, past = true) => {
if (n === 1) {
return past ? (pastSingular || `last ${singular}`) : (futureSingular || `next ${singular}`)
}
return `${n} ${plural}`
}
}
function createSimpleTimeUnit(unit: string): TimeUnit {
return (n: number) => `${n} ${unit}${n > 1 ? 's' : ''}`
}
export const activityMessages: Record<'en' | 'fr' | 'es', ActivityMessages> = {
en: {
justNow: 'just now',
past: (n: string) => n.match(/\d/) ? `${n} ago` : n,
future: (n: string) => n.match(/\d/) ? `in ${n}` : n,
month: createTimeUnit('month', 'months'),
year: createTimeUnit('year', 'years'),
day: createTimeUnit('day', 'days', 'yesterday', 'tomorrow'),
week: createTimeUnit('week', 'weeks'),
hour: createSimpleTimeUnit('hour'),
minute: createSimpleTimeUnit('minute'),
second: createSimpleTimeUnit('second'),
invalid: ''
},
fr: {
justNow: 'à l\'instant',
past: (n: string) => n.match(/\d/) ? `il y a ${n}` : n,
future: (n: string) => n.match(/\d/) ? `dans ${n}` : n,
month: (n: number, past = true) => n === 1 ? (past ? 'le mois dernier' : 'le mois prochain') : `${n} mois`,
year: (n: number, past = true) => n === 1 ? (past ? 'l\'année dernière' : 'l\'année prochaine') : `${n} ans`,
day: (n: number, past = true) => n === 1 ? (past ? 'hier' : 'demain') : `${n} jours`,
week: (n: number, past = true) => n === 1 ? (past ? 'la semaine dernière' : 'la semaine prochaine') : `${n} semaines`,
hour: createSimpleTimeUnit('heure'),
minute: createSimpleTimeUnit('minute'),
second: createSimpleTimeUnit('seconde'),
invalid: ''
},
es: {
justNow: 'justo ahora',
past: (n: string) => n.match(/\d/) ? `hace ${n}` : n,
future: (n: string) => n.match(/\d/) ? `dentro de ${n}` : n,
month: (n: number, past = true) => n === 1 ? (past ? 'el mes pasado' : 'el próximo mes') : `${n} meses`,
year: (n: number, past = true) => n === 1 ? (past ? 'el año pasado' : 'el próximo año') : `${n} años`,
day: (n: number, past = true) => n === 1 ? (past ? 'ayer' : 'mañana') : `${n} días`,
week: (n: number, past = true) => n === 1 ? (past ? 'la semana pasada' : 'la próxima semana') : `${n} semanas`,
hour: createSimpleTimeUnit('hora'),
minute: createSimpleTimeUnit('minuto'),
second: createSimpleTimeUnit('segundo'),
invalid: ''
}
}
export const socials = [
{ icon: 'i-ph-x-logo-duotone', label: 'Twitter', to: 'https://go.arthurdanjou.fr/twitter' },
{ icon: 'i-ph-github-logo-duotone', label: 'GitHub', to: 'https://go.arthurdanjou.fr/github' },
@@ -130,22 +50,21 @@ export const socials = [
{ icon: 'i-ph-discord-logo-duotone', label: 'Discord', to: 'https://go.arthurdanjou.fr/discord' }
] as const
type Locale = 'en' | 'fr' | 'es'
interface Nav {
label: Record<Locale, string>
label: string
to: string
icon?: string
target?: string
}
export const navs: readonly Nav[] = [
{ label: { en: 'home', fr: 'accueil', es: 'inicio' }, to: '/', icon: 'house-duotone' },
{ label: { en: 'chat', fr: 'chat', es: 'chat' }, to: '/chat', icon: 'chat-circle-dots-duotone' },
{ label: 'home', to: '/', icon: 'house-duotone' },
{ label: 'projects', to: '/projects', icon: 'folder-duotone' },
{ label: 'stats', to: '/stats', icon: 'chart-bar-duotone' },
{
label: { en: 'resume', fr: 'cv', es: 'currículum' },
label: 'resume',
icon: 'address-book-duotone',
to: 'https://api.arthurdanjou.fr/api/resumes/en',
to: 'resumes/en',
target: '_blank'
}
] as const

View File

@@ -1,18 +1,29 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 7333ef017a5016b51354dce96a893062)
// Runtime types generated with workerd@1.20251217.0 2025-12-13
// Generated by Wrangler by running `wrangler types` (hash: 730a20d40c88c8f7167cb4afae90a541)
// Runtime types generated with workerd@1.20251217.0 2025-12-13 nodejs_compat
declare namespace Cloudflare {
interface Env {
CACHE: KVNamespace;
NUXT_PUBLIC_I18N_BASE_URL: string;
NUXT_API_URL: string;
STUDIO_GITHUB_CLIENT_ID: string;
STUDIO_GITHUB_CLIENT_SECRET: string;
NUXT_DISCORD_USER_ID: string;
NUXT_WAKATIME_CODING: string;
NUXT_WAKATIME_EDITORS: string;
NUXT_WAKATIME_LANGUAGES: string;
NUXT_WAKATIME_OS: string;
NUXT_WAKATIME_USER_ID: string;
NUXT_STATUS_PAGE: string;
DB: D1Database;
AI: Ai;
ASSETS: Fetcher;
}
}
interface Env extends Cloudflare.Env {}
type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "STUDIO_GITHUB_CLIENT_ID" | "STUDIO_GITHUB_CLIENT_SECRET" | "NUXT_DISCORD_USER_ID" | "NUXT_WAKATIME_CODING" | "NUXT_WAKATIME_EDITORS" | "NUXT_WAKATIME_LANGUAGES" | "NUXT_WAKATIME_OS" | "NUXT_WAKATIME_USER_ID" | "NUXT_STATUS_PAGE">> {}
}
// Begin runtime types
/*! *****************************************************************************
@@ -8594,7 +8605,7 @@ type AIGatewayHeaders = {
[key: string]: string | number | boolean | object;
};
type AIGatewayUniversalRequest = {
provider: AIGatewayProviders | string;
provider: AIGatewayProviders | string; // eslint-disable-line
endpoint: string;
headers: Partial<AIGatewayHeaders>;
query: unknown;
@@ -8610,7 +8621,7 @@ declare abstract class AiGateway {
gateway?: UniversalGatewayOptions;
extraHeaders?: object;
}): Promise<Response>;
getUrl(provider?: AIGatewayProviders | string): Promise<string>;
getUrl(provider?: AIGatewayProviders | string): Promise<string>; // eslint-disable-line
}
interface AutoRAGInternalError extends Error {
}

View File

@@ -2,7 +2,26 @@
"$schema": "node_modules/wrangler/config-schema.json",
"name": "artsite",
"compatibility_date": "2025-12-13",
"pages_build_output_dir": "dist/public",
"compatibility_flags": [
"nodejs_compat",
],
"preview_urls": true,
"workers_dev": true,
"main": "./.output/server/index.mjs",
"routes": [
{
"pattern": "v2.arthurdanjou.fr",
"zone_name": "arthurdanjou.fr",
"custom_domain": true
}
],
"placement": {
"mode": "smart",
},
"assets": {
"binding": "ASSETS",
"directory": "./.output/public/"
},
"d1_databases": [
{
"binding": "DB",
@@ -15,8 +34,18 @@
"id": "f0766ace1d24423ba6e5cac4fb8f2054"
}
],
"ai": {
"binding": "AI",
"remote": true
"observability": {
"enabled": true,
"logs": {
"enabled": true,
"head_sampling_rate": 1,
"persist": true,
"invocation_logs": true
},
"traces": {
"enabled": true,
"head_sampling_rate": 1,
"persist": true
}
}
}