lint code

Signed-off-by: Arthur DANJOU <arthurdanjou@outlook.fr>
This commit is contained in:
2024-04-20 01:12:41 +02:00
parent c698bfec8a
commit 4574a7dccd
69 changed files with 3110 additions and 3013 deletions

View File

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

View File

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

16
auth.d.ts vendored
View File

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

View File

@@ -3,20 +3,32 @@ const { data: announce } = await useFetch('/api/announcement')
const appConfig = useAppConfig()
function getColor() {
return `bg-${appConfig.ui.primary}-500`
return `bg-${appConfig.ui.primary}-500`
}
</script>
<template>
<div v-if="announce" class="w-container flex justify-center mt-8">
<div class="relative">
<h1 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" />
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="getColor()" />
<span class="relative inline-flex rounded-full h-2 w-2" :class="getColor()" />
</span>
</div>
</div>
<div
v-if="announce"
class="w-container flex justify-center mt-8"
>
<div class="relative">
<h1
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"
/>
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
:class="getColor()"
/>
<span
class="relative inline-flex rounded-full h-2 w-2"
:class="getColor()"
/>
</span>
</div>
</div>
</template>
<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(', '))
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
function jumpPoints() {
for (let i = 0; i < points.value.length; i++)
points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])]
for (let i = 0; i < points.value.length; i++)
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())
@@ -20,17 +20,17 @@ onUnmounted(() => clearTimeout(timeout))
</script>
<template>
<ClientOnly>
<div
aria-hidden="true"
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
>
<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"
:style="{ 'clip-path': `polygon(${poly})` }"
/>
</div>
</ClientOnly>
<ClientOnly>
<div
aria-hidden="true"
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
>
<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"
:style="{ 'clip-path': `polygon(${poly})` }"
/>
</div>
</ClientOnly>
</template>
<style scoped>

View File

@@ -3,37 +3,37 @@ const year = computed(() => new Date().getFullYear())
</script>
<template>
<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 duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
<p class="text-subtitle text-sm">
© {{ year }} ArtDanjProduction
</p>
<div class="flex items-center">
<p class="text-subtitle">
Designed & Built by
</p>
<UButton
color="primary"
label="Arthur Danjou"
target="_blank"
to="https://twitter.com/arthurdanj"
variant="link"
/>
</div>
<p class="text-subtitle flex items-center">
Made with
<UButton
color="green"
icon="i-vscode-icons-file-type-nuxt"
label="Nuxt 3"
target="_blank"
to="https://nuxt.com/"
trailing
variant="link"
/>
</p>
</div>
</div>
</footer>
<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 duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
<p class="text-subtitle text-sm">
© {{ year }} ArtDanjProduction
</p>
<div class="flex items-center">
<p class="text-subtitle">
Designed & Built by
</p>
<UButton
color="primary"
label="Arthur Danjou"
target="_blank"
to="https://twitter.com/arthurdanj"
variant="link"
/>
</div>
<p class="text-subtitle flex items-center">
Made with
<UButton
color="green"
icon="i-vscode-icons-file-type-nuxt"
label="Nuxt 3"
target="_blank"
to="https://nuxt.com/"
trailing
variant="link"
/>
</p>
</div>
</div>
</footer>
</template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
defineProps({
title: {
type: String,
default: 'Uses Section title',
},
title: {
type: String,
default: 'Uses Section title',
},
})
const appConfig = useAppConfig()
@@ -12,22 +12,22 @@ const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
</script>
<template>
<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">
<h2
:class="getColor"
class="relative text-sm font-semibold pl-3.5"
>
<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>
{{ title }}
</h2>
<div class="md:col-span-3">
<ul class="space-y-8">
<slot />
</ul>
</div>
</div>
</section>
<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">
<h2
:class="getColor"
class="relative text-sm font-semibold pl-3.5"
>
<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>
{{ title }}
</h2>
<div class="md:col-span-3">
<ul class="space-y-8">
<slot />
</ul>
</div>
</div>
</section>
</template>

View File

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

View File

@@ -1,26 +1,26 @@
<script setup lang="ts">
defineProps({
href: {
type: String,
default: '',
},
target: {
type: String,
default: undefined,
required: false,
},
href: {
type: String,
default: '',
},
target: {
type: String,
default: undefined,
required: false,
},
})
const appConfig = useAppConfig()
</script>
<template>
<NuxtLink
:href="href"
:target="target"
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`"
>
<slot />
</NuxtLink>
<NuxtLink
:href="href"
:target="target"
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`"
>
<slot />
</NuxtLink>
</template>

View File

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

View File

