mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-02-03 05:31:44 +01:00
Compare commits
3 Commits
fb22fdf057
...
bac370e465
| Author | SHA1 | Date | |
|---|---|---|---|
| bac370e465 | |||
| b8332b13af | |||
| 91422148dd |
15
.env.example
15
.env.example
@@ -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=
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
123
app/components/home/live/Activity.vue
Normal file
123
app/components/home/live/Activity.vue
Normal 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>
|
||||
192
app/components/home/live/StatusPage.vue
Normal file
192
app/components/home/live/StatusPage.vue
Normal 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>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
42
bun.lock
42
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Bachelor's Degree in Mathematics
|
||||
type: Bachelor
|
||||
degree: Bachelor
|
||||
institution: Paris-Saclay University
|
||||
location: Paris, France
|
||||
startDate: 2021-09
|
||||
|
||||
16
content/education/doctorate.md
Normal file
16
content/education/doctorate.md
Normal 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
|
||||
---
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
server/db/migrations/sqlite/meta/_journal.json
Normal file
1
server/db/migrations/sqlite/meta/_journal.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"7","dialect":"sqlite","entries":[]}
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user