Compare commits

3 Commits

23 changed files with 421 additions and 362 deletions

View File

@@ -1,5 +1,12 @@
NUXT_PUBLIC_I18N_BASE_URL=
NUXT_API_URL=
STUDIO_GITHUB_CLIENT_ID=
STUDIO_GITHUB_CLIENT_SECRET=
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=

View File

@@ -1,125 +0,0 @@
<script lang="ts" setup>
import type { Activity } from '~~/types'
import { IDEs } from '~~/types'
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'), { lazy: true })
useIntervalFn(refresh, 5000)
const currentSession = computed(() => {
const list = activity.value?.data.activities ?? []
const ideActivity = list.find(a => IDEs.some(ide => ide.name === a.name))
if (!ideActivity) return null
const name = ideActivity.assets?.small_text === 'Cursor' ? 'Cursor' : ideActivity.name
const isIdling = ideActivity.details?.includes('Idling') || (!ideActivity.state?.toLowerCase().includes('editing') && name !== 'Visual Studio Code')
const rawProject = ideActivity.details ? ideActivity.details.replace('Workspace:', '').replace('Editing', '').trim() : 'Unknown Context'
const project = rawProject.charAt(0).toUpperCase() + rawProject.slice(1)
const file = ideActivity.state?.replace('Editing', '').trim() || 'No active file'
return {
name,
project,
file,
isIdling,
startTime: ideActivity.timestamps?.start,
icon: IDEs.find(ide => ide.name === name)?.icon ?? 'i-ph-code-duotone'
}
})
const timeAgo = useTimeAgo(computed(() => currentSession.value?.startTime ?? new Date()))
const statusColor = computed(() => {
if (!currentSession.value) return 'red'
return currentSession.value.isIdling ? 'orange' : 'green'
})
const statusLabel = computed(() => {
if (!currentSession.value) return 'System Offline'
if (currentSession.value.isIdling) return 'System Idling'
return 'Active Development'
})
</script>
<template>
<ClientOnly>
<div class="w-full mb-4">
<UCard v-if="activity">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div class="relative flex h-3 w-3">
<span
v-if="statusColor === 'green'"
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-green-400"
/>
<span
class="relative inline-flex rounded-full h-3 w-3 transition-colors duration-300"
:class="{
'bg-green-500': statusColor === 'green',
'bg-orange-500': statusColor === 'orange',
'bg-red-500': statusColor === 'red'
}"
/>
</div>
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{{ statusLabel }}
</span>
</div>
<UIcon
v-if="currentSession"
:name="currentSession.icon"
class="w-5 h-5 opacity-80"
/>
<UIcon
v-else
name="i-ph-power-duotone"
class="w-5 h-5 text-red-400 opacity-80"
/>
</div>
<div
v-if="currentSession"
class="space-y-1 pl-6 border-l-2 border-neutral-100 dark:border-neutral-800 ml-1.5"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<h3 class="font-semibold text-neutral-900 dark:text-white truncate">
{{ currentSession.project }}
</h3>
<span class="hidden sm:inline text-neutral-400 text-xs"></span>
<span class="text-sm text-neutral-500 dark:text-neutral-400 truncate">
{{ currentSession.file }}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-neutral-400 mt-1">
<UIcon
name="i-ph-timer-duotone"
class="w-4 h-4"
/>
<span>Started {{ timeAgo }}</span>
</div>
</div>
<div
v-else
class="text-sm text-neutral-500 dark:text-neutral-400 flex items-center gap-2 pl-6 border-l-2 border-red-100 dark:border-red-900/30 ml-1.5"
>
<p>Telemetry disconnected. Research in progress.</p>
</div>
</UCard>
<UCard v-else>
<div class="flex items-center gap-3">
<USkeleton class="h-3 w-3 rounded-full" /> <div class="space-y-2 flex-1">
<USkeleton class="h-4 w-1/3" />
<USkeleton class="h-3 w-2/3" />
</div>
</div>
</UCard>
</div>
</ClientOnly>
</template>

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ const { skills } = await useContent()
:key="item.name"
:icon="item.icon"
variant="soft"
size="lg"
color="primary"
class="transition-colors duration-200 hover:opacity-80"
:aria-label="item.name"

View File