@@ -1,14 +1,14 @@
<template>
<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">
<Logo />
<div class="hidden grow lg:flex justify-center">
<NavBar />
</div>
<div class="flex justify-end gap-2 items-center">
<ThemePicker />
<MobileNavBar />
</div>
</div>
</header>
<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">
<Logo />
<div class="hidden grow lg:flex justify-center">
<NavBar />
</div>
<div class="flex justify-end gap-2 items-center">
<ThemePicker />
<MobileNavBar />
</div>
</div>
</header>
</template>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import {useColorStore} from '~/store/color'
import {ColorsTheme} from '~~/types'
import { useColorStore } from '~/store/color'
import { ColorsTheme } from '~~/types'
const colors = Object.values(ColorsTheme)
@@ -9,80 +9,80 @@ const { getColor, setColor } = useColorStore()
const colorMode = useColorMode()
const isDark = ref(colorMode.value === 'dark')
watch(isDark, () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
})
</script>
<template>
<UPopover
mode="hover"
:ui="{
background: 'bg-white dark:bg-stone-900',
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
container: 'z-30',
}"
>
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
size="lg"
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
aria-label="Color picker"
>
<UIcon
class="w-5 h-5 text-primary-500 dark:text-primary-400"
name="i-ph-paint-brush-bold"
/>
</UButton>
</template>
<UPopover
mode="hover"
:ui="{
background: 'bg-white dark:bg-stone-900',
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
container: 'z-30',
}"
>
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
size="lg"
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
aria-label="Color picker"
>
<UIcon
class="w-5 h-5 text-primary-500 dark:text-primary-400"
name="i-ph-paint-brush-bold"
/>
</UButton>
</template>
<template #panel>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<UTooltip
v-for="color in colors"
:key="color"
:open-delay="500"
:text="color"
class="capitalize"
>
<UButton
color="gray"
square
:ui="{
color: {
white: {
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',
},
},
}"
:variant="color === getColor ? 'solid' : 'ghost'"
@click.stop.prevent="setColor(color)"
>
<span
:class="`bg-${color}-500/80 border-${color}-500`"
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
>
<UIcon
v-if="color === getColor"
name="i-ic-round-check"
/>
</span>
</UButton>
</UTooltip>
</div>
<UDivider class="my-2" />
<div>
<UToggle
v-model="isDark"
on-icon="i-heroicons-moon-20-solid"
off-icon="i-heroicons-sun-20-solid"
/>
</div>
</div>
</template>
</UPopover>
<template #panel>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<UTooltip
v-for="color in colors"
:key="color"
:open-delay="500"
:text="color"
class="capitalize"
>
<UButton
color="gray"
square
:ui="{
color: {
white: {
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',
},
},
}"
:variant="color === getColor ? 'solid' : 'ghost'"
@click.stop.prevent="setColor(color)"
>
<span
:class="`bg-${color}-500/80 border-${color}-500`"
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
>
<UIcon
v-if="color === getColor"
name="i-ic-round-check"
/>
</span>
</UButton>
</UTooltip>
</div>
<UDivider class="my-2" />
<div>
<UToggle
v-model="isDark"
on-icon="i-heroicons-moon-20-solid"
off-icon="i-heroicons-sun-20-solid"
/>
</div>
</div>
</template>
</UPopover>
</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])
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 = {
footer: { padding: 'px-4 py-2' },
body: { base: 'h-full flex items-center' },
footer: { padding: 'px-4 py-2' },
body: { base: 'h-full flex items-center' },
}
useIntervalFn(async () => await refresh(), 5000)
</script>
<template>
<UCard
:ui="CardUi"
class="flex flex-col justify-between"
>
<div
v-if="activity && activity.data.activities"
class="flex items-center gap-x-4"
>
<p
class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
>
Activity
</p>
<div v-if="codingActivity">
<div class="flex gap-4 items-center">
<UIcon
class="h-10 w-10"
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
/>
<div>
<div class="flex items-center gap-2">
<h1>{{ codingActivity.name }}</h1>
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
<div
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
class="h-3 w-3 inline-flex rounded-full cursor-pointer"
/>
</UTooltip>
</div>
<h3 v-if="codingActivity.details === 'Idling'">
I'm Idling on my computer
</h3>
<h3 v-else>
{{ codingActivity.details }} - {{ codingActivity.state }}
</h3>
</div>
</div>
</div>
<div
v-else
class="text-subtitle"
>
<div class="flex items-center gap-2">
<h1>I'm currently offline</h1>
<UTooltip text="I'm offline 🫥">
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
</UTooltip>
</div>
<h3>Come back later to see what I'm doing</h3>
</div>
</div>
<template #footer>
<div class="flex items-center justify-end w-full">
<ClientOnly>
<p
v-if="codingActivity"
class="text-subtitle text-xs w-1/2"
>
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
</p>
</ClientOnly>
<div class="flex items-center space-x-1 w-1/2 justify-end">
<p class="text-subtitle text-xs">
powered by
</p>
<UButton
size="xs"
:padded="false"
variant="link"
to="https://github.com/Phineas/lanyard"
target="_blank"
label="Lanyard"
/>
<UIcon
class="text-subtitle"
name="i-jam-thunder"
/>
</div>
</div>
</template>
</UCard>
<UCard
:ui="CardUi"
class="flex flex-col justify-between"
>
<div
v-if="activity && activity.data.activities"
class="flex items-center gap-x-4"
>
<p
class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
>
Activity
</p>
<div v-if="codingActivity">
<div class="flex gap-4 items-center">
<UIcon
class="h-10 w-10"
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
/>
<div>
<div class="flex items-center gap-2">
<h1>{{ codingActivity.name }}</h1>
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
<div
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
class="h-3 w-3 inline-flex rounded-full cursor-pointer"
/>
</UTooltip>
</div>
<h3 v-if="codingActivity.details === 'Idling'">
I'm Idling on my computer
</h3>
<h3 v-else>
{{ codingActivity.details }} - {{ codingActivity.state }}
</h3>
</div>
</div>
</div>
<div
v-else
class="text-subtitle"
>
<div class="flex items-center gap-2">
<h1>I'm currently offline</h1>
<UTooltip text="I'm offline 🫥">
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
</UTooltip>
</div>
<h3>Come back later to see what I'm doing</h3>
</div>
</div>
<template #footer>
<div class="flex items-center justify-end w-full">
<ClientOnly>
<p
v-if="codingActivity"
class="text-subtitle text-xs w-1/2"
>
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
</p>
</ClientOnly>
<div class="flex items-center space-x-1 w-1/2 justify-end">
<p class="text-subtitle text-xs">
powered by
</p>
<UButton
size="xs"
:padded="false"
variant="link"
to="https://github.com/Phineas/lanyard"
target="_blank"
label="Lanyard"
/>
<UIcon
class="text-subtitle"
name="i-jam-thunder"
/>
</div>
</div>
</template>
</UCard>
</template>
<style>

