Lint code

Signed-off-by: Arthur DANJOU <arthurdanjou@outlook.fr>
This commit is contained in:
2024-04-20 01:33:40 +02:00
parent a29f6bc0f6
commit 5ca0137c4e
67 changed files with 2671 additions and 2671 deletions

View File

@@ -1,30 +1,30 @@
export default defineAppConfig({ export default defineAppConfig({
ui: { ui: {
icons: { icons: {
dynamic: true, dynamic: true,
}, },
gray: 'neutral', gray: 'neutral',
primary: 'cyan', primary: 'cyan',
notifications: { notifications: {
position: 'bottom-0 right-0', position: 'bottom-0 right-0',
}, },
container: { container: {
base: 'mx-auto', base: 'mx-auto',
padding: 'px-4 sm:px-6 lg:px-8', padding: 'px-4 sm:px-6 lg:px-8',
constrained: 'max-w-9xl', constrained: 'max-w-9xl',
}, },
dropdown: { dropdown: {
container: 'z-50', container: 'z-50',
background: 'bg-white dark:bg-zinc-900/90', background: 'bg-white dark:bg-zinc-900/90',
item: { item: {
base: 'duration-300 group flex items-center gap-2 w-full', base: 'duration-300 group flex items-center gap-2 w-full',
}, },
}, },
button: { button: {
base: 'duration-300 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75', base: 'duration-300 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
}, },
popover: { popover: {
container: 'z-50', container: 'z-50',
}, },
}, },
}) })

View File

@@ -1,49 +1,49 @@
import type {RouterConfig} from '@nuxt/schema' import type {RouterConfig} from '@nuxt/schema'
function findHashPosition(hash: string): { el: string, behavior: ScrollBehavior, top: number } | undefined { function findHashPosition(hash: string): { el: string, behavior: ScrollBehavior, top: number } | undefined {
const el = document.querySelector(hash) const el = document.querySelector(hash)
// vue-router does not incorporate scroll-margin-top on its own. // vue-router does not incorporate scroll-margin-top on its own.
if (el) { if (el) {
const top = Number.parseFloat(getComputedStyle(el).scrollMarginTop) const top = Number.parseFloat(getComputedStyle(el).scrollMarginTop)
return { return {
el: hash, el: hash,
behavior: 'smooth', behavior: 'smooth',
top, top,
} }
} }
} }
// https://router.vuejs.org/api/#routeroptions // https://router.vuejs.org/api/#routeroptions
export default <RouterConfig>{ export default <RouterConfig>{
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
// If history back // If history back
if (savedPosition) { if (savedPosition) {
// Handle Suspense resolution // Handle Suspense resolution
return new Promise((resolve) => { return new Promise((resolve) => {
nuxtApp.hooks.hookOnce('page:finish', () => { nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(savedPosition), 50) setTimeout(() => resolve(savedPosition), 50)
}) })
}) })
} }
// Scroll to heading on click // Scroll to heading on click
if (to.hash) { if (to.hash) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (to.path === from.path) { if (to.path === from.path) {
setTimeout(() => resolve(findHashPosition(to.hash)), 50) setTimeout(() => resolve(findHashPosition(to.hash)), 50)
} }
else { else {
nuxtApp.hooks.hookOnce('page:finish', () => { nuxtApp.hooks.hookOnce('page:finish', () => {
setTimeout(() => resolve(findHashPosition(to.hash)), 50) setTimeout(() => resolve(findHashPosition(to.hash)), 50)
}) })
} }
}) })
} }
// Scroll to top of window // Scroll to top of window
return { top: 0 } return { top: 0 }
}, },
} }

16
auth.d.ts vendored
View File

@@ -1,10 +1,10 @@
declare module '#auth-utils' { declare module '#auth-utils' {
interface UserSession { interface UserSession {
user: { user: {
email: string email: string
username: string username: string
picture: string picture: string
admin: boolean admin: boolean
} }
} }
} }

View File

@@ -3,32 +3,32 @@ const { data: announce } = await useFetch('/api/announcement')
const appConfig = useAppConfig() const appConfig = useAppConfig()
function getColor() { function getColor() {
return `bg-${appConfig.ui.primary}-500` return `bg-${appConfig.ui.primary}-500`
} }
</script> </script>
<template> <template>
<div <div
v-if="announce" v-if="announce"
class="w-container flex justify-center mt-8" class="w-container flex justify-center mt-8"
> >
<div class="relative"> <div class="relative">
<h1 <h1
class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10" class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10"
v-html="announce.content" v-html="announce.content"
/> />
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2"> <span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
<span <span
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
:class="getColor()" :class="getColor()"
/> />
<span <span
class="relative inline-flex rounded-full h-2 w-2" class="relative inline-flex rounded-full h-2 w-2"
:class="getColor()" :class="getColor()"
/> />
</span> </span>
</div> </div>
</div> </div>
</template> </template>
<style> <style>

View File

@@ -4,15 +4,15 @@ const points = useState(() => Array.from({ length: 25 }).fill(0).map(() => [Math
const poly = computed(() => points.value.map(([x, y]) => `${x * 100}% ${y * 100}%`).join(', ')) const poly = computed(() => points.value.map(([x, y]) => `${x * 100}% ${y * 100}%`).join(', '))
function jumpVal(val: number) { function jumpVal(val: number) {
return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random() return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random()
} }
let timeout: NodeJS.Timeout let timeout: NodeJS.Timeout
function jumpPoints() { function jumpPoints() {
for (let i = 0; i < points.value.length; i++) for (let i = 0; i < points.value.length; i++)
points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])] points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])]
timeout = setTimeout(jumpPoints, Math.random() * 1000) timeout = setTimeout(jumpPoints, Math.random() * 1000)
} }
onMounted(() => jumpPoints()) onMounted(() => jumpPoints())
@@ -20,17 +20,17 @@ onUnmounted(() => clearTimeout(timeout))
</script> </script>
<template> <template>
<ClientOnly> <ClientOnly>
<div <div
aria-hidden="true" aria-hidden="true"
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden" class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
> >
<div <div
class="aspect-[2] h-2/3 w-full bg-gradient-to-r from-[rgb(var(--color-primary-DEFAULT))] to-white/10 lg:opacity-30 xs:opacity-50" class="aspect-[2] h-2/3 w-full bg-gradient-to-r from-[rgb(var(--color-primary-DEFAULT))] to-white/10 lg:opacity-30 xs:opacity-50"
:style="{ 'clip-path': `polygon(${poly})` }" :style="{ 'clip-path': `polygon(${poly})` }"
/> />
</div> </div>
</ClientOnly> </ClientOnly>
</template> </template>
<style scoped> <style scoped>

View File

@@ -3,37 +3,37 @@ const year = computed(() => new Date().getFullYear())
</script> </script>
<template> <template>
<footer class="w-full flex justify-center"> <footer class="w-full flex justify-center">
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 flex justify-between bg-white dark:bg-zinc-900 border-t border-zinc-100 dark:border-zinc-300/10"> <div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 flex justify-between bg-white dark:bg-zinc-900 border-t border-zinc-100 dark:border-zinc-300/10">
<div class="w-full duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2"> <div class="w-full duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
<p class="text-subtitle text-sm"> <p class="text-subtitle text-sm">
© {{ year }} ArtDanjProduction © {{ year }} ArtDanjProduction
</p> </p>
<div class="flex items-center"> <div class="flex items-center">
<p class="text-subtitle"> <p class="text-subtitle">
Designed & Built by Designed & Built by
</p> </p>
<UButton <UButton
color="primary" color="primary"
label="Arthur Danjou" label="Arthur Danjou"
target="_blank" target="_blank"
to="https://twitter.com/arthurdanj" to="https://twitter.com/arthurdanj"
variant="link" variant="link"
/> />
</div> </div>
<p class="text-subtitle flex items-center"> <p class="text-subtitle flex items-center">
Made with Made with
<UButton <UButton
color="green" color="green"
icon="i-vscode-icons-file-type-nuxt" icon="i-vscode-icons-file-type-nuxt"
label="Nuxt 3" label="Nuxt 3"
target="_blank" target="_blank"
to="https://nuxt.com/" to="https://nuxt.com/"
trailing trailing
variant="link" variant="link"
/> />
</p> </p>
</div> </div>
</div> </div>
</footer> </footer>
</template> </template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: 'Uses Section title', default: 'Uses Section title',
}, },
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -12,22 +12,22 @@ const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
</script> </script>
<template> <template>
<section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4"> <section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4">
<div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4"> <div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
<h2 <h2
:class="getColor" :class="getColor"
class="relative text-sm font-semibold pl-3.5" class="relative text-sm font-semibold pl-3.5"
> >
<span class="md:hidden absolute inset-y-0 left-0 flex items-center"> <span class="md:hidden absolute inset-y-0 left-0 flex items-center">
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" /> <span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span> </span>
{{ title }} {{ title }}
</h2> </h2>
<div class="md:col-span-3"> <div class="md:col-span-3">
<ul class="space-y-8"> <ul class="space-y-8">
<slot /> <slot />
</ul> </ul>
</div> </div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: 'Uses Slot title', default: 'Uses Slot title',
}, },
}) })
</script> </script>
<template> <template>
<li class="group relative flex flex-col items-start"> <li class="group relative flex flex-col items-start">
<h3 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"> <h3 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{{ title }} {{ title }}
</h3> </h3>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400"> <p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
<slot /> <slot />
</p> </p>
</li> </li>
</template> </template>

View File

@@ -1,26 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
href: { href: {
type: String, type: String,
default: '', default: '',
}, },
target: { target: {
type: String, type: String,
default: undefined, default: undefined,
required: false, required: false,
}, },
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
</script> </script>
<template> <template>
<NuxtLink <NuxtLink
:href="href" :href="href"
:target="target" :target="target"
class="border-b border-zinc-200 dark:border-zinc-700/70 duration-300" class="border-b border-zinc-200 dark:border-zinc-700/70 duration-300"
:class="`hover:border-${appConfig.ui.primary}-500 dark:hover:border-${appConfig.ui.primary}-500`" :class="`hover:border-${appConfig.ui.primary}-500 dark:hover:border-${appConfig.ui.primary}-500`"
> >
<slot /> <slot />
</NuxtLink> </NuxtLink>
</template> </template>

View File

@@ -8,14 +8,14 @@ const generate = computed(() => props.id && headings?.anchorLinks?.h2)
</script> </script>
<template> <template>
<h2 :id="id"> <h2 :id="id">
<a <a
v-if="id && generate" v-if="id && generate"
:href="`#${id}`" :href="`#${id}`"
class="pl-6 border-l border-zinc-200 dark:border-zinc-700/70 duration-300" class="pl-6 border-l border-zinc-200 dark:border-zinc-700/70 duration-300"
> >
<slot /> <slot />
</a> </a>
<slot v-else /> <slot v-else />
</h2> </h2>
</template> </template>

View File

@@ -1,14 +1,14 @@
<template> <template>
<header class="z-30 sticky top-0 left-0 flex justify-center w-full"> <header class="z-30 sticky top-0 left-0 flex justify-center w-full">
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 grid grid-cols-2 lg:grid-cols-3 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-300/10"> <div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 grid grid-cols-2 lg:grid-cols-3 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-300/10">
<Logo /> <Logo />
<div class="hidden grow lg:flex justify-center"> <div class="hidden grow lg:flex justify-center">
<NavBar /> <NavBar />
</div> </div>
<div class="flex justify-end gap-2 items-center"> <div class="flex justify-end gap-2 items-center">
<ThemePicker /> <ThemePicker />
<MobileNavBar /> <MobileNavBar />
</div> </div>
</div> </div>
</header> </header>
</template> </template>

View File

@@ -3,26 +3,26 @@ const appConfig = useAppConfig()
const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`) const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`)
function getGroupColor() { function getGroupColor() {
return `group-hover:text-${appConfig.ui.primary}-500` return `group-hover:text-${appConfig.ui.primary}-500`
} }
</script> </script>
<template> <template>
<NuxtLink <NuxtLink
class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent" class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent"
to="/" to="/"
> >
<span <span
:class="getTextColor" :class="getTextColor"
class="font-black group-hover:text-black dark:group-hover:text-white duration-300" class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
>Arthur</span> >Arthur</span>
<span <span
:class="getGroupColor()" :class="getGroupColor()"
class="font-bold text-gray-300 dark:text-neutral-600 duration-300" class="font-bold text-gray-300 dark:text-neutral-600 duration-300"
>/</span> >/</span>
<span <span
:class="getTextColor" :class="getTextColor"
class="font-black group-hover:text-black dark:group-hover:text-white duration-300" class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
>Danjou</span> >Danjou</span>
</NuxtLink> </NuxtLink>
</template> </template>

View File

@@ -9,171 +9,171 @@ router.afterEach(() => isOpenSidebar.value = false)
const route = useRoute() const route = useRoute()
function isRoute(path: string) { function isRoute(path: string) {
return route.path === path return route.path === path
} }
function openModal() { function openModal() {
isOpenSidebar.value = false isOpenSidebar.value = false
isOpenModal.value = true isOpenModal.value = true
} }
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 }) const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
</script> </script>
<template> <template>
<div class="lg:hidden"> <div class="lg:hidden">
<div class="p-1 rounded-md bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300"> <div class="p-1 rounded-md bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
<UButton <UButton
variant="ghost" variant="ghost"
color="primary" color="primary"
size="sm" size="sm"
icon="i-ph-list-bold" icon="i-ph-list-bold"
@click="isOpenSidebar = true" @click="isOpenSidebar = true"
/> />
</div> </div>
<USlideover v-model="isOpenSidebar"> <USlideover v-model="isOpenSidebar">
<UCard <UCard
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
class="flex flex-col flex-1" class="flex flex-col flex-1"
> >
<template #header> <template #header>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<Logo /> <Logo />
<UButton <UButton
size="md" size="md"
icon="i-ic-round-close" icon="i-ic-round-close"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
@click.prevent="isOpenSidebar = false" @click.prevent="isOpenSidebar = false"
/> />
</div> </div>
</template> </template>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<div <div
v-for="nav in navs" v-for="nav in navs"
:key="nav.label" :key="nav.label"
class="w-full" class="w-full"
> >
<UButton <UButton
v-if="nav.to" v-if="nav.to"
size="sm" size="sm"
class="w-full" class="w-full"
:variant="isRoute(nav.to) ? 'solid' : 'ghost'" :variant="isRoute(nav.to) ? 'solid' : 'ghost'"
color="primary" color="primary"
:to="nav.to" :to="nav.to"
:icon="nav.icon" :icon="nav.icon"
:label="nav.label" :label="nav.label"
/> />
<UButton <UButton
v-else v-else
class="w-full" class="w-full"
size="sm" size="sm"
color="primary" color="primary"
variant="ghost" variant="ghost"
:icon="nav.icon" :icon="nav.icon"
:label="nav.label" :label="nav.label"
@click.prevent="openModal()" @click.prevent="openModal()"
/> />
</div> </div>
</div> </div>
<template #footer> <template #footer>
Footer Footer
</template> </template>
</UCard> </UCard>
</USlideover> </USlideover>
<UModal v-model="isOpenModal"> <UModal v-model="isOpenModal">
<UCard class="p-4"> <UCard class="p-4">
<div> <div>
<div class="mb-8 flex justify-between items-center"> <div class="mb-8 flex justify-between items-center">
<h1 class="text-xl font-bold"> <h1 class="text-xl font-bold">
Contact me Contact me
</h1> </h1>
<UButton <UButton
icon="i-akar-icons-cross" icon="i-akar-icons-cross"
size="xs" size="xs"
variant="ghost" variant="ghost"
@click.prevent="isOpenModal = false" @click.prevent="isOpenModal = false"
/> />
</div> </div>
<div class="flex flex-col space-y-6"> <div class="flex flex-col space-y-6">
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2"> <div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-sm"> <h3 class="text-sm">
Email Email
</h3> </h3>
<p class="text-xs text-subtitle"> <p class="text-xs text-subtitle">
arthurdanjou@outlook.fr arthurdanjou@outlook.fr
</p> </p>
</div> </div>
<div> <div>
<UButtonGroup <UButtonGroup
orientation="horizontal" orientation="horizontal"
size="sm" size="sm"
> >
<UButton <UButton
color="gray" color="gray"
icon="i-mdi-note-edit-outline" icon="i-mdi-note-edit-outline"
label="Compose" label="Compose"
to="mailto:arthurdanjou@outlook.fr" to="mailto:arthurdanjou@outlook.fr"
variant="solid" variant="solid"
/> />
<UButton <UButton
v-if="copied" v-if="copied"
color="green" color="green"
icon="i-mdi-content-copy" icon="i-mdi-content-copy"
label="Copied" label="Copied"
variant="solid" variant="solid"
/> />
<UButton <UButton
v-else v-else
color="gray" color="gray"
icon="i-mdi-content-copy" icon="i-mdi-content-copy"
label="Copy" label="Copy"
variant="solid" variant="solid"
@click.prevent="copy()" @click.prevent="copy()"
/> />
</UButtonGroup> </UButtonGroup>
</div> </div>
</div> </div>
<UDivider label="OR" /> <UDivider label="OR" />
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2"> <div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-sm"> <h3 class="text-sm">
Get in touch Get in touch
</h3> </h3>
<p class="text-xs text-subtitle"> <p class="text-xs text-subtitle">
I'm most active on Twitter I'm most active on Twitter
</p> </p>
</div> </div>
<div> <div>
<UButtonGroup <UButtonGroup
orientation="horizontal" orientation="horizontal"
size="sm" size="sm"
> >
<UButton <UButton
color="gray" color="gray"
icon="i-ph-github-logo-bold" icon="i-ph-github-logo-bold"
label="Github" label="Github"
target="_blank" target="_blank"
to="https://github.com/ArthurDanjou" to="https://github.com/ArthurDanjou"
variant="solid" variant="solid"
/> />
<UButton <UButton
color="gray" color="gray"
icon="i-ph-twitter-logo-bold" icon="i-ph-twitter-logo-bold"
label="Twitter" label="Twitter"
target="_blank" target="_blank"
to="https://twitter.com/ArthurDanj" to="https://twitter.com/ArthurDanj"
variant="solid" variant="solid"
/> />
</UButtonGroup> </UButtonGroup>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</UCard> </UCard>
</UModal> </UModal>
</div> </div>
</template> </template>

