mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 09:54:05 +01:00
feat: ajouter des composants de statut en direct et mettre à jour la configuration de l'éducation
This commit is contained in:
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>
|
||||
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 text-neutral-900 dark:text-white">{{ 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 text-neutral-900 dark:text-white">{{ 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-amber-500' : 'text-neutral-400'"
|
||||
>
|
||||
<UIcon
|
||||
name="i-ph-wrench-duotone"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
<span class="text-xl font-bold text-neutral-900 dark:text-white">{{ 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,7 +41,7 @@ const items = computed<TimelineItem[]>(() => {
|
||||
<UTimeline
|
||||
:orientation="orientation"
|
||||
:items="items"
|
||||
:default-value="items.length"
|
||||
:default-value="items.length - 1"
|
||||
active-color="primary"
|
||||
color="neutral"
|
||||
class="w-full max-w-5xl"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: undefined
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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