View File

@@ -1,49 +1,49 @@
<script setup>
const socials = [
{
name: 'mail',
icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr',
},
{
name: 'twitter',
icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj',
},
{
name: 'github',
icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou',
},
{
name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/',
},
{
name: 'mail',
icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr',
},
{
name: 'twitter',
icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj',
},
{
name: 'github',
icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou',
},
{
name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/',
},
]
</script>
<template>
<div class="w-container mt-32 mb-24">
<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">
Software engineer, mathematics lover and AI enthusiast
</h1>
<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.
</p>
<div class="flex gap-4">
<UButton
v-for="social in socials"
:key="social.name"
:icon="social.icon"
size="md"
:to="social.link"
variant="ghost"
target="_blank"
:ui="{ rounded: 'rounded-full' }"
/>
</div>
</div>
</div>
<div class="w-container mt-32 mb-24">
<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">
Software engineer, mathematics lover and AI enthusiast
</h1>
<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.
</p>
<div class="flex gap-4">
<UButton
v-for="social in socials"
:key="social.name"
:icon="social.icon"
size="md"
:to="social.link"
variant="ghost"
target="_blank"
:ui="{ rounded: 'rounded-full' }"
/>
</div>
</div>
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import type {Skill} from '~~/types'
defineProps({
skill: Object as PropType<Skill>,
skill: Object as PropType<Skill>,
})
const { $colorMode } = useNuxtApp()
@@ -10,24 +10,24 @@ const isLight = computed(() => $colorMode.value === 'light')
</script>
<template>
<li
v-if="skill"
class="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">
<UIcon
v-if="isLight"
:name="skill.icon.light ? skill.icon.light : skill.icon"
dynamic
size="20"
/>
<UIcon
v-else
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
dynamic
size="20"
/>
</div>
<span class="text-sm text-subtitle">{{ skill.name }}</span>
</li>
<li
v-if="skill"
class="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">
<UIcon
v-if="isLight"
:name="skill.icon.light ? skill.icon.light : skill.icon"
dynamic
size="20"
/>
<UIcon
v-else
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
dynamic
size="20"
/>
</div>
<span class="text-sm text-subtitle">{{ skill.name }}</span>
</li>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,31 @@
<script setup lang="ts">
import { SpeedInsights } from '@vercel/speed-insights/nuxt'
import {SpeedInsights} from '@vercel/speed-insights/nuxt'
const router = useRouter()
router.afterEach((route) => {
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
})
useHead({
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
})
</script>
<template>
<div>
<SpeedInsights />
<Background />
<NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" />
<section class="fixed inset-0 flex justify-center sm:px-8">
<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>
</section>
<main class="relative z-50 min-h-[100svh]">
<Header />
<NuxtPage />
<Footer />
</main>
<UNotifications />
</div>
<div>
<SpeedInsights />
<Background />
<NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" />
<section class="fixed inset-0 flex justify-center sm:px-8">
<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>
</section>
<main class="relative z-50 min-h-[100svh]">
<Header />
<NuxtPage />
<Footer />
</main>
<UNotifications />
</div>
</template>