View File

@@ -8,163 +8,163 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
</script> </script>
<template> <template>
<nav class="hidden lg:block z-50"> <nav class="hidden lg:block z-50">
<div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300"> <div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
<UButton <UButton
:class="{ 'link-active': route.path === '/' }" :class="{ 'link-active': route.path === '/' }"
color="white" color="white"
size="sm" size="sm"
to="/" to="/"
variant="ghost" variant="ghost"
> >
Home Home
</UButton> </UButton>
<UButton <UButton
:class="{ 'link-active': route.path.includes('/about') }" :class="{ 'link-active': route.path.includes('/about') }"
color="white" color="white"
size="sm" size="sm"
to="/about" to="/about"
variant="ghost" variant="ghost"
> >
About About
</UButton> </UButton>
<!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }"> <!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }">
Articles Articles
</UButton> --> </UButton> -->
<UButton <UButton
:class="{ 'link-active': route.path.includes('/work') }" :class="{ 'link-active': route.path.includes('/work') }"
color="white" color="white"
size="sm" size="sm"
to="/work" to="/work"
variant="ghost" variant="ghost"
> >
Projects Projects
</UButton> </UButton>
<UButton <UButton
:class="{ 'link-active': route.path.includes('/uses') }" :class="{ 'link-active': route.path.includes('/uses') }"
color="white" color="white"
size="sm" size="sm"
to="/uses" to="/uses"
variant="ghost" variant="ghost"
> >
Uses Uses
</UButton> </UButton>
<UDropdown <UDropdown
:items="otherTab" :items="otherTab"
:popper="{ placement: 'bottom' }" :popper="{ placement: 'bottom' }"
mode="hover" mode="hover"
> >
<UButton <UButton
class="duration-300" class="duration-300"
color="white" color="white"
size="sm" size="sm"
variant="ghost" variant="ghost"
> >
Other Other
</UButton> </UButton>
</UDropdown> </UDropdown>
<UButton <UButton
color="white" color="white"
size="sm" size="sm"
variant="ghost" variant="ghost"
@click="isOpenModal = true" @click="isOpenModal = true"
> >
Contact Contact
</UButton> </UButton>
</div> </div>
<UModal v-model="isOpenModal"> <UModal v-model="isOpenModal">
<UCard class="p-4"> <UCard class="p-4">
<div> <div>
<div class="mb-8 flex justify-between items-center"> <div class="mb-8 flex justify-between items-center">
<h1 class="text-xl font-bold"> <h1 class="text-xl font-bold">
Contact me Contact me
</h1> </h1>
<UButton <UButton
icon="i-akar-icons-cross" icon="i-akar-icons-cross"
size="xs" size="xs"
variant="ghost" variant="ghost"
@click.prevent="isOpenModal = false" @click.prevent="isOpenModal = false"
/> />
</div> </div>
<div class="flex flex-col space-y-6"> <div class="flex flex-col space-y-6">
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2"> <div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-sm"> <h3 class="text-sm">
Email Email
</h3> </h3>
<p class="text-xs text-subtitle"> <p class="text-xs text-subtitle">
arthurdanjou@outlook.fr arthurdanjou@outlook.fr
</p> </p>
</div> </div>
<div> <div>
<UButtonGroup <UButtonGroup
orientation="horizontal" orientation="horizontal"
size="sm" size="sm"
> >
<UButton <UButton
color="gray" color="gray"
icon="i-mdi-note-edit-outline" icon="i-mdi-note-edit-outline"
label="Compose" label="Compose"
to="mailto:arthurdanjou@outlook.fr" to="mailto:arthurdanjou@outlook.fr"
variant="solid" variant="solid"
/> />
<UButton <UButton
v-if="copied" v-if="copied"
color="green" color="green"
icon="i-mdi-content-copy" icon="i-mdi-content-copy"
label="Copied" label="Copied"
variant="solid" variant="solid"
/> />
<UButton <UButton
v-else v-else
color="gray" color="gray"
icon="i-mdi-content-copy" icon="i-mdi-content-copy"
label="Copy" label="Copy"
variant="solid" variant="solid"
@click.prevent="copy()" @click.prevent="copy()"
/> />
</UButtonGroup> </UButtonGroup>
</div> </div>
</div> </div>
<UDivider label="OR" /> <UDivider label="OR" />
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2"> <div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-sm"> <h3 class="text-sm">
Get in touch Get in touch
</h3> </h3>
<p class="text-xs text-subtitle"> <p class="text-xs text-subtitle">
I'm most active on Twitter I'm most active on Twitter
</p> </p>
</div> </div>
<div> <div>
<UButtonGroup <UButtonGroup
orientation="horizontal" orientation="horizontal"
size="sm" size="sm"
> >
<UButton <UButton
color="gray" color="gray"
icon="i-ph-github-logo-bold" icon="i-ph-github-logo-bold"
label="Github" label="Github"
target="_blank" target="_blank"
to="https://github.com/ArthurDanjou" to="https://github.com/ArthurDanjou"
variant="solid" variant="solid"
/> />
<UButton <UButton
color="gray" color="gray"
icon="i-ph-twitter-logo-bold" icon="i-ph-twitter-logo-bold"
label="Twitter" label="Twitter"
target="_blank" target="_blank"
to="https://twitter.com/ArthurDanj" to="https://twitter.com/ArthurDanj"
variant="solid" variant="solid"
/> />
</UButtonGroup> </UButtonGroup>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</UCard> </UCard>
</UModal> </UModal>
</nav> </nav>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useColorStore } from '~/store/color' import {useColorStore} from '~/store/color'
import { ColorsTheme } from '~~/types' import {ColorsTheme} from '~~/types'
const colors = Object.values(ColorsTheme) const colors = Object.values(ColorsTheme)
@@ -9,80 +9,80 @@ const { getColor, setColor } = useColorStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const isDark = ref(colorMode.value === 'dark') const isDark = ref(colorMode.value === 'dark')
watch(isDark, () => { watch(isDark, () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark' colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}) })
</script> </script>
<template> <template>
<UPopover <UPopover
mode="hover" mode="hover"
:ui="{ :ui="{
background: 'bg-white dark:bg-stone-900', background: 'bg-white dark:bg-stone-900',
ring: 'ring-1 ring-gray-200 dark:ring-stone-800', ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
container: 'z-30', container: 'z-30',
}" }"
> >
<template #default="{ open }"> <template #default="{ open }">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
square square
size="lg" size="lg"
:class="[open && 'bg-gray-50 dark:bg-gray-800']" :class="[open && 'bg-gray-50 dark:bg-gray-800']"
aria-label="Color picker" aria-label="Color picker"
> >
<UIcon <UIcon
class="w-5 h-5 text-primary-500 dark:text-primary-400" class="w-5 h-5 text-primary-500 dark:text-primary-400"
name="i-ph-paint-brush-bold" name="i-ph-paint-brush-bold"
/> />
</UButton> </UButton>
</template> </template>
<template #panel> <template #panel>
<div class="p-2"> <div class="p-2">
<div class="grid grid-cols-5 gap-px"> <div class="grid grid-cols-5 gap-px">
<UTooltip <UTooltip
v-for="color in colors" v-for="color in colors"
:key="color" :key="color"
:open-delay="500" :open-delay="500"
:text="color" :text="color"
class="capitalize" class="capitalize"
> >
<UButton <UButton
color="gray" color="gray"
square square
:ui="{ :ui="{
color: { color: {
white: { white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800', solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50', ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
}, },
}, },
}" }"
:variant="color === getColor ? 'solid' : 'ghost'" :variant="color === getColor ? 'solid' : 'ghost'"
@click.stop.prevent="setColor(color)" @click.stop.prevent="setColor(color)"
> >
<span <span
:class="`bg-${color}-500/80 border-${color}-500`" :class="`bg-${color}-500/80 border-${color}-500`"
class="flex items-center justify-center w-3 h-3 rounded-full border text-white" class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
> >
<UIcon <UIcon
v-if="color === getColor" v-if="color === getColor"
name="i-ic-round-check" name="i-ic-round-check"
/> />
</span> </span>
</UButton> </UButton>
</UTooltip> </UTooltip>
</div> </div>
<UDivider class="my-2" /> <UDivider class="my-2" />
<div> <div>
<UToggle <UToggle
v-model="isDark" v-model="isDark"
on-icon="i-heroicons-moon-20-solid" on-icon="i-heroicons-moon-20-solid"
off-icon="i-heroicons-sun-20-solid" off-icon="i-heroicons-sun-20-solid"
/> />
</div> </div>
</div> </div>
</template> </template>
</UPopover> </UPopover>
</template> </template>

View File

@@ -5,100 +5,100 @@ const { data: activity, refresh } = await useAsyncData<Activity>('activity', ()
const codingActivity = computed(() => activity.value!.data.activities.filter(activity => IDEs.some(ide => ide.name === activity.name))[0]) const codingActivity = computed(() => activity.value!.data.activities.filter(activity => IDEs.some(ide => ide.name === activity.name))[0])
function formatDate(date: number) { function formatDate(date: number) {
return `${useDateFormat(date, 'DD MMM YYYY').value} at ${useDateFormat(date, 'HH:mm:ss').value}` return `${useDateFormat(date, 'DD MMM YYYY').value} at ${useDateFormat(date, 'HH:mm:ss').value}`
} }
const CardUi = { const CardUi = {
footer: { padding: 'px-4 py-2' }, footer: { padding: 'px-4 py-2' },
body: { base: 'h-full flex items-center' }, body: { base: 'h-full flex items-center' },
} }
useIntervalFn(async () => await refresh(), 5000) useIntervalFn(async () => await refresh(), 5000)
</script> </script>
<template> <template>
<UCard <UCard
:ui="CardUi" :ui="CardUi"
class="flex flex-col justify-between" class="flex flex-col justify-between"
> >
<div <div
v-if="activity && activity.data.activities" v-if="activity && activity.data.activities"
class="flex items-center gap-x-4" class="flex items-center gap-x-4"
> >
<p <p
class="uppercase tracking-widest text-sm" class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }" :style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
> >
Activity Activity
</p> </p>
<div v-if="codingActivity"> <div v-if="codingActivity">
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<UIcon <UIcon
class="h-10 w-10" class="h-10 w-10"
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon" :name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
/> />
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>{{ codingActivity.name }}</h1> <h1>{{ codingActivity.name }}</h1>
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'"> <UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
<div <div
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'" :class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
class="h-3 w-3 inline-flex rounded-full cursor-pointer" class="h-3 w-3 inline-flex rounded-full cursor-pointer"
/> />
</UTooltip> </UTooltip>
</div> </div>
<h3 v-if="codingActivity.details === 'Idling'"> <h3 v-if="codingActivity.details === 'Idling'">
I'm Idling on my computer I'm Idling on my computer
</h3> </h3>
<h3 v-else> <h3 v-else>
{{ codingActivity.details }} - {{ codingActivity.state }} {{ codingActivity.details }} - {{ codingActivity.state }}
</h3> </h3>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-else v-else
class="text-subtitle" class="text-subtitle"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>I'm currently offline</h1> <h1>I'm currently offline</h1>
<UTooltip text="I'm offline 🫥"> <UTooltip text="I'm offline 🫥">
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" /> <div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
</UTooltip> </UTooltip>
</div> </div>
<h3>Come back later to see what I'm doing</h3> <h3>Come back later to see what I'm doing</h3>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex items-center justify-end w-full"> <div class="flex items-center justify-end w-full">
<ClientOnly> <ClientOnly>
<p <p
v-if="codingActivity" v-if="codingActivity"
class="text-subtitle text-xs w-1/2" class="text-subtitle text-xs w-1/2"
> >
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }} Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
</p> </p>
</ClientOnly> </ClientOnly>
<div class="flex items-center space-x-1 w-1/2 justify-end"> <div class="flex items-center space-x-1 w-1/2 justify-end">
<p class="text-subtitle text-xs"> <p class="text-subtitle text-xs">
powered by powered by
</p> </p>
<UButton <UButton
size="xs" size="xs"
:padded="false" :padded="false"
variant="link" variant="link"
to="https://github.com/Phineas/lanyard" to="https://github.com/Phineas/lanyard"
target="_blank" target="_blank"
label="Lanyard" label="Lanyard"
/> />
<UIcon <UIcon
class="text-subtitle" class="text-subtitle"
name="i-jam-thunder" name="i-jam-thunder"
/> />
</div> </div>
</div> </div>
</template> </template>
</UCard> </UCard>
</template> </template>
<style> <style>

View File