@@ -0,0 +1,123 @@
<script lang="ts" setup>
import type { Activity } from '~~/types'
import { IDEs } from '~~/types'
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'), { lazy: true })
useIntervalFn(refresh, 5000)
const currentSession = computed(() => {
const list = activity.value?.data.activities ?? []
const ideActivity = list.find(a => IDEs.some(ide => ide.name === a.name))
if (!ideActivity) return null
const name = ideActivity.assets?.small_text === 'Cursor' ? 'Cursor' : ideActivity.name
const isIdling = ideActivity.details?.includes('Idling') || (!ideActivity.state?.toLowerCase().includes('editing') && name !== 'Visual Studio Code')
const rawProject = ideActivity.details ? ideActivity.details.replace('Workspace:', '').replace('Editing', '').trim() : 'Unknown Context'
const project = rawProject.charAt(0).toUpperCase() + rawProject.slice(1)
const file = ideActivity.state?.replace('Editing', '').trim() || 'No active file'
return {
name,
project,
file,
isIdling,
startTime: ideActivity.timestamps?.start,
icon: IDEs.find(ide => ide.name === name)?.icon ?? 'i-ph-code-duotone'
}
})
const timeAgo = useTimeAgo(computed(() => currentSession.value?.startTime ?? new Date()))
const statusColor = computed(() => {
if (!currentSession.value) return 'red'
return currentSession.value.isIdling ? 'orange' : 'green'
})
const statusLabel = computed(() => {
if (!currentSession.value) return 'System Offline'
if (currentSession.value.isIdling) return 'System Idling'
return 'Active Development'
})
</script>
<template>
<ClientOnly>
<UCard v-if="activity">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<div class="relative flex h-3 w-3">
<span
v-if="statusColor === 'green'"
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-green-400"
/>
<span
class="relative inline-flex rounded-full h-3 w-3 transition-colors duration-300"
:class="{
'bg-green-500': statusColor === 'green',
'bg-orange-500': statusColor === 'orange',
'bg-red-500': statusColor === 'red'
}"
/>
</div>
<span class="text-xs font-bold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
{{ statusLabel }}
</span>
</div>
<UIcon
v-if="currentSession"
:name="currentSession.icon"
class="w-5 h-5 opacity-80"
/>
<UIcon
v-else
name="i-ph-power-duotone"
class="w-5 h-5 text-red-400 opacity-80"
/>
</div>
<div
v-if="currentSession"
class="space-y-1 pl-6 border-l-2 border-neutral-100 dark:border-neutral-800 ml-1.5"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<h3 class="font-semibold text-neutral-900 dark:text-white truncate">
{{ currentSession.project }}
</h3>
<span class="hidden sm:inline text-neutral-400 text-xs"></span>
<span class="text-sm text-neutral-500 dark:text-neutral-400 truncate">
{{ currentSession.file }}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-neutral-400 mt-1">
<UIcon
name="i-ph-timer-duotone"
class="w-4 h-4"
/>
<span>Started {{ timeAgo }}</span>
</div>
</div>
<div
v-else
class="text-sm text-neutral-500 dark:text-neutral-400 flex items-center gap-2 pl-6 border-l-2 border-red-100 dark:border-red-900/30 ml-1.5"
>
<p>Telemetry disconnected. Research in progress.</p>
</div>
</UCard>
<UCard v-else>
<div class="flex items-center gap-3">
<USkeleton class="h-3 w-3 rounded-full" /> <div class="space-y-2 flex-1">
<USkeleton class="h-4 w-1/3" />
<USkeleton class="h-3 w-2/3" />
</div>
</div>
</UCard>
</ClientOnly>
</template>

View File