View File

@@ -4,19 +4,19 @@ const getColor = computed(() => appConfig.ui.primary)
</script>
<template>
<div>
<NuxtLoadingIndicator :color="getColor" />
<section class="fixed inset-0 flex justify-center sm:px-8">
<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>
</section>
<div class="relative z-50 min-h-[100svh]">
<Header :navigation="false" />
<UContainer>
<NuxtPage />
</UContainer>
<Footer />
</div>
</div>
<div>
<NuxtLoadingIndicator :color="getColor" />
<section class="fixed inset-0 flex justify-center sm:px-8">
<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>
</section>
<div class="relative z-50 min-h-[100svh]">
<Header :navigation="false" />
<UContainer>
<NuxtPage />
</UContainer>
<Footer />
</div>
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
"@pinia/nuxt": "0.5.1",
"@vercel/analytics": "1.2.2",
"@vercel/speed-insights": "1.0.10",
"drizzle-kit": "0.20.14",
"drizzle-kit": "0.20.16",
"drizzle-orm": "0.30.8",
"nuxt": "3.10.3",
"nuxt-auth-utils": "0.0.20",
@@ -32,8 +32,8 @@
"zod": "3.22.5"
},
"devDependencies": {
"@antfu/eslint-config": "2.15.0",
"@iconify/json": "2.2.202",
"@nuxt/eslint": "0.3.8",
"@nuxthq/studio": "1.0.13",
"@nuxtjs/seo": "2.0.0-rc.10",
"@pinia-plugin-persistedstate/nuxt": "1.2.0",
@@ -41,6 +41,7 @@
"@vueuse/core": "10.9.0",
"@vueuse/nuxt": "10.9.0",
"eslint": "9.1.0",
"typescript": "5.4.5"
"typescript": "5.4.5",
"vite-plugin-eslint2": "^4.4.0"
}
}

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
useHead({
title: 'About me • Arthur Danjou',
title: 'About me • Arthur Danjou',
})
const { data: skills } = await getSkills()
@@ -9,100 +9,100 @@ const { data: experiences } = await getWorkExperiences()
</script>
<template>
<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="lg:pl-20 flex justify-center">
<div class="max-w-xs px-2.5 lg:max-w-none">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="It's me 👋"
>
<img
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"
src="/about.png"
>
</UTooltip>
</div>
</div>
<div class="lg:order-first lg:row-span-2">
<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">
I'm Arthur, I live and study in France where I learn new things.
</h1>
<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.
</p>
<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.
</p>
</div>
</div>
</div>
<GridSection title="Interests">
<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.
</GridSlot>
<GridSlot title="Mathematics">
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
</GridSlot>
<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.
</GridSlot>
<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.
</GridSlot>
<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.
</GridSlot>
</GridSection>
<GridSection
v-if="skills"
title="Skills"
>
<div class="grid grid-cols-3 md:grid-cols-4 gap-2">
<Skill
v-for="skill in skills.body"
:key="skill.name"
:skill="skill"
/>
</div>
</GridSection>
<GridSection
v-if="experiences"
title="Work Experiences"
>
<Experience
v-for="experience in experiences"
:key="experience.title"
:experience="experience"
/>
</GridSection>
<GridSection
v-if="educations"
title="Educations"
>
<Education
v-for="education in educations"
:key="education.title"
:education="education"
/>
</GridSection>
<div class="flex justify-center">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="Click to discover my journey"
>
<UButton
label="Download my CV"
icon="i-material-symbols-lab-profile-outline-rounded"
color="primary"
variant="outline"
size="xl"
to="/resume.pdf"
target="_blank"
/>
</UTooltip>
</div>
</section>
<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="lg:pl-20 flex justify-center">
<div class="max-w-xs px-2.5 lg:max-w-none">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="It's me 👋"
>
<img
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"
src="/about.png"
>
</UTooltip>
</div>
</div>
<div class="lg:order-first lg:row-span-2">
<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">
I'm Arthur, I live and study in France where I learn new things.
</h1>
<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.
</p>
<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.
</p>
</div>
</div>
</div>
<GridSection title="Interests">
<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.
</GridSlot>
<GridSlot title="Mathematics">
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
</GridSlot>
<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.
</GridSlot>
<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.
</GridSlot>
<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.
</GridSlot>
</GridSection>
<GridSection
v-if="skills"
title="Skills"
>
<div class="grid grid-cols-3 md:grid-cols-4 gap-2">
<Skill
v-for="skill in skills.body"
:key="skill.name"
:skill="skill"
/>
</div>
</GridSection>
<GridSection
v-if="experiences"
title="Work Experiences"
>
<Experience
v-for="experience in experiences"
:key="experience.title"
:experience="experience"
/>
</GridSection>
<GridSection
v-if="educations"
title="Educations"
>
<Education
v-for="education in educations"
:key="education.title"
:education="education"
/>
</GridSection>
<div class="flex justify-center">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="Click to discover my journey"
>
<UButton
label="Download my CV"
icon="i-material-symbols-lab-profile-outline-rounded"
color="primary"
variant="outline"
size="xl"
to="/resume.pdf"
target="_blank"
/>
</UTooltip>
</div>
</section>
</template>