@@ -1,49 +1,49 @@
<script setup> <script setup>
const socials = [ const socials = [
{ {
name: 'mail', name: 'mail',
icon: 'i-material-symbols-alternate-email', icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr', link: 'mailto:arthurdanjou@outlook.fr',
}, },
{ {
name: 'twitter', name: 'twitter',
icon: 'i-ph-twitter-logo-bold', icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj', link: 'https://twitter.com/ArthurDanj',
}, },
{ {
name: 'github', name: 'github',
icon: 'i-ph-github-logo-bold', icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou', link: 'https://github.com/ArthurDanjou',
}, },
{ {
name: 'linkedin', name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold', icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/', link: 'https://www.linkedin.com/in/arthurdanjou/',
}, },
] ]
</script> </script>
<template> <template>
<div class="w-container mt-32 mb-24"> <div class="w-container mt-32 mb-24">
<div class="flex items-center flex-col space-y-4"> <div class="flex items-center flex-col space-y-4">
<h1 class="text-center lg:text-6xl sm:text-5xl text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 !leading-tight md:w-2/3"> <h1 class="text-center lg:text-6xl sm:text-5xl text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 !leading-tight md:w-2/3">
Software engineer, mathematics lover and AI enthusiast Software engineer, mathematics lover and AI enthusiast
</h1> </h1>
<p class="leading-relaxed text-subtitle text-center md:w-2/3 p-2"> <p class="leading-relaxed text-subtitle text-center md:w-2/3 p-2">
I'm Arthur, a software engineer passionate about artificial intelligence and the cloud but also a mathematics student living in France. I am currently studying mathematics at the Faculty of Sciences of Paris-Saclay. I'm Arthur, a software engineer passionate about artificial intelligence and the cloud but also a mathematics student living in France. I am currently studying mathematics at the Faculty of Sciences of Paris-Saclay.
</p> </p>
<div class="flex gap-4"> <div class="flex gap-4">
<UButton <UButton
v-for="social in socials" v-for="social in socials"
:key="social.name" :key="social.name"
:icon="social.icon" :icon="social.icon"
size="md" size="md"
:to="social.link" :to="social.link"
variant="ghost" variant="ghost"
target="_blank" target="_blank"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
/> />
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -4,84 +4,84 @@ import type {Stats} from '~~/types'
const stats = await $fetch<Stats>('/api/stats') const stats = await $fetch<Stats>('/api/stats')
const CardUi = { const CardUi = {
footer: { padding: 'px-4 py-2' }, footer: { padding: 'px-4 py-2' },
body: { base: 'h-full' }, body: { base: 'h-full' },
} }
</script> </script>
<template> <template>
<UCard <UCard
:ui="CardUi" :ui="CardUi"
class="flex flex-col justify-between" class="flex flex-col justify-between"
> >
<div class="flex items-center gap-x-4 h-full"> <div class="flex items-center gap-x-4 h-full">
<p <p
class="uppercase tracking-widest text-sm" class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }" :style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
> >
STATS STATS
</p> </p>
<div v-if="stats"> <div v-if="stats">
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<div class="text-md"> <div class="text-md">
<div class="flex items-center gap-x-1"> <div class="flex items-center gap-x-1">
<h3>Total hours:</h3> <h3>Total hours:</h3>
<p class="text-subtitle"> <p class="text-subtitle">
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours {{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
</p> </p>
</div> </div>
<div class="flex items-start gap-x-1 flex-wrap"> <div class="flex items-start gap-x-1 flex-wrap">
<h3>Best Editors:</h3> <h3>Best Editors:</h3>
<p class="text-subtitle"> <p class="text-subtitle">
{{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(', ') }} {{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(', ') }}
</p> </p>
</div> </div>
<div class="flex items-center gap-x-1"> <div class="flex items-center gap-x-1">
<h3>Best OS:</h3> <h3>Best OS:</h3>
<p class="text-subtitle"> <p class="text-subtitle">
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}% {{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
</p> </p>
</div> </div>
<div class="flex items-start gap-x-1 flex-wrap"> <div class="flex items-start gap-x-1 flex-wrap">
<h3>Top languages:</h3> <h3>Top languages:</h3>
<p class="text-subtitle"> <p class="text-subtitle">
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }} {{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<ClientOnly> <ClientOnly>
<p <p
v-if="stats" v-if="stats"
class="text-subtitle text-xs w-1/2" class="text-subtitle text-xs w-1/2"
> >
Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }} Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }}
</p> </p>
</ClientOnly> </ClientOnly>
<div class="flex items-center justify-end space-x-1"> <div class="flex items-center justify-end space-x-1">
<p class="text-subtitle text-xs"> <p class="text-subtitle text-xs">
powered by powered by
</p> </p>
<UButton <UButton
size="xs" size="xs"
:padded="false" :padded="false"
variant="link" variant="link"
to="https://wakatime.com/" to="https://wakatime.com/"
target="_blank" target="_blank"
label="Wakatime" label="Wakatime"
/> />
<UIcon <UIcon
class="text-subtitle" class="text-subtitle"
name="i-jam-thunder" name="i-jam-thunder"
/> />
</div> </div>
</div> </div>
</template> </template>
</UCard> </UCard>
</template> </template>
<style> <style>

View File

@@ -1,31 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps({ defineProps({
startDate: String, startDate: String,
endDate: { endDate: {
type: String, type: String,
required: true, required: true,
}, },
}) })
function formatTodayDate(date: string) { function formatTodayDate(date: string) {
const split = date.split(' ') const split = date.split(' ')
return date === 'Today' ? 'Today' : `${split[0]} ${split[1]}` return date === 'Today' ? 'Today' : `${split[0]} ${split[1]}`
} }
</script> </script>
<template> <template>
<UBadge <UBadge
v-if="startDate !== endDate" v-if="startDate !== endDate"
size="xs" size="xs"
variant="soft" variant="soft"
> >
{{ formatTodayDate(startDate!.toString()) }} {{ formatTodayDate(endDate) }} {{ formatTodayDate(startDate!.toString()) }} {{ formatTodayDate(endDate) }}
</UBadge> </UBadge>
<UBadge <UBadge
v-else v-else
size="xs" size="xs"
variant="soft" variant="soft"
> >
{{ formatTodayDate(endDate) }} {{ formatTodayDate(endDate) }}
</UBadge> </UBadge>
</template> </template>

View File

@@ -2,28 +2,28 @@
import type {Education} from '~~/types' import type {Education} from '~~/types'
defineProps({ defineProps({
education: Object as PropType<Education>, education: Object as PropType<Education>,
}) })
</script> </script>
<template> <template>
<div <div
v-if="education" v-if="education"
class="group relative flex flex-col items-start" class="group relative flex flex-col items-start"
> >
<div class="flex flex-col"> <div class="flex flex-col">
<div> <div>
<DateTag <DateTag
:end-date="education.endDate" :end-date="education.endDate"
:start-date="education.startDate" :start-date="education.startDate"
/> />
</div> </div>
<h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"> <h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{{ education.title }} {{ education.title }}
</h1> </h1>
</div> </div>
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400"> <p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
{{ education.location }} {{ education.description }} {{ education.location }} {{ education.description }}
</p> </p>
</div> </div>
</template> </template>

View File

