mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 11:54:09 +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_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',
|
title: item.title || 'Degree',
|
||||||
description: item.institution || '',
|
description: item.institution || '',
|
||||||
date: formatDate(item.startDate, item.endDate, item.duration),
|
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>
|
</script>
|
||||||
@@ -41,7 +41,7 @@ const items = computed<TimelineItem[]>(() => {
|
|||||||
<UTimeline
|
<UTimeline
|
||||||
:orientation="orientation"
|
:orientation="orientation"
|
||||||
:items="items"
|
:items="items"
|
||||||
:default-value="items.length"
|
:default-value="items.length - 1"
|
||||||
active-color="primary"
|
active-color="primary"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
class="w-full max-w-5xl"
|
class="w-full max-w-5xl"
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export const collections = {
|
|||||||
source: 'education/*.md',
|
source: 'education/*.md',
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
type: z.string().optional(),
|
|
||||||
degree: z.string().optional(),
|
degree: z.string().optional(),
|
||||||
institution: z.string(),
|
institution: z.string(),
|
||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Bachelor's Degree in Mathematics
|
title: Bachelor's Degree in Mathematics
|
||||||
type: Bachelor
|
degree: Bachelor
|
||||||
institution: Paris-Saclay University
|
institution: Paris-Saclay University
|
||||||
location: Paris, France
|
location: Paris, France
|
||||||
startDate: 2021-09
|
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)
|
title: Master's Degree in Applied Mathematics (Year 1)
|
||||||
type: Master
|
degree: Master
|
||||||
institution: Paris Dauphine-PSL University
|
institution: Paris Dauphine-PSL University
|
||||||
location: Paris, France
|
location: Paris, France
|
||||||
startDate: 2024-09
|
startDate: 2024-09
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Master's Degree in Applied Mathematics (Year 2)
|
title: Master's Degree in Applied Mathematics (Year 2)
|
||||||
type: Master
|
degree: Master
|
||||||
institution: Paris Dauphine-PSL University
|
institution: Paris Dauphine-PSL University
|
||||||
location: Paris, France
|
location: Paris, France
|
||||||
startDate: 2025-09
|
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.
|
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
|
::home-quote
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
|
|||||||
export default defineCachedEventHandler(async (event) => {
|
export default defineCachedEventHandler(async (event) => {
|
||||||
const result = await queryCollection(event, 'education')
|
const result = await queryCollection(event, 'education')
|
||||||
.where('extension', '=', 'md')
|
.where('extension', '=', 'md')
|
||||||
|
.order('startDate', 'DESC')
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
@@ -10,14 +11,6 @@ export default defineCachedEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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,
|
maxAge: 60 * 60 * 24,
|
||||||
name: 'education'
|
name: 'education'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { queryCollection } from '@nuxt/content/server'
|
|||||||
export default defineCachedEventHandler(async (event) => {
|
export default defineCachedEventHandler(async (event) => {
|
||||||
const result = await queryCollection(event, 'experiences')
|
const result = await queryCollection(event, 'experiences')
|
||||||
.where('extension', '=', 'md')
|
.where('extension', '=', 'md')
|
||||||
|
.order('startDate', 'DESC')
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
@@ -10,16 +11,6 @@ export default defineCachedEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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,
|
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 = [
|
export const IDEs = [
|
||||||
{ name: 'Visual Studio Code', icon: 'i-logos:visual-studio-code' },
|
{ name: 'Visual Studio Code', icon: 'i-logos:visual-studio-code' },
|
||||||
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-logos:intellij-idea' },
|
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-logos:intellij-idea' },
|
||||||
|
|||||||
Reference in New Issue
Block a user