View File

@@ -2,185 +2,185 @@
import {useBookmarksStore} from '~/store/bookmarks'
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 { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
method: 'get',
query: {
favorite: isFavorite,
category: getCategory,
},
watch: [isFavorite, getCategory],
method: 'get',
query: {
favorite: isFavorite,
category: getCategory,
},
watch: [isFavorite, getCategory],
})
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 }))
function isCategory(slug: string) {
return getCategory.value === slug
return getCategory.value === slug
}
const getMarkerStyle = computed(() => {
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
return {
top: `${selected?.offsetTop}px`,
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
height: `${selected?.offsetHeight}px`,
width: `${selected?.offsetWidth}px`,
}
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
return {
top: `${selected?.offsetTop}px`,
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
height: `${selected?.offsetHeight}px`,
width: `${selected?.offsetWidth}px`,
}
})
const appConfig = useAppConfig()
function getColor() {
return `text-${appConfig.ui.primary}-500`
return `text-${appConfig.ui.primary}-500`
}
</script>
<template>
<section class="w-container lg:my-24 my-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">
My library where I save some resources
</h1>
<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.
</p>
</div>
<div
v-if="getCategories"
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">
<ClientOnly>
<div
class="absolute duration-300 left-1 ease-out focus:outline-none"
:style="[getMarkerStyle]"
>
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
</div>
</ClientOnly>
<div
v-for="category in categories"
:id="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="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
@click.prevent="setCategory(category.slug)"
>
<p class="w-full">
{{ category.label }}
</p>
</div>
</div>
<UPopover>
<UButton
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
color="primary"
variant="soft"
size="lg"
/>
<template #panel>
<div
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
@click.prevent="toggleFavorite()"
>
<UIcon
v-if="isFavorite"
name="i-material-symbols-check-box-outline-rounded"
/>
<UIcon
v-else
name="i-material-symbols-check-box-outline-blank"
/>
<p>Show favorites only</p>
</div>
</template>
</UPopover>
</div>
<UDivider class="my-2" />
<div
v-if="bookmarks && getCategories"
class="mt-8"
>
<div
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"
>
<div
v-for="bookmark in bookmarks"
:key="bookmark.name.toLowerCase().trim()"
class="group relative flex justify-between items-center"
>
<div class="flex flex-col gap-y-1">
<div class="flex gap-6 items-center">
<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" />
<NuxtLink
:href="bookmark.website"
external
target="_blank"
>
<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">
<h1 class="relative z-10">
{{ bookmark.name }}
</h1>
<UTooltip
v-if="bookmark.favorite"
text="You can set the filter to only show favorites."
>
<UIcon
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
name="i-ic-round-star"
/>
</UTooltip>
</div>
</NuxtLink>
</h2>
</div>
<div class="flex gap-2 z-10">
<UBadge
v-for="category in bookmark.bookmarkCategories"
:key="category.category.slug"
color="primary"
variant="soft"
size="xs"
>
{{ category.category.name }}
</UBadge>
</div>
</div>
<p
:class="getColor()"
class="relative z-10 flex text-sm font-medium items-center"
>
<UIcon name="i-ph-link-bold" />
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
</p>
</div>
</div>
<div
v-else-if="bookmarks?.length === 0 && !pending"
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-akar-icons-cross" />
<p>There are no bookmarks for this category. Maybe soon...</p>
</div>
</div>
<div
v-else
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" />
<p>The bookmarks are loading...</p>
</div>
</div>
</div>
</section>
<section class="w-container lg:my-24 my-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">
My library where I save some resources
</h1>
<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.
</p>
</div>
<div
v-if="getCategories"
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">
<ClientOnly>
<div
class="absolute duration-300 left-1 ease-out focus:outline-none"
:style="[getMarkerStyle]"
>
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
</div>
</ClientOnly>
<div
v-for="category in categories"
:id="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="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
@click.prevent="setCategory(category.slug)"
>
<p class="w-full">
{{ category.label }}
</p>
</div>
</div>
<UPopover>
<UButton
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
color="primary"
variant="soft"
size="lg"
/>
<template #panel>
<div
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
@click.prevent="toggleFavorite()"
>
<UIcon
v-if="isFavorite"
name="i-material-symbols-check-box-outline-rounded"
/>
<UIcon
v-else
name="i-material-symbols-check-box-outline-blank"
/>
<p>Show favorites only</p>
</div>
</template>
</UPopover>
</div>
<UDivider class="my-2" />
<div
v-if="bookmarks && getCategories"
class="mt-8"
>
<div
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"
>
<div
v-for="bookmark in bookmarks"
:key="bookmark.name.toLowerCase().trim()"
class="group relative flex justify-between items-center"
>
<div class="flex flex-col gap-y-1">
<div class="flex gap-6 items-center">
<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" />
<NuxtLink
:href="bookmark.website"
external
target="_blank"
>
<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">
<h1 class="relative z-10">
{{ bookmark.name }}
</h1>
<UTooltip
v-if="bookmark.favorite"
text="You can set the filter to only show favorites."
>
<UIcon
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
name="i-ic-round-star"
/>
</UTooltip>
</div>
</NuxtLink>
</h2>
</div>
<div class="flex gap-2 z-10">
<UBadge
v-for="category in bookmark.bookmarkCategories"
:key="category.category.slug"
color="primary"
variant="soft"
size="xs"
>
{{ category.category.name }}
</UBadge>
</div>
</div>
<p
:class="getColor()"
class="relative z-10 flex text-sm font-medium items-center"
>
<UIcon name="i-ph-link-bold" />
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
</p>
</div>
</div>
<div
v-else-if="bookmarks?.length === 0 && !pending"
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-akar-icons-cross" />
<p>There are no bookmarks for this category. Maybe soon...</p>
</div>
</div>
<div
v-else
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" />
<p>The bookmarks are loading...</p>
</div>
</div>
</div>
</section>
</template>