@@ -2,56 +2,56 @@
import type {WorkExperience} from '~~/types' import type {WorkExperience} from '~~/types'
defineProps({ defineProps({
experience: Object as PropType<WorkExperience>, experience: Object as PropType<WorkExperience>,
}) })
</script> </script>
<template> <template>
<div <div
v-if="experience" v-if="experience"
class="group relative flex flex-col items-start" class="group relative flex flex-col items-start"
> >
<div> <div>
<div class="flex flex-col"> <div class="flex flex-col">
<div> <div>
<DateTag <DateTag
:end-date="experience.endDate" :end-date="experience.endDate"
:start-date="experience.startDate" :start-date="experience.startDate"
/> />
</div> </div>
<div class="flex items-center my-1"> <div class="flex items-center my-1">
<UButton <UButton
v-if="experience.companyLink" v-if="experience.companyLink"
:to="experience.companyLink" :to="experience.companyLink"
variant="link" variant="link"
:padded="false" :padded="false"
color="white" color="white"
size="xl" size="xl"
target="_blank" target="_blank"
:label="experience.company" :label="experience.company"
class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100" class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
> >
<template #leading> <template #leading>
<UIcon <UIcon
color="gray" color="gray"
name="i-akar-icons-link-chain" name="i-akar-icons-link-chain"
/> />
</template> </template>
</UButton> </UButton>
<h1 <h1
v-else v-else
class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100" class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
> >
{{ experience.company }} {{ experience.company }}
</h1> </h1>
<div class="text-subtitle text-xs"> <div class="text-subtitle text-xs">
{{ experience.location }} {{ experience.location }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400"> <p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
{{ experience.title }} {{ experience.description }} {{ experience.title }} {{ experience.description }}
</p> </p>
</div> </div>
</template> </template>

View File

@@ -2,7 +2,7 @@
import type {Skill} from '~~/types' import type {Skill} from '~~/types'
defineProps({ defineProps({
skill: Object as PropType<Skill>, skill: Object as PropType<Skill>,
}) })
const { $colorMode } = useNuxtApp() const { $colorMode } = useNuxtApp()
@@ -10,25 +10,25 @@ const isLight = computed(() => $colorMode.value === 'light')
</script> </script>
<template> <template>
<a <a
v-if="skill" v-if="skill"
:href="skill.link" :href="skill.link"
class="cursor-pointer flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800" class="cursor-pointer flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
> >
<div class="flex items-center"> <div class="flex items-center">
<UIcon <UIcon
v-if="isLight" v-if="isLight"
:name="skill.icon.light ? skill.icon.light : skill.icon" :name="skill.icon.light ? skill.icon.light : skill.icon"
:dynamic="true" :dynamic="true"
size="20" size="20"
/> />
<UIcon <UIcon
v-else v-else
:name="skill.icon.dark ? skill.icon.dark : skill.icon" :name="skill.icon.dark ? skill.icon.dark : skill.icon"
:dynamic="true" :dynamic="true"
size="20" size="20"
/> />
</div> </div>
<span class="text-sm text-subtitle">{{ skill.name }}</span> <span class="text-sm text-subtitle">{{ skill.name }}</span>
</a> </a>
</template> </template>

View File

@@ -1,39 +1,39 @@
import type { Education, Post, Project, Skill, WorkExperience } from '~~/types' import type {Education, Post, Project, Skill, WorkExperience} from '~~/types'
export function getProjects() { export function getProjects() {
return useAsyncData('content:projects', () => { return useAsyncData('content:projects', () => {
return queryContent<Project>('projects').find() return queryContent<Project>('projects').find()
}) })
} }
export function getEducations() { export function getEducations() {
return useAsyncData('content:educations', () => { return useAsyncData('content:educations', () => {
return queryContent<Education>('educations') return queryContent<Education>('educations')
.sort({ .sort({
endDate: -1, endDate: -1,
}) })
.find() .find()
}) })
} }
export function getWorkExperiences() { export function getWorkExperiences() {
return useAsyncData('content:experiences', async () => { return useAsyncData('content:experiences', async () => {
const experiences = await queryContent<WorkExperience>('experiences').find() const experiences = await queryContent<WorkExperience>('experiences').find()
return experiences.sort((a, b) => { return experiences.sort((a, b) => {
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime() return new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
}) })
}) })
} }
export function getSkills() { export function getSkills() {
return useAsyncData('content:skills', () => queryContent<Skill[]>('skills').findOne()) return useAsyncData('content:skills', () => queryContent<Skill[]>('skills').findOne())
} }
export function getPosts() { export function getPosts() {
return useAsyncData('content:posts', async () => { return useAsyncData('content:posts', async () => {
const posts = await queryContent<Post>('writing').find() const posts = await queryContent<Post>('writing').find()
return posts.sort((a, b) => { return posts.sort((a, b) => {
return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime() return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
}) })
}) })
} }

View File

@@ -1,12 +1,12 @@
import {defineConfig} from 'drizzle-kit' import {defineConfig} from 'drizzle-kit'
export default defineConfig({ export default defineConfig({
driver: 'pg', driver: 'pg',
schema: './server/database/schema.ts', schema: './server/database/schema.ts',
out: './server/database/migrations', out: './server/database/migrations',
dbCredentials: { dbCredentials: {
connectionString: process.env.DATABASE_URL as string, connectionString: process.env.DATABASE_URL as string,
}, },
strict: true, strict: true,
verbose: true, verbose: true,
}) })

View File

@@ -2,7 +2,7 @@
import type {NuxtError} from 'nuxt/app' import type {NuxtError} from 'nuxt/app'
defineProps({ defineProps({
error: Object as () => NuxtError, error: Object as () => NuxtError,
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
@@ -10,46 +10,46 @@ const getColor = computed(() => appConfig.ui.primary)
</script> </script>
<template> <template>
<section> <section>
<NuxtLoadingIndicator :color="getColor" /> <NuxtLoadingIndicator :color="getColor" />
<section class="fixed inset-0 flex justify-center sm:px-8"> <section class="fixed inset-0 flex justify-center sm:px-8">
<div class="flex w-full max-w-9xl"> <div class="flex w-full max-w-9xl">
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" /> <div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div> </div>
</section> </section>
<div class="relative z-50 min-h-[100svh]"> <div class="relative z-50 min-h-[100svh]">
<Header /> <Header />
<UContainer> <UContainer>
<div class="flex flex-col items-center gap-4 mt-12"> <div class="flex flex-col items-center gap-4 mt-12">
<h1 <h1
class="font-medium text-[8rem] md:text-[16rem] leading-none bg-error bg-clip-text tracking-wider font-error" class="font-medium text-[8rem] md:text-[16rem] leading-none bg-error bg-clip-text tracking-wider font-error"
:class="`text-${getColor}-500`" :class="`text-${getColor}-500`"
> >
{{ error?.statusCode }} {{ error?.statusCode }}
</h1> </h1>
<p class="text-lg md:text-2xl text-subtitle text-center"> <p class="text-lg md:text-2xl text-subtitle text-center">
Sorry, {{ error?.statusCode === 404 Sorry, {{ error?.statusCode === 404
? "the page you are looking for doesn't exist or as been moved." ? "the page you are looking for doesn't exist or as been moved."
: "you have encountered a problem." : "you have encountered a problem."
}} }}
<br> <br>
Let's find a better place for you to go. Let's find a better place for you to go.
</p> </p>
</div> </div>
<div class="flex justify-center mt-8 mb-12"> <div class="flex justify-center mt-8 mb-12">
<UButton <UButton
to="/" to="/"
size="md" size="md"
variant="soft" variant="soft"
color="primary" color="primary"
> >
Go back to the main page Go back to the main page
</UButton> </UButton>
</div> </div>
</UContainer> </UContainer>
<Footer /> <Footer />
</div> </div>
</section> </section>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,7 +1,7 @@
import withNuxt from './.nuxt/eslint.config.mjs' import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({ export default withNuxt({
rules: { rules: {
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
}, },
}) })

View File

@@ -3,29 +3,29 @@ import {SpeedInsights} from '@vercel/speed-insights/nuxt'
const router = useRouter() const router = useRouter()
router.afterEach((route) => { router.afterEach((route) => {
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
}) })
useHead({ useHead({
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }], link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
}) })
</script> </script>
<template> <template>
<div> <div>
<SpeedInsights /> <SpeedInsights />
<Background /> <Background />
<NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" /> <NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" />
<section class="fixed inset-0 flex justify-center sm:px-8"> <section class="fixed inset-0 flex justify-center sm:px-8">
<div class="flex w-full max-w-9xl"> <div class="flex w-full max-w-9xl">
<div class="w-full z-20 bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" /> <div class="w-full z-20 bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div> </div>
</section> </section>
<main class="relative z-50 min-h-[100svh]"> <main class="relative z-50 min-h-[100svh]">
<Header /> <Header />
<NuxtPage /> <NuxtPage />
<Footer /> <Footer />
</main> </main>
<UNotifications /> <UNotifications />
</div> </div>
</template> </template>

View File

@@ -4,19 +4,19 @@ const getColor = computed(() => appConfig.ui.primary)
</script> </script>
<template> <template>
<div> <div>
<NuxtLoadingIndicator :color="getColor" /> <NuxtLoadingIndicator :color="getColor" />
<section class="fixed inset-0 flex justify-center sm:px-8"> <section class="fixed inset-0 flex justify-center sm:px-8">
<div class="flex w-full max-w-9xl"> <div class="flex w-full max-w-9xl">
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" /> <div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div> </div>
</section> </section>
<div class="relative z-50 min-h-[100svh]"> <div class="relative z-50 min-h-[100svh]">
<Header :navigation="false" /> <Header :navigation="false" />
<UContainer> <UContainer>
<NuxtPage /> <NuxtPage />
</UContainer> </UContainer>
<Footer /> <Footer />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,24 +1,24 @@
export default defineNuxtRouteMiddleware(async (to) => { export default defineNuxtRouteMiddleware(async (to) => {
const isMaintenance = ref<boolean>(true) const isMaintenance = ref<boolean>(true)
try { try {
await $fetch('/api/maintenance').then((maintenance) => { await $fetch('/api/maintenance').then((maintenance) => {
isMaintenance.value = maintenance.enabled isMaintenance.value = maintenance.enabled
}) })
} }
catch (error) { catch (error) {
return navigateTo('/maintenance') return navigateTo('/maintenance')
} }
if (isMaintenance.value && to.path !== '/maintenance') { if (isMaintenance.value && to.path !== '/maintenance') {
return navigateTo('/maintenance', { return navigateTo('/maintenance', {
redirectCode: 301, redirectCode: 301,
}) })
} }
if (!isMaintenance.value && to.path === '/maintenance') { if (!isMaintenance.value && to.path === '/maintenance') {
return navigateTo('/', { return navigateTo('/', {
redirectCode: 301, redirectCode: 301,
replace: true, replace: true,
}) })
} }
}) })

View File

@@ -1,7 +1,7 @@
export default defineNuxtRouteMiddleware((to) => { export default defineNuxtRouteMiddleware((to) => {
if (to.path === '/writing' && process.env.NODE_ENV !== 'development') { if (to.path === '/writing' && process.env.NODE_ENV !== 'development') {
return navigateTo('/', { return navigateTo('/', {
redirectCode: 301, redirectCode: 301,
}) })
} }
}) })

View File

@@ -2,19 +2,19 @@ import {defineNuxtModule} from 'nuxt/kit'
import {addCustomTab} from '@nuxt/devtools-kit' import {addCustomTab} from '@nuxt/devtools-kit'
export default defineNuxtModule({ export default defineNuxtModule({
meta: { meta: {
name: 'drizzle-studio', name: 'drizzle-studio',
version: '0.0.1', version: '0.0.1',
}, },
setup() { setup() {
addCustomTab({ addCustomTab({
name: 'drizzle-studio', name: 'drizzle-studio',
title: 'Drizzle Studio', title: 'Drizzle Studio',
icon: 'simple-icons:drizzle', icon: 'simple-icons:drizzle',
view: { view: {
type: 'iframe', type: 'iframe',
src: 'https://local.drizzle.studio/?themeId=azX2nOTScT9U6SWEmlq7z', src: 'https://local.drizzle.studio/?themeId=azX2nOTScT9U6SWEmlq7z',
}, },
}) })
}, },
}) })

View File

@@ -1,80 +1,80 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
css: [ css: [
'@/assets/css/main.scss', '@/assets/css/main.scss',
], ],
app: { app: {
pageTransition: { name: 'page', mode: 'out-in' }, pageTransition: { name: 'page', mode: 'out-in' },
}, },
modules: [ modules: [
'@nuxt/content', '@nuxt/content',
'@nuxtjs/seo', '@nuxtjs/seo',
'nuxt-auth-utils', 'nuxt-auth-utils',
'@nuxthq/studio', '@nuxthq/studio',
'@pinia/nuxt', '@pinia/nuxt',
'@pinia-plugin-persistedstate/nuxt', '@pinia-plugin-persistedstate/nuxt',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxt/ui', '@nuxt/ui',
'@nuxt/eslint', '@nuxt/eslint',
], ],
colorMode: { colorMode: {
preference: 'light', preference: 'light',
fallback: 'light', fallback: 'light',
classPrefix: '', classPrefix: '',
classSuffix: '', classSuffix: '',
}, },
components: [ components: [
'components/', 'components/',
'components/header', 'components/header',
'components/resume', 'components/resume',
'components/main', 'components/main',
], ],
content: { content: {
highlight: { highlight: {
theme: 'github-dark', theme: 'github-dark',
}, },
}, },
eslint: { eslint: {
config: { config: {
stylistic: { stylistic: {
indent: 2, indent: 2,
semi: false, semi: false,
blockSpacing: true, blockSpacing: true,
}, },
}, },
}, },
ui: { ui: {
icons: 'all', icons: 'all',
}, },
devtools: { devtools: {
enabled: true, enabled: true,
timeline: { timeline: {
enabled: true, enabled: true,
}, },
}, },
site: { site: {
url: 'https://arthurdanjou.fr', url: 'https://arthurdanjou.fr',
name: 'Arthur Danjou\'s website', name: 'Arthur Danjou\'s website',
description: 'I\'m Arthur DANJOU, a developer enjoying Cloud Infrastructure and Artificial Intelligence. Mathematics Student at Paris-Saclay', description: 'I\'m Arthur DANJOU, a developer enjoying Cloud Infrastructure and Artificial Intelligence. Mathematics Student at Paris-Saclay',
}, },
runtimeConfig: { runtimeConfig: {
discordUserId: process.env.NUXT_DISCORD_USER_ID, discordUserId: process.env.NUXT_DISCORD_USER_ID,
discordId: process.env.NUXT_DISCORD_ID, discordId: process.env.NUXT_DISCORD_ID,
discordToken: process.env.NUXT_DISCORD_TOKEN, discordToken: process.env.NUXT_DISCORD_TOKEN,
wakatimeUserId: process.env.NUXT_WAKATIME_USER_UD, wakatimeUserId: process.env.NUXT_WAKATIME_USER_UD,
wakatimeCodig: process.env.NUXT_WAKATIME_CODING, wakatimeCodig: process.env.NUXT_WAKATIME_CODING,
wakatimeEditors: process.env.NUXT_WAKATIME_EDITORS, wakatimeEditors: process.env.NUXT_WAKATIME_EDITORS,
wakatimeLanguages: process.env.NUXT_WAKATIME_LANGUAGES, wakatimeLanguages: process.env.NUXT_WAKATIME_LANGUAGES,
wakatimeOs: process.env.NUXT_WAKATIME_OS, wakatimeOs: process.env.NUXT_WAKATIME_OS,
}, },
}) })

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
useHead({ useHead({
title: 'About me • Arthur Danjou', title: 'About me • Arthur Danjou',
}) })
const { data: skills } = await getSkills() const { data: skills } = await getSkills()
@@ -9,100 +9,100 @@ const { data: experiences } = await getWorkExperiences()
</script> </script>
<template> <template>
<section class="w-container lg:my-24 my-16"> <section class="w-container lg:my-24 my-16">
<div class="px-4 grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12"> <div class="px-4 grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div class="lg:pl-20 flex justify-center"> <div class="lg:pl-20 flex justify-center">
<div class="max-w-xs px-2.5 lg:max-w-none"> <div class="max-w-xs px-2.5 lg:max-w-none">
<UTooltip <UTooltip
:popper="{ offsetDistance: 20 }" :popper="{ offsetDistance: 20 }"
text="It's me 👋" text="It's me 👋"
> >
<img <img
alt="My main profile picture" alt="My main profile picture"
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800" class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
src="/about.png" src="/about.png"
> >
</UTooltip> </UTooltip>
</div> </div>
</div> </div>
<div class="lg:order-first lg:row-span-2"> <div class="lg:order-first lg:row-span-2">
<div class="max-w-2xl space-y-8 mb-16"> <div class="max-w-2xl space-y-8 mb-16">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
I'm Arthur, I live and study in France where I learn new things. I'm Arthur, I live and study in France where I learn new things.
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
As a software engineer with a passion for AI and the cloud, I have a deep understanding of emerging technologies that are transforming the way businesses and organizations operate. I am at the heart of an ever-changing and rapidly growing field. My background in mathematics also gives me an edge in understanding the mathematical concepts and theories behind these technologies as well as how to design them. As a software engineer with a passion for AI and the cloud, I have a deep understanding of emerging technologies that are transforming the way businesses and organizations operate. I am at the heart of an ever-changing and rapidly growing field. My background in mathematics also gives me an edge in understanding the mathematical concepts and theories behind these technologies as well as how to design them.
</p> </p>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
I enjoy sharing my knowledge and learning new theorems and technologies. I am a curious person and eager to continue learning and growing throughout your life. My passion and commitment to these subjects are admirable qualities and will help me succeed in my career and education. I enjoy sharing my knowledge and learning new theorems and technologies. I am a curious person and eager to continue learning and growing throughout your life. My passion and commitment to these subjects are admirable qualities and will help me succeed in my career and education.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<GridSection title="Interests"> <GridSection title="Interests">
<GridSlot title="Development"> <GridSlot title="Development">
Development is the passion that appeared the earliest in my life. I started developing on Minecraft and then I migrated to the broad field of the web. Development is the passion that appeared the earliest in my life. I started developing on Minecraft and then I migrated to the broad field of the web.
</GridSlot> </GridSlot>
<GridSlot title="Mathematics"> <GridSlot title="Mathematics">
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field. During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
</GridSlot> </GridSlot>
<GridSlot title="Artificial Intelligence"> <GridSlot title="Artificial Intelligence">
We hear more and more about artificial intelligence with the evolution of our society. So I quickly got interested by doing my own research and I quickly discovered that this field is closely related to mathematics, hence my interest. We hear more and more about artificial intelligence with the evolution of our society. So I quickly got interested by doing my own research and I quickly discovered that this field is closely related to mathematics, hence my interest.
</GridSlot> </GridSlot>
<GridSlot title="Cloud and infrastructure"> <GridSlot title="Cloud and infrastructure">
When you're doing development and deploying projects online, you discover and are forced to touch the cloud, infrastructure, and network. It's a totally different field than the others but just as interesting. When you're doing development and deploying projects online, you discover and are forced to touch the cloud, infrastructure, and network. It's a totally different field than the others but just as interesting.
</GridSlot> </GridSlot>
<GridSlot title="Fitness"> <GridSlot title="Fitness">
In addition to my studies and programming, I go to the gym every day to relax and stay in shape. Sport allows me to recharge my batteries and move on to other things. In addition to my studies and programming, I go to the gym every day to relax and stay in shape. Sport allows me to recharge my batteries and move on to other things.
</GridSlot> </GridSlot>
</GridSection> </GridSection>
<GridSection <GridSection
v-if="skills" v-if="skills"
title="Skills" title="Skills"
> >
<div class="grid grid-cols-3 md:grid-cols-4 gap-2"> <div class="grid grid-cols-3 md:grid-cols-4 gap-2">
<Skill <Skill
v-for="skill in skills.body" v-for="skill in skills.body"
:key="skill.name" :key="skill.name"
:skill="skill" :skill="skill"
/> />
</div> </div>
</GridSection> </GridSection>
<GridSection <GridSection
v-if="experiences" v-if="experiences"
title="Work Experiences" title="Work Experiences"
> >
<Experience <Experience
v-for="experience in experiences" v-for="experience in experiences"
:key="experience.title" :key="experience.title"
:experience="experience" :experience="experience"
/> />
</GridSection> </GridSection>
<GridSection <GridSection
v-if="educations" v-if="educations"
title="Educations" title="Educations"
> >
<Education <Education
v-for="education in educations" v-for="education in educations"
:key="education.title" :key="education.title"
:education="education" :education="education"
/> />
</GridSection> </GridSection>
<div class="flex justify-center"> <div class="flex justify-center">
<UTooltip <UTooltip
:popper="{ offsetDistance: 20 }" :popper="{ offsetDistance: 20 }"
text="Click to discover my journey" text="Click to discover my journey"
> >
<UButton <UButton
label="Download my CV" label="Download my CV"
icon="i-material-symbols-lab-profile-outline-rounded" icon="i-material-symbols-lab-profile-outline-rounded"
color="primary" color="primary"
variant="outline" variant="outline"
size="xl" size="xl"
to="/resume.pdf" to="/resume.pdf"
target="_blank" target="_blank"
/> />
</UTooltip> </UTooltip>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -2,185 +2,185 @@
import {useBookmarksStore} from '~/store/bookmarks' import {useBookmarksStore} from '~/store/bookmarks'
useHead({ useHead({
title: 'Discover my library • Arthur Danjou', title: 'Discover my library • Arthur Danjou',
}) })
const categories = ref<Array<{ label: string, slug: string }>>([{ label: 'All', slug: 'all' }]) const categories = ref<Array<{ label: string, slug: string }>>([{ label: 'All', slug: 'all' }])
const { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore() const { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', { const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
method: 'get', method: 'get',
query: { query: {
favorite: isFavorite, favorite: isFavorite,
category: getCategory, category: getCategory,
}, },
watch: [isFavorite, getCategory], watch: [isFavorite, getCategory],
}) })
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } }) const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } })
getCategories.value!.forEach(category => categories.value.push({ label: category.name, slug: category.slug })) getCategories.value!.forEach(category => categories.value.push({ label: category.name, slug: category.slug }))
function isCategory(slug: string) { function isCategory(slug: string) {
return getCategory.value === slug return getCategory.value === slug
} }
const getMarkerStyle = computed(() => { const getMarkerStyle = computed(() => {
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all') const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
return { return {
top: `${selected?.offsetTop}px`, top: `${selected?.offsetTop}px`,
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`, left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
height: `${selected?.offsetHeight}px`, height: `${selected?.offsetHeight}px`,
width: `${selected?.offsetWidth}px`, width: `${selected?.offsetWidth}px`,
} }
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
function getColor() { function getColor() {
return `text-${appConfig.ui.primary}-500` return `text-${appConfig.ui.primary}-500`
} }
</script> </script>
<template> <template>
<section class="w-container lg:my-24 my-16"> <section class="w-container lg:my-24 my-16">
<div class="max-w-2xl space-y-8 mb-16"> <div class="max-w-2xl space-y-8 mb-16">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
My library where I save some resources My library where I save some resources
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
You will find a selection of some of the most inspiring and complete content I have read through my research and work experience. You will find a selection of some of the most inspiring and complete content I have read through my research and work experience.
</p> </p>
</div> </div>
<div <div
v-if="getCategories" v-if="getCategories"
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between" class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
> >
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative"> <div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
<ClientOnly> <ClientOnly>
<div <div
class="absolute duration-300 left-1 ease-out focus:outline-none" class="absolute duration-300 left-1 ease-out focus:outline-none"
:style="[getMarkerStyle]" :style="[getMarkerStyle]"
> >
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" /> <div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
</div> </div>
</ClientOnly> </ClientOnly>
<div <div
v-for="category in categories" v-for="category in categories"
:id="category.slug" :id="category.slug"
:key="category.slug" :key="category.slug"
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white" class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }" :class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
@click.prevent="setCategory(category.slug)" @click.prevent="setCategory(category.slug)"
> >
<p class="w-full"> <p class="w-full">
{{ category.label }} {{ category.label }}
</p> </p>
</div> </div>
</div> </div>
<UPopover> <UPopover>
<UButton <UButton
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'" :icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
color="primary" color="primary"
variant="soft" variant="soft"
size="lg" size="lg"
/> />
<template #panel> <template #panel>
<div <div
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle" class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
@click.prevent="toggleFavorite()" @click.prevent="toggleFavorite()"
> >
<UIcon <UIcon
v-if="isFavorite" v-if="isFavorite"
name="i-material-symbols-check-box-outline-rounded" name="i-material-symbols-check-box-outline-rounded"
/> />
<UIcon <UIcon
v-else v-else
name="i-material-symbols-check-box-outline-blank" name="i-material-symbols-check-box-outline-blank"
/> />
<p>Show favorites only</p> <p>Show favorites only</p>
</div> </div>
</template> </template>
</UPopover> </UPopover>
</div> </div>
<UDivider class="my-2" /> <UDivider class="my-2" />
<div <div
v-if="bookmarks && getCategories" v-if="bookmarks && getCategories"
class="mt-8" class="mt-8"
> >
<div <div
v-if="bookmarks.length > 0 && !pending" v-if="bookmarks.length > 0 && !pending"
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3" class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
> >
<div <div
v-for="bookmark in bookmarks" v-for="bookmark in bookmarks"
:key="bookmark.name.toLowerCase().trim()" :key="bookmark.name.toLowerCase().trim()"
class="group relative flex justify-between items-center" class="group relative flex justify-between items-center"
> >
<div class="flex flex-col gap-y-1"> <div class="flex flex-col gap-y-1">
<div class="flex gap-6 items-center"> <div class="flex gap-6 items-center">
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100"> <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<NuxtLink <NuxtLink
:href="bookmark.website" :href="bookmark.website"
external external
target="_blank" target="_blank"
> >
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<h1 class="relative z-10"> <h1 class="relative z-10">
{{ bookmark.name }} {{ bookmark.name }}
</h1> </h1>
<UTooltip <UTooltip
v-if="bookmark.favorite" v-if="bookmark.favorite"
text="You can set the filter to only show favorites." text="You can set the filter to only show favorites."
> >
<UIcon <UIcon
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
name="i-ic-round-star" name="i-ic-round-star"
/> />
</UTooltip> </UTooltip>
</div> </div>
</NuxtLink> </NuxtLink>
</h2> </h2>
</div> </div>
<div class="flex gap-2 z-10"> <div class="flex gap-2 z-10">
<UBadge <UBadge
v-for="category in bookmark.bookmarkCategories" v-for="category in bookmark.bookmarkCategories"
:key="category.category.slug" :key="category.category.slug"
color="primary" color="primary"
variant="soft" variant="soft"
size="xs" size="xs"
> >
{{ category.category.name }} {{ category.category.name }}
</UBadge> </UBadge>
</div> </div>
</div> </div>
<p <p
:class="getColor()" :class="getColor()"
class="relative z-10 flex text-sm font-medium items-center" class="relative z-10 flex text-sm font-medium items-center"
> >
<UIcon name="i-ph-link-bold" /> <UIcon name="i-ph-link-bold" />
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span> <span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
</p> </p>
</div> </div>
</div> </div>
<div <div
v-else-if="bookmarks?.length === 0 && !pending" v-else-if="bookmarks?.length === 0 && !pending"
class="my-4 text-subtitle" class="my-4 text-subtitle"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-akar-icons-cross" /> <UIcon name="i-akar-icons-cross" />
<p>There are no bookmarks for this category. Maybe soon...</p> <p>There are no bookmarks for this category. Maybe soon...</p>
</div> </div>
</div> </div>
<div <div
v-else v-else
class="my-4 text-subtitle" class="my-4 text-subtitle"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" /> <UIcon name="i-eos-icons-loading" />
<p>The bookmarks are loading...</p> <p>The bookmarks are loading...</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -2,7 +2,7 @@
import {providers} from '~~/types' import {providers} from '~~/types'
useHead({ useHead({
title: 'Sign my guestbook • Arthur Danjou', title: 'Sign my guestbook • Arthur Danjou',
}) })
const { loggedIn, clear, user } = useUserSession() const { loggedIn, clear, user } = useUserSession()
@@ -13,196 +13,196 @@ const isOpen = ref(false)
const toast = useToast() const toast = useToast()
const messageContent = ref<string>('') const messageContent = ref<string>('')
async function sign() { async function sign() {
if (messageContent.value.length < 7 || messageContent.value.length > 250) if (messageContent.value.length < 7 || messageContent.value.length > 250)
return return
isOpen.value = false isOpen.value = false
await $fetch('/api/message', { await $fetch('/api/message', {
method: 'post', method: 'post',
body: { body: {
message: messageContent.value, message: messageContent.value,
}, },
}).then(async () => { }).then(async () => {
toast.add({ toast.add({
title: `Thanks for leaving a message!`, title: `Thanks for leaving a message!`,
description: 'Your can see it at the top of the messages.', description: 'Your can see it at the top of the messages.',
icon: 'i-material-symbols-check-circle-outline-rounded', icon: 'i-material-symbols-check-circle-outline-rounded',
timeout: 4000, timeout: 4000,
}) })
await refresh() await refresh()
}).catch(() => { }).catch(() => {
toast.add({ toast.add({
title: 'An error occurred when signing the book!', title: 'An error occurred when signing the book!',
color: 'red', color: 'red',
}) })
}) })
messageContent.value = '' messageContent.value = ''
} }
async function deleteMessage(id: number) { async function deleteMessage(id: number) {
if (!user.value.admin) if (!user.value.admin)
return return
await $fetch('/api/message', { await $fetch('/api/message', {
method: 'delete', method: 'delete',
body: { body: {
id, id,
}, },
}).then(async () => { }).then(async () => {
toast.add({ toast.add({
title: `Message successfully deleted`, title: `Message successfully deleted`,
icon: 'i-material-symbols-check-circle-outline-rounded', icon: 'i-material-symbols-check-circle-outline-rounded',
color: 'green', color: 'green',
timeout: 4000, timeout: 4000,
}) })
await refresh() await refresh()
}).catch(() => { }).catch(() => {
toast.add({ toast.add({
title: 'An error occured when deleting a message!', title: 'An error occured when deleting a message!',
color: 'red', color: 'red',
}) })
}) })
} }
</script> </script>
<template> <template>
<section class="w-container lg:mt-24 my-8"> <section class="w-container lg:mt-24 my-8">
<div class="max-w-2xl space-y-8 md:mb-16 mb-8"> <div class="max-w-2xl space-y-8 md:mb-16 mb-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
You want to leave a message ? You want to leave a message ?
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
Your opinion means a lot to me. Feel free to share your impressions of my projects, explore my site, or simply leave a personalised message. Your comments are a source of inspiration and continuous improvement. Thank you for taking the time to contribute to this virtual community. I look forward to reading what you have to share! Your opinion means a lot to me. Feel free to share your impressions of my projects, explore my site, or simply leave a personalised message. Your comments are a source of inspiration and continuous improvement. Thank you for taking the time to contribute to this virtual community. I look forward to reading what you have to share!
</p> </p>
</div> </div>
<div class="flex justify-center md:justify-start"> <div class="flex justify-center md:justify-start">
<UButton <UButton
class="mb-8 md:mb-16" class="mb-8 md:mb-16"
label="Want to sign my book ?" label="Want to sign my book ?"
icon="i-ph-circle-wavy-question-bold" icon="i-ph-circle-wavy-question-bold"
@click.prevent="isOpen = true" @click.prevent="isOpen = true"
/> />
</div> </div>
<UModal v-model="isOpen"> <UModal v-model="isOpen">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1 <h1
v-if="loggedIn" v-if="loggedIn"
class="text-md font-bold" class="text-md font-bold"
> >
Enter just below your message to sign my book Enter just below your message to sign my book
</h1> </h1>
<h1 <h1
v-else v-else
class="text-md font-bold" class="text-md font-bold"
> >
Sign before writing your message Sign before writing your message
</h1> </h1>
</div> </div>
<UButton <UButton
class="-my-1" class="-my-1"
color="gray" color="gray"
icon="i-heroicons-x-mark-20-solid" icon="i-heroicons-x-mark-20-solid"
variant="ghost" variant="ghost"
@click="isOpen = false" @click="isOpen = false"
/> />
</div> </div>
</template> </template>
<div> <div>
<div <div
v-if="loggedIn" v-if="loggedIn"
class="flex items-center justify-between gap-4" class="flex items-center justify-between gap-4"
> >
<div class="w-full relative flex items-center"> <div class="w-full relative flex items-center">
<input <input
v-model="messageContent" v-model="messageContent"
type="text" type="text"
required required
min="7" min="7"
max="58" max="58"
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800" class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
placeholder="Leave a message" placeholder="Leave a message"
> >
<UButton <UButton
class="absolute right-1 top-1 rounded-md" class="absolute right-1 top-1 rounded-md"
label="Send" label="Send"
:disabled="messageContent.trim().length < 7 || messageContent.trim().length > 250" :disabled="messageContent.trim().length < 7 || messageContent.trim().length > 250"
variant="solid" variant="solid"
@click.prevent="sign()" @click.prevent="sign()"
/> />
</div> </div>
<UButton <UButton
variant="outline" variant="outline"
@click.prevent="clear()" @click.prevent="clear()"
> >
Logout Logout
</UButton> </UButton>
</div> </div>
<div <div
v-else v-else
class="flex gap-2 justify-center" class="flex gap-2 justify-center"
> >
<UButton <UButton
v-for="provider in providers" v-for="provider in providers"
:key="provider.slug" :key="provider.slug"
:label="provider.label" :label="provider.label"
color="black" color="black"
variant="solid" variant="solid"
:to="provider.link" :to="provider.link"
:icon="provider.icon" :icon="provider.icon"
:external="true" :external="true"
/> />
</div> </div>
</div> </div>
</UCard> </UCard>
</UModal> </UModal>
<div <div
v-if="messages" v-if="messages"
class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8" class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8"
> >
<div <div
v-for="message in messages" v-for="message in messages"
:key="message.id" :key="message.id"
class="relative overflow-hidden sm:p-6 px-4 py-5 border border-zinc-100 p-6 dark:border-zinc-700/40 rounded-lg" class="relative overflow-hidden sm:p-6 px-4 py-5 border border-zinc-100 p-6 dark:border-zinc-700/40 rounded-lg"
> >
<p class="text-sm text-subtitle"> <p class="text-sm text-subtitle">
{{ message.message }} {{ message.message }}
</p> </p>
<div class="flex items-center gap-4 mt-4"> <div class="flex items-center gap-4 mt-4">
<div class="h-8 w-8 rounded-full"> <div class="h-8 w-8 rounded-full">
<img <img
:src="message.image" :src="message.image"
alt="Author profile picture" alt="Author profile picture"
class="w-full h-full rounded-full" class="w-full h-full rounded-full"
> >
</div> </div>
<p class="font-bold"> <p class="font-bold">
{{ message.username }} {{ message.username }}
</p> </p>
</div> </div>
<UButton <UButton
v-if="user && user.admin" v-if="user && user.admin"
class="absolute top-1 right-1" class="absolute top-1 right-1"
icon="i-material-symbols-delete-forever-outline-rounded" icon="i-material-symbols-delete-forever-outline-rounded"
color="red" color="red"
variant="ghost" variant="ghost"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
size="xs" size="xs"
@click.prevent="deleteMessage(message.id)" @click.prevent="deleteMessage(message.id)"
/> />
</div> </div>
</div> </div>
<div <div
v-else v-else
class="my-4 text-subtitle" class="my-4 text-subtitle"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" /> <UIcon name="i-eos-icons-loading" />
<p>The messages are loading...</p> <p>The messages are loading...</p>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<style> <style>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
useHead({ useHead({
title: 'Arthur Danjou • Software Engineer and Maths Lover', title: 'Arthur Danjou • Software Engineer and Maths Lover',
}) })
</script> </script>
<template> <template>
<section> <section>
<Announcement /> <Announcement />
<MainBanner /> <MainBanner />
<div class="px-4 lg:px-44 md:px-16 sm:px-8 w-full my-16 grid grid-cols-1 md:grid-cols-2 md:gap-x-16 gap-y-4 md:gap-y-16"> <div class="px-4 lg:px-44 md:px-16 sm:px-8 w-full my-16 grid grid-cols-1 md:grid-cols-2 md:gap-x-16 gap-y-4 md:gap-y-16">
<MainActivity /> <MainActivity />
<MainStats /> <MainStats />
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
definePageMeta({ definePageMeta({
layout: 'maintenance', layout: 'maintenance',
}) })
useHead({ useHead({
title: 'Site under maintenance • Arthur Danjou', title: 'Site under maintenance • Arthur Danjou',
}) })
const { data: maintenance } = await useFetch('/api/maintenance') const { data: maintenance } = await useFetch('/api/maintenance')
@@ -14,68 +14,68 @@ const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`) const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
const socials = [ const socials = [
{ {
name: 'mail', name: 'mail',
icon: 'i-material-symbols-alternate-email', icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr', link: 'mailto:arthurdanjou@outlook.fr',
}, },
{ {
name: 'twitter', name: 'twitter',
icon: 'i-ph-twitter-logo-bold', icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj', link: 'https://twitter.com/ArthurDanj',
}, },
{ {
name: 'github', name: 'github',
icon: 'i-ph-github-logo-bold', icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou', link: 'https://github.com/ArthurDanjou',
}, },
{ {
name: 'linkedin', name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold', icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/', link: 'https://www.linkedin.com/in/arthurdanjou/',
}, },
] ]
</script> </script>
<template> <template>
<section class="w-full min-h-[80svh] flex justify-center items-center"> <section class="w-full min-h-[80svh] flex justify-center items-center">
<div class="text-center space-y-8 max-w-5xl"> <div class="text-center space-y-8 max-w-5xl">
<h3 class="uppercase text-xs text-transparent bg-clip-text bg-origin-content bg-gradient-to-b from-gray-100 to-gray-300 dark:from-zinc-600 to-55% dark:to-zinc-800"> <h3 class="uppercase text-xs text-transparent bg-clip-text bg-origin-content bg-gradient-to-b from-gray-100 to-gray-300 dark:from-zinc-600 to-55% dark:to-zinc-800">
Coming back soon Coming back soon
</h3> </h3>
<h1 class="text-4xl md:text-7xl font-bold"> <h1 class="text-4xl md:text-7xl font-bold">
The website is under maintenance The website is under maintenance
</h1> </h1>
<div v-if="maintenance && maintenance.maintenance"> <div v-if="maintenance && maintenance.maintenance">
<p <p
:class="getColor" :class="getColor"
class="font-bold mb-8 text-xl" class="font-bold mb-8 text-xl"
> >
{{ maintenance.maintenance.reason }} {{ maintenance.maintenance.reason }}
</p> </p>
<div> <div>
<p class="text-subtitle italic"> <p class="text-subtitle italic">
Maintenance planned from {{ useDateFormat(maintenance.maintenance.beginAt, format).value }} to {{ useDateFormat(maintenance.maintenance.endAt, format).value }} Maintenance planned from {{ useDateFormat(maintenance.maintenance.beginAt, format).value }} to {{ useDateFormat(maintenance.maintenance.endAt, format).value }}
</p> </p>
</div> </div>
</div> </div>
<div class="flex justify-center items-center gap-4"> <div class="flex justify-center items-center gap-4">
<a <a
v-for="social in socials" v-for="social in socials"
:key="social.name" :key="social.name"
:href="social.link" :href="social.link"
class="link" class="link"
target="_blank" target="_blank"
> >
<span <span
:class="social.icon" :class="social.icon"
aria-hidden="true" aria-hidden="true"
class="flex-shrink-0 h-5 w-5" class="flex-shrink-0 h-5 w-5"
/> />
</a> </a>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTalentsStore } from '~/store/talents' import {useTalentsStore} from '~/store/talents'
import { providers } from '~~/types' import {providers} from '~~/types'
useHead({ useHead({
title: 'Discover new talents • Arthur Danjou', title: 'Discover new talents • Arthur Danjou',
}) })
const categories = ref<Array<{ label: string, slug: string, id: number }>>([{ label: 'All', slug: 'all', id: 0 }]) const categories = ref<Array<{ label: string, slug: string, id: number }>>([{ label: 'All', slug: 'all', id: 0 }])
@@ -11,297 +11,297 @@ const { getCategory, setCategory, isFavorite, toggleFavorite } = useTalentsStore
const { loggedIn, clear } = useUserSession() const { loggedIn, clear } = useUserSession()
const { data: talents, pending } = await useFetch('/api/talents', { const { data: talents, pending } = await useFetch('/api/talents', {
method: 'get', method: 'get',
query: { query: {
favorite: isFavorite, favorite: isFavorite,
category: getCategory, category: getCategory,
}, },
watch: [isFavorite, getCategory], watch: [isFavorite, getCategory],
}) })
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } }) const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } })
getCategories.value!.forEach(category => categories.value.push({ getCategories.value!.forEach(category => categories.value.push({
label: category.name, label: category.name,
slug: category.slug, slug: category.slug,
id: category.id, id: category.id,
})) }))
function isCategory(slug: string) { function isCategory(slug: string) {
return getCategory.value === slug return getCategory.value === slug
} }
const getMarkerStyle = computed(() => { const getMarkerStyle = computed(() => {
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all') const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
return { return {
top: `${selected?.offsetTop}px`, top: `${selected?.offsetTop}px`,
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`, left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
height: `${selected?.offsetHeight}px`, height: `${selected?.offsetHeight}px`,
width: `${selected?.offsetWidth}px`, width: `${selected?.offsetWidth}px`,
} }
}) })
const appConfig = useAppConfig() const appConfig = useAppConfig()
function getColor() { function getColor() {
return `text-${appConfig.ui.primary}-500` return `text-${appConfig.ui.primary}-500`
} }
const isOpen = ref(false) const isOpen = ref(false)
const toast = useToast() const toast = useToast()
const suggestContent = ref<string>('') const suggestContent = ref<string>('')
async function suggest() { async function suggest() {
if (suggestContent.value.trim().length < 4) if (suggestContent.value.trim().length < 4)
return return
isOpen.value = false isOpen.value = false
await $fetch('/api/suggestion', { await $fetch('/api/suggestion', {
method: 'post', method: 'post',
body: { body: {
content: suggestContent.value, content: suggestContent.value,
}, },
}).then((response) => { }).then((response) => {
toast.add({ toast.add({
title: `Your suggestion for '${response[0].content}' has been successfully added`, title: `Your suggestion for '${response[0].content}' has been successfully added`,
color: 'green', color: 'green',
icon: 'i-material-symbols-check-circle-outline-rounded', icon: 'i-material-symbols-check-circle-outline-rounded',
timeout: 4000, timeout: 4000,
}) })
}).catch(() => { }).catch(() => {
toast.add({ toast.add({
title: 'An error occurred when suggesting someone', title: 'An error occurred when suggesting someone',
color: 'red', color: 'red',
}) })
}) })
suggestContent.value = '' suggestContent.value = ''
} }
</script> </script>
<template> <template>
<section class="w-container lg:mt-24 my-8"> <section class="w-container lg:mt-24 my-8">
<div class="max-w-2xl space-y-8 md:mb-16 mb-8"> <div class="max-w-2xl space-y-8 md:mb-16 mb-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
Showcasing here, I aim to share and introduce inspiring talents. Showcasing here, I aim to share and introduce inspiring talents.
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
You will find a selection of some of the most inspiring web talents I have discovered through my research and work experience. These talents are creative designers, talented web developers, passionate open-source contributors, and much more. You will find a selection of some of the most inspiring web talents I have discovered through my research and work experience. These talents are creative designers, talented web developers, passionate open-source contributors, and much more.
</p> </p>
</div> </div>
<div class="flex justify-center md:justify-start"> <div class="flex justify-center md:justify-start">
<UButton <UButton
class="mb-8 md:mb-16" class="mb-8 md:mb-16"
label="Want to suggest someone ?" label="Want to suggest someone ?"
icon="i-ph-circle-wavy-question-bold" icon="i-ph-circle-wavy-question-bold"
@click.prevent="isOpen = true" @click.prevent="isOpen = true"
/> />
</div> </div>
<UModal v-model="isOpen"> <UModal v-model="isOpen">
<UCard> <UCard>
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1 class="text-md font-bold"> <h1 class="text-md font-bold">
Are you a web talent? Do you want to promote your project? Do you want to launch your career or gain visibility? Are you a web talent? Do you want to promote your project? Do you want to launch your career or gain visibility?
</h1> </h1>
</div> </div>
<UButton <UButton
class="-my-1" class="-my-1"
color="gray" color="gray"
icon="i-heroicons-x-mark-20-solid" icon="i-heroicons-x-mark-20-solid"
variant="ghost" variant="ghost"
@click="isOpen = false" @click="isOpen = false"
/> />
</div> </div>
</template> </template>
<div> <div>
<div <div
v-if="loggedIn" v-if="loggedIn"
class="flex items-center justify-between gap-4" class="flex items-center justify-between gap-4"
> >
<div class="w-full relative flex items-center"> <div class="w-full relative flex items-center">
<input <input
v-model="suggestContent" v-model="suggestContent"
type="text" type="text"
required required
min="4" min="4"
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800" class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
placeholder="Suggest one name" placeholder="Suggest one name"
> >
<UButton <UButton
class="absolute right-1 top-1 rounded-md" class="absolute right-1 top-1 rounded-md"
label="Send" label="Send"
:disabled="suggestContent.trim().length < 4" :disabled="suggestContent.trim().length < 4"
variant="solid" variant="solid"
@click.prevent="suggest()" @click.prevent="suggest()"
/> />
</div> </div>
<UButton <UButton
variant="outline" variant="outline"
@click.prevent="clear()" @click.prevent="clear()"
> >
Logout Logout
</UButton> </UButton>
</div> </div>
<div <div
v-else v-else
class="flex gap-2 justify-center" class="flex gap-2 justify-center"
> >
<UButton <UButton
v-for="provider in providers" v-for="provider in providers"
:key="provider.slug" :key="provider.slug"
:label="provider.label" :label="provider.label"
color="black" color="black"
variant="solid" variant="solid"
:to="provider.link" :to="provider.link"
:icon="provider.icon" :icon="provider.icon"
:external="true" :external="true"
/> />
</div> </div>
</div> </div>
</UCard> </UCard>
</UModal> </UModal>
<div class="sticky z-40 top-[4.55rem] left-0 z-100 bg-white dark:bg-gray-900 pt-2"> <div class="sticky z-40 top-[4.55rem] left-0 z-100 bg-white dark:bg-gray-900 pt-2">
<div <div
v-if="getCategories" v-if="getCategories"
class="flex gap-2 w-full items-center justify-between" class="flex gap-2 w-full items-center justify-between"
> >
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative"> <div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
<ClientOnly> <ClientOnly>
<div <div
class="absolute duration-300 left-1 ease-out focus:outline-none" class="absolute duration-300 left-1 ease-out focus:outline-none"
:style="[getMarkerStyle]" :style="[getMarkerStyle]"
> >
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" /> <div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
</div> </div>
</ClientOnly> </ClientOnly>
<div <div
v-for="category in categories" v-for="category in categories"
:id="category.slug" :id="category.slug"
:key="category.slug" :key="category.slug"
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white" class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }" :class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
@click.prevent="setCategory(category.slug)" @click.prevent="setCategory(category.slug)"
> >
<p class="w-full"> <p class="w-full">
{{ category.label }} {{ category.label }}
</p> </p>
</div> </div>
</div> </div>
<UPopover> <UPopover>
<UButton <UButton
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'" :icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
color="primary" color="primary"
variant="soft" variant="soft"
size="lg" size="lg"
/> />
<template #panel> <template #panel>
<div <div
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle" class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
@click.prevent="toggleFavorite()" @click.prevent="toggleFavorite()"
> >
<UIcon <UIcon
v-if="isFavorite" v-if="isFavorite"
name="i-material-symbols-check-box-outline-rounded" name="i-material-symbols-check-box-outline-rounded"
/> />
<UIcon <UIcon
v-else v-else
name="i-material-symbols-check-box-outline-blank" name="i-material-symbols-check-box-outline-blank"
/> />
<p>Show favorites only</p> <p>Show favorites only</p>
</div> </div>
</template> </template>
</UPopover> </UPopover>
</div> </div>
<UDivider class="my-2" /> <UDivider class="my-2" />
</div> </div>
<div <div
v-if="talents && getCategories" v-if="talents && getCategories"
class="mt-8" class="mt-8"
> >
<div <div
v-if="talents.length > 0 && !pending" v-if="talents.length > 0 && !pending"
class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3" class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
> >
<div <div
v-for="talent in talents" v-for="talent in talents"
:key="talent.name.toLowerCase().trim()" :key="talent.name.toLowerCase().trim()"
class="group relative flex flex-col justify-between" class="group relative flex flex-col justify-between"
> >
<div class="flex"> <div class="flex">
<div class="flex gap-6 items-center"> <div class="flex gap-6 items-center">
<img <img
:src="talent.logo" :src="talent.logo"
alt="Talent profile picture" alt="Talent profile picture"
class="z-20 h-8 w-8 md:h-12 md:w-12 rounded-md" class="z-20 h-8 w-8 md:h-12 md:w-12 rounded-md"
> >
<div> <div>
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100"> <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<NuxtLink <NuxtLink
:to="talent.website" :to="talent.website"
target="_blank" target="_blank"
> >
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<h1 class="relative z-10"> <h1 class="relative z-10">
{{ talent.name }} {{ talent.name }}
</h1> </h1>
<UTooltip <UTooltip
v-if="talent.favorite" v-if="talent.favorite"
text="You can set the filter to only show favorites." text="You can set the filter to only show favorites."
> >
<UIcon <UIcon
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
name="i-ic-round-star" name="i-ic-round-star"
/> />
</UTooltip> </UTooltip>
</div> </div>
</NuxtLink> </NuxtLink>
</h2> </h2>
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400"> <p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
{{ talent.work }} {{ talent.work }}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center gap-4 mt-2"> <div class="flex items-center gap-4 mt-2">
<p <p
:class="getColor()" :class="getColor()"
class="relative z-10 flex text-xs md:text-sm font-medium items-center" class="relative z-10 flex text-xs md:text-sm font-medium items-center"
> >
<UIcon name="i-ph-link-bold" /> <UIcon name="i-ph-link-bold" />
<span class="ml-2">{{ talent.website.replace('https://', '').replace('/', '') }}</span> <span class="ml-2">{{ talent.website.replace('https://', '').replace('/', '') }}</span>
</p> </p>
<div class="flex gap-2 z-10 flex-wrap"> <div class="flex gap-2 z-10 flex-wrap">
<UBadge <UBadge
v-for="category in talent.talentCategories" v-for="category in talent.talentCategories"
:key="category.category.slug" :key="category.category.slug"
color="primary" color="primary"
variant="soft" variant="soft"
size="xs" size="xs"
> >
{{ category.category.name }} {{ category.category.name }}
</UBadge> </UBadge>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-else-if="talents?.length === 0 && !pending" v-else-if="talents?.length === 0 && !pending"
class="my-4 text-subtitle" class="my-4 text-subtitle"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-akar-icons-cross" /> <UIcon name="i-akar-icons-cross" />
<p>There are no talents for this category. Maybe soon...</p> <p>There are no talents for this category. Maybe soon...</p>
</div> </div>
</div> </div>
<div <div
v-else v-else
class="my-4 text-subtitle" class="my-4 text-subtitle"
> >
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" /> <UIcon name="i-eos-icons-loading" />
<p>The talents are loading...</p> <p>The talents are loading...</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,21 +1,21 @@
<template> <template>
<section class="w-container lg:my-24 my-16"> <section class="w-container lg:my-24 my-16">
<div class="max-w-2xl space-y-8"> <div class="max-w-2xl space-y-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
Software I use, Hardware I own, and my favorite stack Software I use, Hardware I own, and my favorite stack
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
I get often asked what I use to create software, to play games or to work and learn. Here's a big list of all my favourite things. I get often asked what I use to create software, to play games or to work and learn. Here's a big list of all my favourite things.
</p> </p>
<ClientOnly> <ClientOnly>
<ContentDoc <ContentDoc
class="my-16" class="my-16"
path="/uses" path="/uses"
/> />
<template #fallback> <template #fallback>
<USkeleton class="w-full h-1/2" /> <USkeleton class="w-full h-1/2" />
</template> </template>
</ClientOnly> </ClientOnly>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,64 +1,64 @@
<script lang="ts" setup> <script lang="ts" setup>
useHead({ useHead({
title: 'My work • Arthur Danjou', title: 'My work • Arthur Danjou',
}) })
const { data: projects } = await getProjects() const { data: projects } = await getProjects()
</script> </script>
<template> <template>
<section class="w-container lg:my-24 my-16"> <section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8"> <div class="px-4 max-w-3xl space-y-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
All my projects can be found on GitHub and by scrolling down. All my projects can be found on GitHub and by scrolling down.
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
I've worked on tons of little projects over the years but these are the ones that I'm most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved. I've worked on tons of little projects over the years but these are the ones that I'm most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved.
</p> </p>
</div> </div>
<div class="mt-16 md:mt-20"> <div class="mt-16 md:mt-20">
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
<div <div
v-for="project in projects" v-for="project in projects"
:key="project.name" :key="project.name"
class="group relative flex flex-col justify-between" class="group relative flex flex-col justify-between"
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0"> <div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<UIcon <UIcon
:name="project.icon" :name="project.icon"
dynamic dynamic
size="24" size="24"
/> />
</div> </div>
<div> <div>
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100"> <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
<div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" /> <div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<NuxtLink <NuxtLink
:to="project.link" :to="project.link"
target="_blank" target="_blank"
> >
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10">{{ project.title }}</span> <span class="relative z-10">{{ project.title }}</span>
</NuxtLink> </NuxtLink>
</h2> </h2>
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400"> <p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
{{ project.description }} {{ project.description }}
</p> </p>
</div> </div>
</div> </div>
<div class="mt-2 flex gap-2 z-10 flex-wrap"> <div class="mt-2 flex gap-2 z-10 flex-wrap">
<UBadge <UBadge
v-for="tag in project.tags" v-for="tag in project.tags"
:key="tag" :key="tag"
color="primary" color="primary"
variant="soft" variant="soft"
size="xs" size="xs"
> >
{{ tag }} {{ tag }}
</UBadge> </UBadge>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -7,169 +7,169 @@ const appConfig = useAppConfig()
const route = useRoute() const route = useRoute()
const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne()) const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne())
const { const {
data: post, data: post,
} = await useFetch<PrismaPost>('/api/article', { } = await useFetch<PrismaPost>('/api/article', {
method: 'post', method: 'post',
body: { body: {
slug: route.params.slug.toString(), slug: route.params.slug.toString(),
}, },
}) })
const likes = ref(post.value?.likes) const likes = ref(post.value?.likes)
async function like() { async function like() {
const data = await $fetch<PrismaPost>('/api/like', { const data = await $fetch<PrismaPost>('/api/like', {
method: 'PUT', method: 'PUT',
body: { body: {
slug: post.value?.slug, slug: post.value?.slug,
}, },
}) })
likes.value = data.likes likes.value = data.likes
} }
if (!postContent.value) { if (!postContent.value) {
throw showError({ throw showError({
statusMessage: 'The post you are looking for was not found.', statusMessage: 'The post you are looking for was not found.',
statusCode: 404, statusCode: 404,
}) })
} }
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '') const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
useHead({ useHead({
title: `${postContent.value?.title} • Arthur Danjou's shelf`, title: `${postContent.value?.title} • Arthur Danjou's shelf`,
}) })
function top() { function top() {
window.scrollTo({ window.scrollTo({
top: 0, top: 0,
left: 0, left: 0,
behavior: 'smooth', behavior: 'smooth',
}) })
} }
const { copy, copied } = useClipboard({ const { copy, copied } = useClipboard({
source: `https://arthurdanjou.fr/writing/${route.params.slug}`, source: `https://arthurdanjou.fr/writing/${route.params.slug}`,
copiedDuring: 4000, copiedDuring: 4000,
}) })
const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, { const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, {
maxAge: 604_800, maxAge: 604_800,
}) })
async function handleLike() { async function handleLike() {
await like() await like()
likeCookie.value = true likeCookie.value = true
} }
</script> </script>
<template> <template>
<section <section
v-if="postContent && post" v-if="postContent && post"
class="w-container lg:mt-24 mt-16" class="w-container lg:mt-24 mt-16"
> >
<div class="lg:relative"> <div class="lg:relative">
<div class="max-w-3xl space-y-8 mx-auto"> <div class="max-w-3xl space-y-8 mx-auto">
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
<UButton <UButton
icon="i-ph-arrow-circle-left-bold" icon="i-ph-arrow-circle-left-bold"
variant="soft" variant="soft"
size="lg" size="lg"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
class="lg:absolute left-0 mb-8" class="lg:absolute left-0 mb-8"
@click.prevent="useRouter().back()" @click.prevent="useRouter().back()"
/> />
<article> <article>
<header class="flex flex-col space-y-6"> <header class="flex flex-col space-y-6">
<time class="flex items-center text-base text-zinc-400 dark:text-zinc-500"> <time class="flex items-center text-base text-zinc-400 dark:text-zinc-500">
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" /> <span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<div class="ml-3 flex gap-3"> <div class="ml-3 flex gap-3">
<div> <div>
{{ format(postContent.publishedAt) }} {{ format(postContent.publishedAt) }}
</div> </div>
<span></span> <span></span>
<div>{{ postContent.readingMins }} min</div> <div>{{ postContent.readingMins }} min</div>
<span></span> <span></span>
<div>{{ post.views }} {{ post.views > 1 ? 'views' : 'view' }}</div> <div>{{ post.views }} {{ post.views > 1 ? 'views' : 'view' }}</div>
</div> </div>
</time> </time>
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{{ postContent.title }} {{ postContent.title }}
</h1> </h1>
<p class="text-subtitle"> <p class="text-subtitle">
{{ postContent.description }} {{ postContent.description }}
</p> </p>
</header> </header>
<div <div
v-if="postContent.cover" v-if="postContent.cover"
class="w-full rounded-md my-8" class="w-full rounded-md my-8"
> >
{{ postContent.cover }} {{ postContent.cover }}
</div> </div>
<ClientOnly> <ClientOnly>
<ContentRenderer <ContentRenderer
class="mt-12 prose dark:prose-invert max-w-none prose-style" class="mt-12 prose dark:prose-invert max-w-none prose-style"
:class="`prose-${appConfig.ui.primary}`" :class="`prose-${appConfig.ui.primary}`"
:value="postContent" :value="postContent"
/> />
<template #fallback> <template #fallback>
<div class="my-16 text-subtitle"> <div class="my-16 text-subtitle">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" /> <UIcon name="i-eos-icons-loading" />
<p>The content of the post is loading...</p> <p>The content of the post is loading...</p>
</div> </div>
</div> </div>
</template> </template>
</ClientOnly> </ClientOnly>
<footer class="my-8 space-y-8"> <footer class="my-8 space-y-8">
<UDivider /> <UDivider />
<p class="text-subtitle"> <p class="text-subtitle">
Thanks for reading this post! If you liked it, please consider sharing it with your friends. <strong>Don't forget to leave a like!</strong> Thanks for reading this post! If you liked it, please consider sharing it with your friends. <strong>Don't forget to leave a like!</strong>
</p> </p>
<div class="flex gap-4 flex-wrap"> <div class="flex gap-4 flex-wrap">
<UButton <UButton
:label="`${likes} ${likes! > 1 ? 'likes' : 'like'}`" :label="`${likes} ${likes! > 1 ? 'likes' : 'like'}`"
icon="i-ph-heart-bold" icon="i-ph-heart-bold"
size="lg" size="lg"
variant="soft" variant="soft"
@click.prevent="handleLike()" @click.prevent="handleLike()"
/> />
<UButton <UButton
label="Go to top" label="Go to top"
icon="i-ph-arrow-up-bold" icon="i-ph-arrow-up-bold"
size="lg" size="lg"
variant="soft" variant="soft"
@click.prevent="top()" @click.prevent="top()"
/> />
<UButton <UButton
label="Share on Twitter" label="Share on Twitter"
icon="i-ph-twitter-logo-bold" icon="i-ph-twitter-logo-bold"
size="lg" size="lg"
variant="soft" variant="soft"
@click.prevent="copy()" @click.prevent="copy()"
/> />
<UButton <UButton
v-if="copied" v-if="copied"
label="Link copied" label="Link copied"
icon="i-lucide-clipboard-check" icon="i-lucide-clipboard-check"
color="green" color="green"
size="lg" size="lg"
variant="soft" variant="soft"
@click.prevent="copy()" @click.prevent="copy()"
/> />
<UButton <UButton
v-else v-else
label="Copy link" label="Copy link"
icon="i-lucide-clipboard" icon="i-lucide-clipboard"
size="lg" size="lg"
variant="soft" variant="soft"
@click.prevent="copy()" @click.prevent="copy()"
/> />
</div> </div>
</footer> </footer>
</article> </article>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@@ -3,7 +3,7 @@ const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`) const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
useHead({ useHead({
title: 'My Shelf • Arthur Danjou', title: 'My Shelf • Arthur Danjou',
}) })
const { data: posts } = await getPosts() const { data: posts } = await getPosts()
@@ -11,59 +11,59 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
</script> </script>
<template> <template>
<section class="w-container lg:my-24 my-16"> <section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8"> <div class="px-4 max-w-3xl space-y-8">
<div> <div>
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight"> <h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
Writing on my life, development and my passions. Writing on my life, development and my passions.
</h1> </h1>
<p class="leading-relaxed text-subtitle"> <p class="leading-relaxed text-subtitle">
All my thoughts on programming, mathematics, artificial intelligence design, etc., are put together in chronological order. I also write about my projects, my discoveries, and my thoughts. <s>It is sometimes updated.</s> All my thoughts on programming, mathematics, artificial intelligence design, etc., are put together in chronological order. I also write about my projects, my discoveries, and my thoughts. <s>It is sometimes updated.</s>
</p> </p>
</div> </div>
</div> </div>
<div class="mt-16 md:mt-20"> <div class="mt-16 md:mt-20">
<div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40"> <div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div class="flex max-w-3xl flex-col space-y-16"> <div class="flex max-w-3xl flex-col space-y-16">
<article <article
v-for="post in posts" v-for="post in posts"
:key="post.slug" :key="post.slug"
class="px-6 md:grid md:grid-cols-4 md:items-baseline" class="px-6 md:grid md:grid-cols-4 md:items-baseline"
> >
<div class="group md:col-span-3 group relative flex flex-col items-start"> <div class="group md:col-span-3 group relative flex flex-col items-start">
<h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"> <h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
<div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" /> <div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" />
<NuxtLink :to="post._path"> <NuxtLink :to="post._path">
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" /> <span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10"> <span class="relative z-10">
{{ post.title }} {{ post.title }}
</span> </span>
</NuxtLink> </NuxtLink>
</h2> </h2>
<time class="md:hidden relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500 pl-3.5"> <time class="md:hidden relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500 pl-3.5">
<span class="absolute inset-y-0 left-0 flex items-center"> <span class="absolute inset-y-0 left-0 flex items-center">
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" /> <span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span> </span>
{{ format(post.publishedAt) }} {{ format(post.publishedAt) }}
</time> </time>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400"> <p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{{ post.description }} {{ post.description }}
</p> </p>
<div <div
:class="getColor" :class="getColor"
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium" class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
> >
<p>Read article</p> <p>Read article</p>
<UIcon name="i-ph-arrow-circle-right-bold" /> <UIcon name="i-ph-arrow-circle-right-bold" />
</div> </div>
</div> </div>
<time class="mt-1 md:block relative z-10 order-first mb-3 hidden text-sm text-zinc-400 dark:text-zinc-500"> <time class="mt-1 md:block relative z-10 order-first mb-3 hidden text-sm text-zinc-400 dark:text-zinc-500">
<p>{{ format(post.publishedAt) }}</p> <p>{{ format(post.publishedAt) }}</p>
<p>{{ post.readingMins }} min.</p> <p>{{ post.readingMins }} min.</p>
</time> </time>
</article> </article>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>

View File

@@ -1,5 +1,5 @@
import { inject } from '@vercel/analytics' import {inject} from '@vercel/analytics'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
inject() inject()
}) })

View File

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

View File

@@ -1,5 +1,5 @@
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
return useDB().query.announcements.findFirst({ return useDB().query.announcements.findFirst({
orderBy: (announcement, { asc }) => [asc(announcement.createdAt)], orderBy: (announcement, { asc }) => [asc(announcement.createdAt)],
}) })
}) })

View File

@@ -3,14 +3,14 @@ import {z} from 'zod'
const PostSchema = z.object({ slug: z.string() }).parse const PostSchema = z.object({ slug: z.string() }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { slug } = await readValidatedBody(event, PostSchema) const { slug } = await readValidatedBody(event, PostSchema)
return useDB().insert(tables.posts).values({ return useDB().insert(tables.posts).values({
slug, slug,
}).onConflictDoUpdate({ }).onConflictDoUpdate({
target: tables.posts.id, target: tables.posts.id,
set: { set: {
views: sql`${tables.posts.views} views: sql`${tables.posts.views}
+ 1`, + 1`,
}, },
}) })
}) })

View File

@@ -1,20 +1,20 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { favorite, category } = getQuery(event) const { favorite, category } = getQuery(event)
const bookmarks = await useDB().query.bookmarks const bookmarks = await useDB().query.bookmarks
.findMany({ .findMany({
orderBy: [asc(tables.talents.id)], orderBy: [asc(tables.talents.id)],
with: { with: {
bookmarkCategories: { bookmarkCategories: {
with: { with: {
category: true, category: true,
}, },
}, },
}, },
}) })
return bookmarks.filter(bookmark => return bookmarks.filter(bookmark =>
(category === 'all' || bookmark.bookmarkCategories.some(cat => cat.category.slug === category)) (category === 'all' || bookmark.bookmarkCategories.some(cat => cat.category.slug === category))
&& (favorite === 'false' || bookmark.favorite), && (favorite === 'false' || bookmark.favorite),
) )
}) })

View File

@@ -1,4 +1,4 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event) const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
return useDB().select().from(tables.categories).where(eq(tables.categories.type, type)) return useDB().select().from(tables.categories).where(eq(tables.categories.type, type))
}) })

View File

@@ -3,11 +3,11 @@ import {z} from 'zod'
const PostSchema = z.object({ slug: z.string() }).parse const PostSchema = z.object({ slug: z.string() }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { slug } = await readValidatedBody(event, PostSchema) const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts) return useDB().update(tables.posts)
.set({ .set({
likes: sql`${tables.posts.likes} likes: sql`${tables.posts.likes}
+ 1`, + 1`,
}) })
.where(eq(tables.posts.slug, slug)) .where(eq(tables.posts.slug, slug))
}) })

View File

@@ -1,22 +1,22 @@
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
const maintenance = await useDB().query.maintenances.findFirst({ const maintenance = await useDB().query.maintenances.findFirst({
orderBy: [asc(tables.maintenances.createdAt)], orderBy: [asc(tables.maintenances.createdAt)],
}) })
let enabled = true let enabled = true
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
enabled = false enabled = false
} }
else { else {
const today = new Date() const today = new Date()
enabled = !!maintenance enabled = !!maintenance
&& maintenance.enabled && maintenance.enabled
&& new Date(maintenance.beginAt).getTime() < today.getTime() && new Date(maintenance.beginAt).getTime() < today.getTime()
&& new Date(maintenance.endAt).getTime() > today.getTime() && new Date(maintenance.endAt).getTime() > today.getTime()
} }
return { return {
enabled, enabled,
maintenance, maintenance,
} }
}) })

View File

@@ -1,13 +1,13 @@
import {z} from 'zod' import {z} from 'zod'
const MessageValidator = z.object({ const MessageValidator = z.object({
id: z.number(), id: z.number(),
}).parse }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { id } = await readValidatedBody(event, MessageValidator) const { id } = await readValidatedBody(event, MessageValidator)
const { user } = await requireUserSession(event) const { user } = await requireUserSession(event)
if (!user.admin) if (!user.admin)
throw createError({ statusCode: 400, message: 'You need the permission to delete a message!' }) throw createError({ statusCode: 400, message: 'You need the permission to delete a message!' })
return useDB().delete(tables.guestbookMessages).where(eq(tables.guestbookMessages.id, id)) return useDB().delete(tables.guestbookMessages).where(eq(tables.guestbookMessages.id, id))
}) })

View File

@@ -1,30 +1,30 @@
import {z} from 'zod' import {z} from 'zod'
const MessageValidator = z.object({ const MessageValidator = z.object({
message: z.string(), message: z.string(),
}).parse }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { message } = await readValidatedBody(event, MessageValidator) const { message } = await readValidatedBody(event, MessageValidator)
const { user } = await requireUserSession(event) const { user } = await requireUserSession(event)
const config = useRuntimeConfig(event) const config = useRuntimeConfig(event)
await sendDiscordWebhookMessage(config, { await sendDiscordWebhookMessage(config, {
title: 'New guestbook message ✨', title: 'New guestbook message ✨',
description: `**${user.username}** has signed the book : "*${message}*"`, description: `**${user.username}** has signed the book : "*${message}*"`,
color: 15893567, color: 15893567,
}) })
return useDB().insert(tables.guestbookMessages) return useDB().insert(tables.guestbookMessages)
.values({ .values({
message, message,
email: user.email, email: user.email,
username: user.username, username: user.username,
image: user.picture, image: user.picture,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: tables.guestbookMessages.email, target: tables.guestbookMessages.email,
set: { set: {
message, message,
}, },
}) })
}) })

View File

@@ -1,3 +1,3 @@
export default defineEventHandler(async () => { export default defineEventHandler(async () => {
return useDB().select().from(tables.guestbookMessages).orderBy(asc(tables.guestbookMessages.createdAt)) return useDB().select().from(tables.guestbookMessages).orderBy(asc(tables.guestbookMessages.createdAt))
}) })

View File

@@ -1,15 +1,15 @@
export default defineCachedEventHandler(async (event) => { export default defineCachedEventHandler(async (event) => {
const config = useRuntimeConfig(event) const config = useRuntimeConfig(event)
const coding = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeCodig}.json`) const coding = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeCodig}.json`)
const editors = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeEditors}.json`) const editors = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeEditors}.json`)
const os = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeOs}.json`) const os = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeOs}.json`)
const languages = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeLanguages}.json`) const languages = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeLanguages}.json`)
return { return {
coding, coding,
editors, editors,
os, os,
languages, languages,
} }
}, { }, {
maxAge: 60 * 60 * 3, // 3 hours, maxAge: 60 * 60 * 3, // 3 hours,
}) })

View File

@@ -1,34 +1,34 @@
import {z} from 'zod' import {z} from 'zod'
const SuggestionValidator = z.object({ const SuggestionValidator = z.object({
content: z.string(), content: z.string(),
}).parse }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { content } = await readValidatedBody(event, SuggestionValidator) const { content } = await readValidatedBody(event, SuggestionValidator)
const { user } = await requireUserSession(event) const { user } = await requireUserSession(event)
const config = useRuntimeConfig(event) const config = useRuntimeConfig(event)
await sendDiscordWebhookMessage(config, { await sendDiscordWebhookMessage(config, {
title: 'New suggestion ✨', title: 'New suggestion ✨',
description: `**${user.username}** has requested **${content}** for the talents page.`, description: `**${user.username}** has requested **${content}** for the talents page.`,
color: 15237114, color: 15237114,
}) })
return useDB().insert(tables.suggestions) return useDB().insert(tables.suggestions)
.values({ .values({
email: user.email, email: user.email,
content, content,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: tables.suggestions.email, target: tables.suggestions.email,
set: { set: {
content, content,
}, },
setWhere: sql`${tables.suggestions.email} setWhere: sql`${tables.suggestions.email}
= =
${user.email}`, ${user.email}`,
}).returning({ }).returning({
content: tables.suggestions.content, content: tables.suggestions.content,
}) })
}) })

View File

@@ -1,20 +1,20 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { favorite, category } = getQuery(event) const { favorite, category } = getQuery(event)
const talents = await useDB().query.talents const talents = await useDB().query.talents
.findMany({ .findMany({
orderBy: [asc(tables.talents.id)], orderBy: [asc(tables.talents.id)],
with: { with: {
talentCategories: { talentCategories: {
with: { with: {
category: true, category: true,
}, },
}, },
}, },
}) })
return talents.filter(talent => return talents.filter(talent =>
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category)) (category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
&& (favorite === 'false' || talent.favorite), && (favorite === 'false' || talent.favorite),
) )
}) })

View File

@@ -1,13 +1,13 @@
import { z } from 'zod' import {z} from 'zod'
const PostSchema = z.object({ slug: z.string() }).parse const PostSchema = z.object({ slug: z.string() }).parse
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { slug } = await readValidatedBody(event, PostSchema) const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts) return useDB().update(tables.posts)
.set({ .set({
views: sql`${tables.posts.views} views: sql`${tables.posts.views}
+ 1`, + 1`,
}) })
.where(eq(tables.posts.slug, slug)) .where(eq(tables.posts.slug, slug))
}) })

View File

@@ -4,125 +4,125 @@ import {relations} from 'drizzle-orm'
// O B J E C T S // O B J E C T S
export const maintenances = pgTable('maintenances', { export const maintenances = pgTable('maintenances', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
reason: text('reason').default(''), reason: text('reason').default(''),
enabled: boolean('enabled').default(false).notNull(), enabled: boolean('enabled').default(false).notNull(),
beginAt: date('begin_at').defaultNow().notNull(), beginAt: date('begin_at').defaultNow().notNull(),
endAt: date('end_at').defaultNow().notNull(), endAt: date('end_at').defaultNow().notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const announcements = pgTable('announcements', { export const announcements = pgTable('announcements', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
content: text('content').default('').notNull(), content: text('content').default('').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const posts = pgTable('posts', { export const posts = pgTable('posts', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
slug: text('slug').notNull(), slug: text('slug').notNull(),
likes: integer('likes').default(0).notNull(), likes: integer('likes').default(0).notNull(),
views: integer('views').default(0).notNull(), views: integer('views').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const suggestions = pgTable('suggestions', { export const suggestions = pgTable('suggestions', {
id: serial('id').notNull(), id: serial('id').notNull(),
email: text('email').notNull().unique(), email: text('email').notNull().unique(),
content: text('content').notNull(), content: text('content').notNull(),
added: boolean('added').default(false).notNull(), added: boolean('added').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, t => ({ }, t => ({
pk: primaryKey({ columns: [t.id, t.email] }), pk: primaryKey({ columns: [t.id, t.email] }),
})) }))
export const guestbookMessages = pgTable('guestbook_messages', { export const guestbookMessages = pgTable('guestbook_messages', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
message: text('message').notNull(), message: text('message').notNull(),
email: text('email').notNull().unique(), email: text('email').notNull().unique(),
username: text('username').notNull(), username: text('username').notNull(),
image: text('image').notNull(), image: text('image').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark']) export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark'])
export const categories = pgTable('categories', { export const categories = pgTable('categories', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
slug: text('slug').unique().notNull(), slug: text('slug').unique().notNull(),
name: text('name').notNull(), name: text('name').notNull(),
type: categoriesType('type').notNull(), type: categoriesType('type').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const talents = pgTable('talents', { export const talents = pgTable('talents', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
logo: text('logo').default('').notNull(), logo: text('logo').default('').notNull(),
website: text('website').default('').notNull(), website: text('website').default('').notNull(),
work: text('work').default('').notNull(), work: text('work').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(), favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const talentsToCategories = pgTable('talents_categories', { export const talentsToCategories = pgTable('talents_categories', {
talentId: integer('talent_id').notNull() talentId: integer('talent_id').notNull()
.references(() => talents.id, { onDelete: 'cascade' }), .references(() => talents.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull() categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }), .references(() => categories.id, { onDelete: 'cascade' }),
}) })
export const bookmarks = pgTable('bookmarks', { export const bookmarks = pgTable('bookmarks', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
name: text('name').notNull(), name: text('name').notNull(),
website: text('website').default('').notNull(), website: text('website').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(), favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
export const bookmarksToCategories = pgTable('bookmarks_categories', { export const bookmarksToCategories = pgTable('bookmarks_categories', {
bookmarkId: integer('bookmark_id').notNull() bookmarkId: integer('bookmark_id').notNull()
.references(() => bookmarks.id, { onDelete: 'cascade' }), .references(() => bookmarks.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull() categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }), .references(() => categories.id, { onDelete: 'cascade' }),
}, t => ({ }, t => ({
pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }), pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }),
})) }))
// R E L A T I O N S // R E L A T I O N S
export const talentsRelations = relations(talents, ({ many }) => ({ export const talentsRelations = relations(talents, ({ many }) => ({
talentCategories: many(talentsToCategories), talentCategories: many(talentsToCategories),
})) }))
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({ export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
bookmarkCategories: many(bookmarksToCategories), bookmarkCategories: many(bookmarksToCategories),
})) }))
export const categoriesRelations = relations(categories, ({ many }) => ({ export const categoriesRelations = relations(categories, ({ many }) => ({
talentsToCategories: many(talentsToCategories), talentsToCategories: many(talentsToCategories),
bookmarksToCategories: many(bookmarksToCategories), bookmarksToCategories: many(bookmarksToCategories),
})) }))
export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({ export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({
talent: one(talents, { talent: one(talents, {
references: [talents.id], references: [talents.id],
fields: [talentsToCategories.talentId], fields: [talentsToCategories.talentId],
}), }),
category: one(categories, { category: one(categories, {
references: [categories.id], references: [categories.id],
fields: [talentsToCategories.categoryId], fields: [talentsToCategories.categoryId],
}), }),
})) }))
export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({ export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({
bookmark: one(bookmarks, { bookmark: one(bookmarks, {
references: [bookmarks.id], references: [bookmarks.id],
fields: [bookmarksToCategories.bookmarkId], fields: [bookmarksToCategories.bookmarkId],
}), }),
category: one(categories, { category: one(categories, {
references: [categories.id], references: [categories.id],
fields: [bookmarksToCategories.categoryId], fields: [bookmarksToCategories.categoryId],
}), }),
})) }))

View File

@@ -1,19 +1,19 @@
export default oauth.githubEventHandler({ export default oauth.githubEventHandler({
config: { config: {
emailRequired: true, emailRequired: true,
}, },
async onSuccess(event, { user }) { async onSuccess(event, { user }) {
await setUserSession(event, { await setUserSession(event, {
user: { user: {
email: user.email, email: user.email,
picture: user.avatar_url, picture: user.avatar_url,
username: String(user.name).trim(), username: String(user.name).trim(),
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL, admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
}, },
}) })
return sendRedirect(event, getCookie(event, 'last-route') || '/') return sendRedirect(event, getCookie(event, 'last-route') || '/')
}, },
onError(error) { onError(error) {
console.error('GitHub OAuth error:', error) console.error('GitHub OAuth error:', error)
}, },
}) })

View File

@@ -1,16 +1,16 @@
export default oauth.googleEventHandler({ export default oauth.googleEventHandler({
async onSuccess(event, { user }) { async onSuccess(event, { user }) {
await setUserSession(event, { await setUserSession(event, {
user: { user: {
email: user.email, email: user.email,
picture: user.picture, picture: user.picture,
username: String(user.name).trim(), username: String(user.name).trim(),
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL, admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
}, },
}) })
return sendRedirect(event, getCookie(event, 'last-route') || '/') return sendRedirect(event, getCookie(event, 'last-route') || '/')
}, },
onError(error) { onError(error) {
console.error('Google OAuth error:', error) console.error('Google OAuth error:', error)
}, },
}) })

View File

@@ -9,5 +9,5 @@ const connectionString = process.env.DATABASE_URL as string
const client = postgres(connectionString, { prepare: false }) const client = postgres(connectionString, { prepare: false })
export function useDB() { export function useDB() {
return drizzle(client, { schema }) return drizzle(client, { schema })
} }

View File

@@ -1,28 +1,28 @@
import type {RuntimeConfig} from 'nuxt/schema' import type {RuntimeConfig} from 'nuxt/schema'
interface WebhookContent { interface WebhookContent {
title: string title: string
description: string description: string
color: number color: number
} }
export async function sendDiscordWebhookMessage(config: RuntimeConfig, content: WebhookContent) { export async function sendDiscordWebhookMessage(config: RuntimeConfig, content: WebhookContent) {
await $fetch(`https://discordapp.com/api/webhooks/${config.discordId}/${config.discordToken}`, { await $fetch(`https://discordapp.com/api/webhooks/${config.discordId}/${config.discordToken}`, {
method: 'POST', method: 'POST',
body: { body: {
embeds: [ embeds: [
{ {
title: content.title, title: content.title,
description: content.description, description: content.description,
color: content.color, color: content.color,
url: 'https://arthurdanjou.fr/talents', url: 'https://arthurdanjou.fr/talents',
footer: { footer: {
text: 'Powered by Nuxt', text: 'Powered by Nuxt',
}, },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
], ],
username: 'ArtDanjRobot - Website', username: 'ArtDanjRobot - Website',
}, },
}) })
} }