@@ -0,0 +1,192 @@
<script lang="ts" setup>
import type { StatusPageData } from '~~/types'
const { data, status } = await useAsyncData<StatusPageData>('home-status', () =>
$fetch('/api/status-page'),
{ lazy: true }
)
const isLoading = computed(() => status.value === 'pending')
const metrics = computed(() => {
if (!data.value || !data.value.publicGroupList) {
return { up: 0, down: 0, maintenance: 0, total: 0, uptime: 0 }
}
let upCount = 0
let downCount = 0
let totalCount = 0
data.value.publicGroupList.forEach((group) => {
group.monitorList.forEach((monitor) => {
totalCount++
const isUp = (monitor as unknown as { status: number }).status !== 0
if (isUp) upCount++
else downCount++
})
})
const activeMaintenances = data.value.maintenanceList?.filter(m => m.active).length || 0
const uptimePercent = totalCount > 0 ? ((upCount / totalCount) * 100).toFixed(1) : '0.0'
return {
up: upCount,
down: downCount,
maintenance: activeMaintenances,
total: totalCount,
uptime: Number(uptimePercent)
}
})
const statusState = computed(() => {
if (isLoading.value) return { color: 'neutral' as const, label: 'Checking status...' }
if (metrics.value.down > 0) return { color: 'red' as const, label: 'Service Disruption' }
if (metrics.value.maintenance > 0) return { color: 'blue' as const, label: 'Maintenance Mode' }
return { color: 'emerald' as const, label: 'All Systems Operational' }
})
</script>
<template>
<ClientOnly>
<UCard class="h-full flex flex-col overflow-hidden">
<div class="p-5 border-b border-neutral-200 dark:border-neutral-800">
<div class="flex items-center justify-between mb-2">
<h3 class="font-bold text-neutral-900 dark:text-white text-sm">
System Status
</h3>
<div class="flex items-center gap-2">
<span
v-if="!isLoading"
class="relative flex h-2.5 w-2.5"
>
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
:class="`bg-${statusState.color}-400`"
/>
<span
class="relative inline-flex rounded-full h-2.5 w-2.5"
:class="`bg-${statusState.color}-500`"
/>
</span>
<USkeleton
v-else
class="h-2.5 w-2.5 rounded-full"
/>
<span
v-if="!isLoading"
class="text-xs font-mono font-medium"
:class="`text-${statusState.color}-600 dark:text-${statusState.color}-400`"
>
{{ statusState.label }}
</span>
<USkeleton
v-else
class="h-4 w-24"
/>
</div>
</div>
<div class="mt-4">
<div class="flex justify-between text-xs mb-1.5">
<span class="text-neutral-500">Global Uptime</span>
<span
v-if="!isLoading"
class="font-mono font-bold text-neutral-900 dark:text-white"
>{{ metrics.uptime }}%</span>
<USkeleton
v-else
class="h-4 w-8"
/>
</div>
<UProgress
v-if="!isLoading"
v-model="metrics.uptime"
:color="statusState.color"
size="sm"
/>
<USkeleton
v-else
class="h-2 w-full rounded-full"
/>
</div>
</div>
<div class="grid grid-cols-3 divide-x divide-neutral-200 dark:divide-neutral-800 flex-1">
<div class="p-4 flex flex-col items-center justify-center text-center group">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Operational</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5 text-emerald-500"
>
<UIcon
name="i-ph-check-circle-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold">{{ metrics.up }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
<div class="p-4 flex flex-col items-center justify-center text-center">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Down</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5"
:class="metrics.down > 0 ? 'text-red-500' : 'text-neutral-400'"
>
<UIcon
name="i-ph-warning-circle-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold">{{ metrics.down }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
<div class="p-4 flex flex-col items-center justify-center text-center">
<span class="text-[10px] text-neutral-400 font-bold uppercase tracking-wider mb-1">Maint.</span>
<div
v-if="!isLoading"
class="flex items-center gap-1.5"
:class="metrics.maintenance > 0 ? 'text-blue-500' : 'text-neutral-400'"
>
<UIcon
name="i-ph-wrench-duotone"
class="w-5 h-5"
/>
<span class="text-xl font-bold">{{ metrics.maintenance }}</span>
</div>
<USkeleton
v-else
class="h-6 w-8 mt-1"
/>
</div>
</div>
<div class="p-2 text-center border-t border-neutral-200 dark:border-neutral-800 mt-auto">
<UButton
to="https://go.arthurdanjou.fr/status"
target="_blank"
rel="noopener noreferrer"
variant="link"
color="neutral"
size="xs"
:padded="false"
class="text-xs hover:text-primary-500"
>
View detailed report
</UButton>
</div>
</UCard>
</ClientOnly>
</template>

View File

@@ -30,7 +30,7 @@ const items = computed<TimelineItem[]>(() => {
title: item.title || 'Degree',
description: item.institution || '',
date: formatDate(item.startDate, item.endDate, item.duration),
icon: item.icon || 'i-ph-graduation-cap-duotone' // Context-aware default icon
icon: item.icon || 'i-ph-graduation-cap-duotone'
}))
})
</script>
@@ -41,8 +41,8 @@ const items = computed<TimelineItem[]>(() => {
<UTimeline
:orientation="orientation"
:items="items"
:default-value="items.length"
active-color="primary"
:default-value="2"
size="lg"
color="neutral"
class="w-full max-w-5xl"
/>

View File

@@ -42,7 +42,7 @@ const items = computed<TimelineItem[]>(() => {
:orientation="orientation"
:items="items"
:default-value="items.length"
active-color="primary"
size="lg"
color="neutral"
class="w-full max-w-5xl"
/>

View File

@@ -17,7 +17,7 @@
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-alpha.4",
"nuxt-studio": "1.0.0-beta.3",
"vue": "3.5.26",
"vue-router": "4.6.4",
"zod": "^4.2.1",
@@ -239,7 +239,7 @@
"@iconify-json/logos": ["@iconify-json/logos@1.2.10", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qxaXKJ6fu8jzTMPQdHtNxlfx6tBQ0jXRbHZIYy5Ilh8Lx9US9FsAdzZWUR8MXV8PnWTKGDFO4ZZee9VwerCyMA=="],
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.73", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-++HFkqDNu4jqG5+vYT+OcVj9OiuPCw9wQuh8G5QWQnBRSJ9eKwSStiU8ORgOoK07xJsm/0VIHySMniXUUXP9Gw=="],
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.82", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-fHZWegspOZonl5GNTvOkHsjnTMdSslFh3EzpzUtRyLxO8bOonqk2OTU3hCl0k4VXzViMjqpRK3X1sotnuBXkFA=="],
"@iconify-json/ph": ["@iconify-json/ph@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw=="],
@@ -1917,7 +1917,7 @@
"nuxt-component-meta": ["nuxt-component-meta@0.16.0", "", { "dependencies": { "@nuxt/kit": "^4.2.1", "citty": "^0.1.6", "json-schema-to-zod": "^2.7.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "scule": "^1.3.0", "typescript": "^5.9.3", "ufo": "^1.6.1", "vue-component-meta": "^3.1.5" }, "bin": { "nuxt-component-meta": "bin/nuxt-component-meta.mjs" } }, "sha512-mxsLl+gcF930dM4ozdxskGKEpldJn/fACR18uXrMDvvwxM+rMZW4tzuRMEuxhoyEXtxPLdOLP52wrS6UzBSx6Q=="],
"nuxt-studio": ["nuxt-studio@1.0.0-alpha.4", "", { "dependencies": { "@iconify-json/lucide": "^1.2.72", "@nuxtjs/mdc": "^0.18.3", "@vueuse/core": "^13.9.0", "defu": "^6.1.4", "destr": "^2.0.5", "js-yaml": "^4.1.1", "minimatch": "^10.1.1", "nuxt-component-meta": "^0.15.0", "shiki": "^3.19.0", "unstorage": "1.17.1" } }, "sha512-ggAXJaolRmRCU8lRO4wcxwZDFTHivw8YAhKDcAVr6YdmBL8HLplceNd/sAOlnqUDAPSy6YJNjDhG7wP9Kdaqvw=="],
"nuxt-studio": ["nuxt-studio@1.0.0-beta.3", "", { "dependencies": { "@iconify-json/lucide": "^1.2.82", "@nuxtjs/mdc": "^0.19.1", "@vueuse/core": "^14.1.0", "defu": "^6.1.4", "destr": "^2.0.5", "js-yaml": "^4.1.1", "minimatch": "^10.1.1", "nuxt-component-meta": "^0.16.0", "remark-mdc": "^3.9.0", "shiki": "^3.20.0", "unstorage": "1.17.3" } }, "sha512-Rcx7sfsQ0CeHY0rtqLKSgm2pRPF0mknJ9UVXyKWHkcnAFnhouAM8r6SoPLVHRdd/cmxdM9uTrNTfz6CNbk7vtQ=="],
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
@@ -2911,16 +2911,8 @@
"nuxt/vue-router": ["vue-router@4.6.3", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg=="],
"nuxt-studio/@nuxtjs/mdc": ["@nuxtjs/mdc@0.18.4", "", { "dependencies": { "@nuxt/kit": "^4.2.1", "@shikijs/core": "^3.15.0", "@shikijs/langs": "^3.15.0", "@shikijs/themes": "^3.15.0", "@shikijs/transformers": "^3.15.0", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@vue/compiler-core": "^3.5.24", "consola": "^3.4.2", "debug": "^4.4.3", "defu": "^6.1.4", "destr": "^2.0.5", "detab": "^3.0.2", "github-slugger": "^2.0.0", "hast-util-format": "^1.1.0", "hast-util-to-mdast": "^10.1.2", "hast-util-to-string": "^3.0.1", "mdast-util-to-hast": "^13.2.0", "micromark-util-sanitize-uri": "^2.0.1", "parse5": "^8.0.0", "pathe": "^2.0.3", "property-information": "^7.1.0", "rehype-external-links": "^3.0.0", "rehype-minify-whitespace": "^6.0.2", "rehype-raw": "^7.0.0", "rehype-remark": "^10.0.1", "rehype-slug": "^6.0.0", "rehype-sort-attribute-values": "^5.0.1", "rehype-sort-attributes": "^5.0.1", "remark-emoji": "^5.0.2", "remark-gfm": "^4.0.1", "remark-mdc": "^3.8.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", "scule": "^1.3.0", "shiki": "^3.15.0", "ufo": "^1.6.1", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-visit": "^5.0.0", "unwasm": "^0.5.0", "vfile": "^6.0.3" } }, "sha512-lM4R0Mbbhw5h5Fwj7LqGiw6eanqjjPkzi+9FaXfn1BdmfbW8GlR2quLIiBXTbw0wUrWYyOWoc5FGIE/gpZUTjQ=="],
"nuxt-studio/@vueuse/core": ["@vueuse/core@13.9.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.9.0", "@vueuse/shared": "13.9.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA=="],
"nuxt-studio/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
"nuxt-studio/nuxt-component-meta": ["nuxt-component-meta@0.15.0", "", { "dependencies": { "@nuxt/kit": "^4.2.1", "citty": "^0.1.6", "json-schema-to-zod": "^2.7.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "scule": "^1.3.0", "typescript": "^5.9.3", "ufo": "^1.6.1", "vue-component-meta": "^3.1.5" }, "bin": { "nuxt-component-meta": "bin/nuxt-component-meta.mjs" } }, "sha512-IW8xzHQdpmfgAyDYw4NPVQLnHAWrNltgJUD3Bww5Miogy8dd/dDdEKexCzFvI+gSa/uAe52zDRcIx9wwavmAmg=="],
"nuxt-studio/unstorage": ["unstorage@1.17.1", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -3443,20 +3435,6 @@
"nitropack/unwasm/unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
"nuxt-studio/@nuxtjs/mdc/@shikijs/transformers": ["@shikijs/transformers@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/types": "3.15.0" } }, "sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A=="],
"nuxt-studio/@nuxtjs/mdc/@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-studio/@nuxtjs/mdc/mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
"nuxt-studio/@nuxtjs/mdc/remark-mdc": ["remark-mdc@3.8.1", "", { "dependencies": { "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3", "flat": "^6.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-to-markdown": "^2.1.2", "micromark": "^4.0.2", "micromark-core-commonmark": "^2.0.3", "micromark-factory-space": "^2.0.1", "micromark-factory-whitespace": "^2.0.1", "micromark-util-character": "^2.1.1", "micromark-util-types": "^2.0.2", "parse-entities": "^4.0.2", "scule": "^1.3.0", "stringify-entities": "^4.0.4", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "yaml": "^2.8.1" } }, "sha512-TGFY61OhgziAITAomenbw4THQvEHC7MxZI1kO1YL/VuWQTHZ0RG20G6GGATIFeGnq65IUe7dngiQVcVIeFdB/g=="],
"nuxt-studio/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@13.9.0", "", {}, "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg=="],
"nuxt-studio/@vueuse/core/@vueuse/shared": ["@vueuse/shared@13.9.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g=="],
"nuxt-studio/unstorage/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"nuxt/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"nuxt/unctx/unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="],
@@ -3625,20 +3603,6 @@
"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=="],
"nuxt-studio/@nuxtjs/mdc/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
"nuxt-studio/@nuxtjs/mdc/@vue/compiler-core/@vue/shared": ["@vue/shared@3.5.24", "", {}, "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A=="],
"nuxt-studio/@nuxtjs/mdc/@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"nuxt-studio/@nuxtjs/mdc/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"nuxt-studio/@nuxtjs/mdc/remark-mdc/yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"nuxt-studio/unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"nuxt/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw=="],

View File

@@ -62,7 +62,6 @@ export const collections = {
source: 'education/*.md',
schema: z.object({
title: z.string(),
type: z.string().optional(),
degree: z.string().optional(),
institution: z.string(),
startDate: z.string(),

View File

@@ -2,7 +2,7 @@
"body": [
{
"id": "personal-email",
"name": "Email Personnel",
"name": "Personal Email",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-perso",
@@ -10,7 +10,7 @@
},
{
"id": "professional-email",
"name": "Email Professionnel",
"name": "Professional Email",
"category": "communication",
"icon": "i-ph-envelope-simple-duotone",
"value": "https://go.arthurdanjou.fr/mail-pro",
@@ -51,7 +51,7 @@
},
{
"id": "personal-website",
"name": "Site Personnel",
"name": "Portfolio",
"category": "web",
"icon": "i-ph:globe-duotone",
"value": "https://arthurdanjou.fr",
@@ -59,7 +59,7 @@
},
{
"id": "status-page",
"name": "Statut des Services",
"name": "Status Page",
"category": "infrastructure",
"icon": "i-ph:fire-duotone",
"value": "https://go.arthurdanjou.fr/status",

View File

@@ -1,6 +1,6 @@
---
title: Bachelor's Degree in Mathematics
type: Bachelor
degree: Bachelor
institution: Paris-Saclay University
location: Paris, France
startDate: 2021-09

View File

@@ -0,0 +1,16 @@
---
title: "PhD Candidate: AI Safety & Mathematical Robustness"
degree: Doctorate
institution: Academic Labs
location: Paris / International
startDate: 2026-10
endDate: null
duration: 3 years
description: I am actively seeking a PhD position starting in Fall 2026. My research interest lies at the intersection of Applied Mathematics and Deep Learning, specifically focusing on AI Safety, Adversarial Robustness, and Formal Verification. I aim to contribute to developing mathematically grounded methods to ensure the reliability and alignment of modern AI systems.
tags:
- AI Safety
- Robustness
- Formal Verification
- Applied Mathematics
icon: i-ph-student-duotone
---

View File

@@ -1,6 +1,6 @@
---
title: Master's Degree in Applied Mathematics (Year 1)
type: Master
degree: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2024-09

View File

@@ -1,6 +1,6 @@
---
title: Master's Degree in Applied Mathematics (Year 2)
type: Master
degree: Master
institution: Paris Dauphine-PSL University
location: Paris, France
startDate: 2025-09

View File

@@ -51,12 +51,14 @@ Mathematical rigor is the cornerstone of Safe AI. My background in :hover-text{t
Research requires discipline and transparency. Here is a real-time overview of my :hover-text{text="current environment" hover="OS, Editor, Activity"} and historical data.
::home-activity
::home-live-status-page{class="mb-4"}
::
::home-stats
::home-live-activity{class="mb-4"}
::
::home-live-stats
---
::home-quote

View File

@@ -23,7 +23,7 @@
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"nuxt": "4.2.2",
"nuxt-studio": "1.0.0-alpha.4",
"nuxt-studio": "1.0.0-beta.3",
"vue": "3.5.26",
"vue-router": "4.6.4",
"zod": "^4.2.1"

View File

@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'education')
.where('extension', '=', 'md')
.order('startDate', 'DESC')
.all()
if (result.length === 0) {
@@ -10,14 +11,6 @@ export default defineCachedEventHandler(async (event) => {
}
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

@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
export default defineCachedEventHandler(async (event) => {
const result = await queryCollection(event, 'experiences')
.where('extension', '=', 'md')
.order('startDate', 'DESC')
.all()
if (result.length === 0) {
@@ -10,16 +11,6 @@ export default defineCachedEventHandler(async (event) => {
}
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,

View File

@@ -0,0 +1 @@
{"version":"7","dialect":"sqlite","entries":[]}

View File

@@ -35,6 +35,62 @@ export interface Activity {
}
}
export interface StatusTag {
id: number
monitor_id: number
tag_id: number
value: string
name: string
color: string
}
export interface StatusMonitor {
id: number
name: string
sendUrl: number
type: string
url?: string
tags: StatusTag[]
}
export interface StatusGroup {
id: number
name: string
weight: number
monitorList: StatusMonitor[]
}
export interface StatusMaintenance {
id: number
title: string
description: string
strategy: string
active: boolean
status: string // 'under-maintenance', etc.
// ... autres champs optionnels (dateRange, etc.)
}
export interface StatusConfig {
slug: string
title: string
description: string
icon: string
autoRefreshInterval: number
theme: string
published: boolean
showTags: boolean
customCSS: string
footerText: string
showPoweredBy: boolean
}
export interface StatusPageData {
config: StatusConfig
incident: unknown | null
publicGroupList: StatusGroup[]
maintenanceList: StatusMaintenance[]
}
export const IDEs = [
{ name: 'Visual Studio Code', icon: 'i-logos:visual-studio-code' },
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-logos:intellij-idea' },