View File

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

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
useHead({
title: 'Arthur Danjou • Software Engineer and Maths Lover',
title: 'Arthur Danjou • Software Engineer and Maths Lover',
})
</script>
<template>
<section>
<Announcement />
<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">
<MainActivity />
<MainStats />
</div>
</section>
<section>
<Announcement />
<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">
<MainActivity />
<MainStats />
</div>
</section>
</template>

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
<template>
<section class="w-container lg:my-24 my-16">
<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">
Software I use, Hardware I own, and my favorite stack
</h1>
<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.
</p>
<ClientOnly>
<ContentDoc
class="my-16"
path="/uses"
/>
<template #fallback>
<USkeleton class="w-full h-1/2" />
</template>
</ClientOnly>
</div>
</section>
<section class="w-container lg:my-24 my-16">
<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">
Software I use, Hardware I own, and my favorite stack
</h1>
<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.
</p>
<ClientOnly>
<ContentDoc
class="my-16"
path="/uses"
/>
<template #fallback>
<USkeleton class="w-full h-1/2" />
</template>
</ClientOnly>
</div>
</section>
</template>

View File

@@ -1,64 +1,64 @@
<script lang="ts" setup>
useHead({
title: 'My work • Arthur Danjou',
title: 'My work • Arthur Danjou',
})
const { data: projects } = await getProjects()
</script>
<template>
<section class="w-container lg:my-24 my-16">
<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">
All my projects can be found on GitHub and by scrolling down.
</h1>
<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.
</p>
</div>
<div class="mt-16 md:mt-20">
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="project in projects"
:key="project.name"
class="group relative flex flex-col justify-between"
>
<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">
<UIcon
:name="project.icon"
dynamic
size="24"
/>
</div>
<div>
<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" />
<NuxtLink
:to="project.link"
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="relative z-10">{{ project.title }}</span>
</NuxtLink>
</h2>
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
{{ project.description }}
</p>
</div>
</div>
<div class="mt-2 flex gap-2 z-10 flex-wrap">
<UBadge
v-for="tag in project.tags"
:key="tag"
color="primary"
variant="soft"
size="xs"
>
{{ tag }}
</UBadge>
</div>
</div>
</div>
</div>
</section>
<section class="w-container lg:my-24 my-16">
<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">
All my projects can be found on GitHub and by scrolling down.
</h1>
<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.
</p>
</div>
<div class="mt-16 md:mt-20">
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="project in projects"
:key="project.name"
class="group relative flex flex-col justify-between"
>
<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">
<UIcon
:name="project.icon"
dynamic
size="24"
/>
</div>
<div>
<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" />
<NuxtLink
:to="project.link"
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="relative z-10">{{ project.title }}</span>
</NuxtLink>
</h2>
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
{{ project.description }}
</p>
</div>
</div>
<div class="mt-2 flex gap-2 z-10 flex-wrap">
<UBadge
v-for="tag in project.tags"
:key="tag"
color="primary"
variant="soft"
size="xs"
>
{{ tag }}
</UBadge>
</div>
</div>
</div>
</div>
</section>
</template>