View File

@@ -1,29 +1,29 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
export const useBookmarksStore = defineStore( export const useBookmarksStore = defineStore(
'bookmarks', 'bookmarks',
() => { () => {
const currentCategory = ref<string>('all') const currentCategory = ref<string>('all')
const currentFavorite = ref<boolean>(false) const currentFavorite = ref<boolean>(false)
const getCategory = computed(() => currentCategory) const getCategory = computed(() => currentCategory)
function setCategory(newCategory: string) { function setCategory(newCategory: string) {
currentCategory.value = newCategory currentCategory.value = newCategory
} }
const isFavorite = computed(() => currentFavorite) const isFavorite = computed(() => currentFavorite)
function toggleFavorite() { function toggleFavorite() {
currentFavorite.value = !currentFavorite.value currentFavorite.value = !currentFavorite.value
} }
return { return {
getCategory, getCategory,
setCategory, setCategory,
isFavorite, isFavorite,
toggleFavorite, toggleFavorite,
} }
}, },
{ {
persist: true, persist: true,
}, },
) )

View File

@@ -2,27 +2,27 @@ import {defineStore} from 'pinia'
import {ColorsTheme} from '~~/types' import {ColorsTheme} from '~~/types'
export const useColorStore = defineStore( export const useColorStore = defineStore(
'color', 'color',
() => { () => {
const colorCookie = useCookie('color', { path: '/', default: () => ColorsTheme.RED }) const colorCookie = useCookie('color', { path: '/', default: () => ColorsTheme.RED })
const appConfig = useAppConfig() const appConfig = useAppConfig()
watch(colorCookie, (newColor) => { watch(colorCookie, (newColor) => {
appConfig.ui.primary = newColor appConfig.ui.primary = newColor
}, { immediate: true }) }, { immediate: true })
function setColor(color: string) { function setColor(color: string) {
colorCookie.value = color as ColorsTheme colorCookie.value = color as ColorsTheme
} }
const getColor = computed(() => colorCookie) const getColor = computed(() => colorCookie)
return { return {
getColor, getColor,
setColor, setColor,
} }
}, },
{ {
persist: true, persist: true,
}, },
) )

