mirror of
https://github.com/ArthurDanjou/website.git
synced 2026-01-14 12:14:42 +01:00
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
16
auth.d.ts
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
76
error.vue
76
error.vue
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
125
nuxt.config.ts
125
nuxt.config.ts
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
194
pages/about.vue
194
pages/about.vue
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
112
pages/work.vue
112
pages/work.vue
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { inject } from '@vercel/analytics'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
inject()
|
||||
inject()
|
||||
})
|
||||
|
||||
@@ -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}`)
|
||||
})
|
||||
|
||||
@@ -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)],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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`,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
278
types.ts
@@ -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' },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user