View File

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

View File

@@ -3,7 +3,7 @@ const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
useHead({
title: 'My Shelf • Arthur Danjou',
title: 'My Shelf • Arthur Danjou',
})
const { data: posts } = await getPosts()
@@ -11,59 +11,59 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
</script>
<template>
<section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8">
<div>
<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.
</h1>
<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>
</p>
</div>
</div>
<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="flex max-w-3xl flex-col space-y-16">
<article
v-for="post in posts"
:key="post.slug"
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">
<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" />
<NuxtLink :to="post._path">
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10">
{{ post.title }}
</span>
</NuxtLink>
</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">
<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>
{{ format(post.publishedAt) }}
</time>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{{ post.description }}
</p>
<div
:class="getColor"
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
>
<p>Read article</p>
<UIcon name="i-ph-arrow-circle-right-bold" />
</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">
<p>{{ format(post.publishedAt) }}</p>
<p>{{ post.readingMins }} min.</p>
</time>
</article>
</div>
</div>
</div>
</section>
<section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8">
<div>
<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.
</h1>
<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>
</p>
</div>
</div>
<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="flex max-w-3xl flex-col space-y-16">
<article
v-for="post in posts"
:key="post.slug"
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">
<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" />
<NuxtLink :to="post._path">
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10">
{{ post.title }}
</span>
</NuxtLink>
</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">
<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>
{{ format(post.publishedAt) }}
</time>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{{ post.description }}
</p>
<div
:class="getColor"
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
>
<p>Read article</p>
<UIcon name="i-ph-arrow-circle-right-bold" />
</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">
<p>{{ format(post.publishedAt) }}</p>
<p>{{ post.readingMins }} min.</p>
</time>
</article>
</div>
</div>
</div>
</section>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
export default defineEventHandler(async (event) => {
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
return useDB().select().from(tables.categories).where(eq(tables.categories.type, type))
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
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
export default defineEventHandler(async (event) => {
const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts)
.set({
likes: sql`${tables.posts.likes}
const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts)
.set({
likes: sql`${tables.posts.likes}
+ 1`,
})
.where(eq(tables.posts.slug, slug))
})
.where(eq(tables.posts.slug, slug))
})

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
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) => {
const config = useRuntimeConfig(event)
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 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`)
return {
coding,
editors,
os,
languages,
}
const config = useRuntimeConfig(event)
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 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`)
return {
coding,
editors,
os,
languages,
}
}, {
maxAge: 60 * 60 * 3, // 3 hours,
maxAge: 60 * 60 * 3, // 3 hours,
})

View File

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

View File

@@ -1,20 +1,20 @@
export default defineEventHandler(async (event) => {
const { favorite, category } = getQuery(event)
const { favorite, category } = getQuery(event)
const talents = await useDB().query.talents
.findMany({
orderBy: [asc(tables.talents.id)],
with: {
talentCategories: {
with: {
category: true,
},
},
},
})
const talents = await useDB().query.talents
.findMany({
orderBy: [asc(tables.talents.id)],
with: {
talentCategories: {
with: {
category: true,
},
},
},
})
return talents.filter(talent =>
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
&& (favorite === 'false' || talent.favorite),
)
return talents.filter(talent =>
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
&& (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
export default defineEventHandler(async (event) => {
const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts)
.set({
views: sql`${tables.posts.views}
const { slug } = await readValidatedBody(event, PostSchema)
return useDB().update(tables.posts)
.set({
views: sql`${tables.posts.views}
+ 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
export const maintenances = pgTable('maintenances', {
id: serial('id').primaryKey(),
reason: text('reason').default(''),
enabled: boolean('enabled').default(false).notNull(),
beginAt: date('begin_at').defaultNow().notNull(),
endAt: date('end_at').defaultNow().notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
reason: text('reason').default(''),
enabled: boolean('enabled').default(false).notNull(),
beginAt: date('begin_at').defaultNow().notNull(),
endAt: date('end_at').defaultNow().notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const announcements = pgTable('announcements', {
id: serial('id').primaryKey(),
content: text('content').default('').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
content: text('content').default('').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
slug: text('slug').notNull(),
likes: integer('likes').default(0).notNull(),
views: integer('views').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
slug: text('slug').notNull(),
likes: integer('likes').default(0).notNull(),
views: integer('views').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const suggestions = pgTable('suggestions', {
id: serial('id').notNull(),
email: text('email').notNull().unique(),
content: text('content').notNull(),
added: boolean('added').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
id: serial('id').notNull(),
email: text('email').notNull().unique(),
content: text('content').notNull(),
added: boolean('added').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, t => ({
pk: primaryKey({ columns: [t.id, t.email] }),
pk: primaryKey({ columns: [t.id, t.email] }),
}))
export const guestbookMessages = pgTable('guestbook_messages', {
id: serial('id').primaryKey(),
message: text('message').notNull(),
email: text('email').notNull().unique(),
username: text('username').notNull(),
image: text('image').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
message: text('message').notNull(),
email: text('email').notNull().unique(),
username: text('username').notNull(),
image: text('image').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark'])
export const categories = pgTable('categories', {
id: serial('id').primaryKey(),
slug: text('slug').unique().notNull(),
name: text('name').notNull(),
type: categoriesType('type').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
slug: text('slug').unique().notNull(),
name: text('name').notNull(),
type: categoriesType('type').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const talents = pgTable('talents', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
logo: text('logo').default('').notNull(),
website: text('website').default('').notNull(),
work: text('work').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
name: text('name').notNull(),
logo: text('logo').default('').notNull(),
website: text('website').default('').notNull(),
work: text('work').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const talentsToCategories = pgTable('talents_categories', {
talentId: integer('talent_id').notNull()
.references(() => talents.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
talentId: integer('talent_id').notNull()
.references(() => talents.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
})
export const bookmarks = pgTable('bookmarks', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
website: text('website').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
id: serial('id').primaryKey(),
name: text('name').notNull(),
website: text('website').default('').notNull(),
favorite: boolean('favorite').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
export const bookmarksToCategories = pgTable('bookmarks_categories', {
bookmarkId: integer('bookmark_id').notNull()
.references(() => bookmarks.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
bookmarkId: integer('bookmark_id').notNull()
.references(() => bookmarks.id, { onDelete: 'cascade' }),
categoryId: integer('category_id').notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
}, t => ({
pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }),
pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }),
}))
// R E L A T I O N S
export const talentsRelations = relations(talents, ({ many }) => ({
talentCategories: many(talentsToCategories),
talentCategories: many(talentsToCategories),
}))
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
bookmarkCategories: many(bookmarksToCategories),
bookmarkCategories: many(bookmarksToCategories),
}))
export const categoriesRelations = relations(categories, ({ many }) => ({
talentsToCategories: many(talentsToCategories),
bookmarksToCategories: many(bookmarksToCategories),
talentsToCategories: many(talentsToCategories),
bookmarksToCategories: many(bookmarksToCategories),
}))
export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({
talent: one(talents, {
references: [talents.id],
fields: [talentsToCategories.talentId],
}),
category: one(categories, {
references: [categories.id],
fields: [talentsToCategories.categoryId],
}),
talent: one(talents, {
references: [talents.id],
fields: [talentsToCategories.talentId],
}),
category: one(categories, {
references: [categories.id],
fields: [talentsToCategories.categoryId],
}),
}))
export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({
bookmark: one(bookmarks, {
references: [bookmarks.id],
fields: [bookmarksToCategories.bookmarkId],
}),
category: one(categories, {
references: [categories.id],
fields: [bookmarksToCategories.categoryId],
}),
bookmark: one(bookmarks, {
references: [bookmarks.id],
fields: [bookmarksToCategories.bookmarkId],
}),
category: one(categories, {
references: [categories.id],
fields: [bookmarksToCategories.categoryId],
}),
}))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

278
types.ts
View File

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

779
yarn.lock

File diff suppressed because it is too large Load Diff