View File

@@ -1,29 +1,29 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
export const useTalentsStore = defineStore( export const useTalentsStore = defineStore(
'talents', 'talents',
() => { () => {
const currentCategory = ref<string>('all') const currentCategory = ref<string>('all')
const currentFavorite = ref<boolean>(false) const currentFavorite = ref<boolean>(false)
const getCategory = computed(() => currentCategory) const getCategory = computed(() => currentCategory)
function setCategory(newCategory: string) { function setCategory(newCategory: string) {
currentCategory.value = newCategory currentCategory.value = newCategory
} }
const isFavorite = computed(() => currentFavorite) const isFavorite = computed(() => currentFavorite)
function toggleFavorite() { function toggleFavorite() {
currentFavorite.value = !currentFavorite.value currentFavorite.value = !currentFavorite.value
} }
return { return {
getCategory, getCategory,
setCategory, setCategory,
isFavorite, isFavorite,
toggleFavorite, toggleFavorite,
} }
}, },
{ {
persist: true, persist: true,
}, },
) )

View File

@@ -2,13 +2,13 @@ import type {Config} from 'tailwindcss'
import {ColorsTheme} from './types' import {ColorsTheme} from './types'
export default { export default {
content: [ content: [
'content/**/*.md', 'content/**/*.md',
], ],
safelist: [ safelist: [
...Object.values(ColorsTheme).map(color => `prose-${color}`), ...Object.values(ColorsTheme).map(color => `prose-${color}`),
...Object.values(ColorsTheme).map(color => `border-${color}-500`), ...Object.values(ColorsTheme).map(color => `border-${color}-500`),
...Object.values(ColorsTheme).map(color => `hover:border-${color}-500`), ...Object.values(ColorsTheme).map(color => `hover:border-${color}-500`),
...Object.values(ColorsTheme).map(color => `dark:hover:border-${color}-500`), ...Object.values(ColorsTheme).map(color => `dark:hover:border-${color}-500`),
], ],
} satisfies Partial<Config> } satisfies Partial<Config>

278
types.ts
View File

@@ -1,191 +1,191 @@
import type {MarkdownParsedContent, ParsedContent} from '@nuxt/content/dist/runtime/types' import type {MarkdownParsedContent, ParsedContent} from '@nuxt/content/dist/runtime/types'
export enum ColorsTheme { export enum ColorsTheme {
RED = 'red', RED = 'red',
ORANGE = 'orange', ORANGE = 'orange',
AMBER = 'amber', AMBER = 'amber',
YELLOW = 'yellow', YELLOW = 'yellow',
LIME = 'lime', LIME = 'lime',
GREEN = 'green', GREEN = 'green',
EMERALD = 'emerald', EMERALD = 'emerald',
TEAL = 'teal', TEAL = 'teal',
CYAN = 'cyan', CYAN = 'cyan',
SKY = 'sky', SKY = 'sky',
BLUE = 'blue', BLUE = 'blue',
INDIGO = 'indigo', INDIGO = 'indigo',
VIOLET = 'violet', VIOLET = 'violet',
PURPLE = 'purple', PURPLE = 'purple',
FUCHSIA = 'fuchsia', FUCHSIA = 'fuchsia',
PINK = 'pink', PINK = 'pink',
ROSE = 'rose', ROSE = 'rose',
} }
interface WakatimeData { interface WakatimeData {
name: string name: string
percent: number percent: number
} }
export interface Stats { export interface Stats {
coding: { coding: {
data: { data: {
grand_total: { grand_total: {
total_seconds_including_other_language: number total_seconds_including_other_language: number
} }
range: { range: {
start: string start: string
} }
} }
} }
editors: { editors: {
data: Array<WakatimeData> data: Array<WakatimeData>
} }
os: { os: {
data: Array<WakatimeData> data: Array<WakatimeData>
} }
languages: { languages: {
data: Array<WakatimeData> data: Array<WakatimeData>
} }
} }
interface LanyardActivity { interface LanyardActivity {
name: string name: string
state: string state: string
details: string details: string
timestamps: { timestamps: {
start: number start: number
} }
} }
export interface Activity { export interface Activity {
data: { data: {
activities: Array<LanyardActivity> activities: Array<LanyardActivity>
} }
} }
export interface Post extends MarkdownParsedContent { export interface Post extends MarkdownParsedContent {
slug: string slug: string
title: string title: string
description: string description: string
readingMins: number readingMins: number
publishedAt: string publishedAt: string
cover?: string cover?: string
} }
export interface Project extends MarkdownParsedContent { export interface Project extends MarkdownParsedContent {
name: string name: string
description: string description: string
latest: boolean latest: boolean
link: string link: string
icon: string icon: string
skills: Skill[] skills: Skill[]
tags: string[] tags: string[]
} }
export interface WorkExperience extends MarkdownParsedContent { export interface WorkExperience extends MarkdownParsedContent {
title: string title: string
description: string description: string
company: string company: string
location: string location: string
companyLink: string companyLink: string
startDate: string startDate: string
endDate: string | 'Today' endDate: string | 'Today'
} }
export interface Education extends MarkdownParsedContent { export interface Education extends MarkdownParsedContent {
title: string title: string
description: string description: string
location: string location: string
startDate: string startDate: string
endDate: string | 'Today' endDate: string | 'Today'
} }
export interface Skill extends ParsedContent { export interface Skill extends ParsedContent {
name: string name: string
icon: string & { icon: string & {
dark: string dark: string
light: string light: string
} }
color: string color: string
link: string link: string
} }
export const providers = [ export const providers = [
{ {
slug: 'github', slug: 'github',
label: 'Login with Github', label: 'Login with Github',
icon: 'i-ph-github-logo-bold', icon: 'i-ph-github-logo-bold',
link: '/auth/github', link: '/auth/github',
}, },
/* { /* {
slug: 'twitter', slug: 'twitter',
label: 'Login with Twitter', label: 'Login with Twitter',
icon: 'i-ph-twitter-logo-bold', icon: 'i-ph-twitter-logo-bold',
link: '/auth/twitter', link: '/auth/twitter',
}, */ }, */
{ {
slug: 'google', slug: 'google',
label: 'Login with Google', label: 'Login with Google',
icon: 'i-ph-google-logo-bold', icon: 'i-ph-google-logo-bold',
link: '/auth/google', link: '/auth/google',
}, },
] ]
export const otherTab = [ export const otherTab = [
[ [
{ {
label: 'Talents', label: 'Talents',
to: '/talents', to: '/talents',
icon: 'i-ph-users-bold', icon: 'i-ph-users-bold',
}, },
{ {
label: 'Guestbook', label: 'Guestbook',
to: '/guestbook', to: '/guestbook',
icon: 'i-material-symbols-book-2-outline', icon: 'i-material-symbols-book-2-outline',
}, },
{ {
label: 'Bookmarks', label: 'Bookmarks',
to: '/bookmarks', to: '/bookmarks',
icon: 'i-material-symbols-bookmark-add-outline-rounded', icon: 'i-material-symbols-bookmark-add-outline-rounded',
}, },
], ],
] ]
export const navs = [ export const navs = [
{ {
label: 'Home', label: 'Home',
to: '/', to: '/',
icon: 'i-ph-house-bold', icon: 'i-ph-house-bold',
}, },
{ {
label: 'About', label: 'About',
to: '/about', to: '/about',
icon: 'i-ph-person-arms-spread-bold', icon: 'i-ph-person-arms-spread-bold',
}, },
/* { /* {
label: 'Articles', label: 'Articles',
to: '/writing', to: '/writing',
icon: 'i-ph-pencil-bold', icon: 'i-ph-pencil-bold',
}, */ }, */
{ {
label: 'Projects', label: 'Projects',
to: '/work', to: '/work',
icon: 'i-ph-flask-bold', icon: 'i-ph-flask-bold',
}, },
{ {
label: 'Uses', label: 'Uses',
to: '/uses', to: '/uses',
icon: 'i-ph-tree-evergreen-bold', icon: 'i-ph-tree-evergreen-bold',
}, },
...otherTab, ...otherTab,
{ {
label: 'Contact', label: 'Contact',
open: true, open: true,
icon: 'i-ph-push-pin-bold', icon: 'i-ph-push-pin-bold',
}, },
].flat() ].flat()
export const IDEs = [ export const IDEs = [
{ name: 'Visual Studio Code', icon: 'i-skill-icons-vscode-light' }, { name: 'Visual Studio Code', icon: 'i-skill-icons-vscode-light' },
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-skill-icons-idea-light' }, { name: 'IntelliJ IDEA Ultimate', icon: 'i-skill-icons-idea-light' },
{ name: 'WebStorm', icon: 'i-skill-icons-webstorm-light' }, { name: 'WebStorm', icon: 'i-skill-icons-webstorm-light' },
] ]