mirror of
https://github.com/ArthurDanjou/website.git
synced 2026-01-14 12:14:42 +01:00
@@ -1,30 +1,30 @@
|
|||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
ui: {
|
ui: {
|
||||||
icons: {
|
icons: {
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
},
|
},
|
||||||
gray: 'neutral',
|
gray: 'neutral',
|
||||||
primary: 'cyan',
|
primary: 'cyan',
|
||||||
notifications: {
|
notifications: {
|
||||||
position: 'bottom-0 right-0',
|
position: 'bottom-0 right-0',
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
base: 'mx-auto',
|
base: 'mx-auto',
|
||||||
padding: 'px-4 sm:px-6 lg:px-8',
|
padding: 'px-4 sm:px-6 lg:px-8',
|
||||||
constrained: 'max-w-9xl',
|
constrained: 'max-w-9xl',
|
||||||
},
|
},
|
||||||
dropdown: {
|
dropdown: {
|
||||||
container: 'z-50',
|
container: 'z-50',
|
||||||
background: 'bg-white dark:bg-zinc-900/90',
|
background: 'bg-white dark:bg-zinc-900/90',
|
||||||
item: {
|
item: {
|
||||||
base: 'duration-300 group flex items-center gap-2 w-full',
|
base: 'duration-300 group flex items-center gap-2 w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
base: 'duration-300 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
|
base: 'duration-300 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75',
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
container: 'z-50',
|
container: 'z-50',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
import type {RouterConfig} from '@nuxt/schema'
|
import type {RouterConfig} from '@nuxt/schema'
|
||||||
|
|
||||||
function findHashPosition(hash: string): { el: string, behavior: ScrollBehavior, top: number } | undefined {
|
function findHashPosition(hash: string): { el: string, behavior: ScrollBehavior, top: number } | undefined {
|
||||||
const el = document.querySelector(hash)
|
const el = document.querySelector(hash)
|
||||||
// vue-router does not incorporate scroll-margin-top on its own.
|
// vue-router does not incorporate scroll-margin-top on its own.
|
||||||
if (el) {
|
if (el) {
|
||||||
const top = Number.parseFloat(getComputedStyle(el).scrollMarginTop)
|
const top = Number.parseFloat(getComputedStyle(el).scrollMarginTop)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
el: hash,
|
el: hash,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
top,
|
top,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://router.vuejs.org/api/#routeroptions
|
// https://router.vuejs.org/api/#routeroptions
|
||||||
export default <RouterConfig>{
|
export default <RouterConfig>{
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
// If history back
|
// If history back
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
// Handle Suspense resolution
|
// Handle Suspense resolution
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
nuxtApp.hooks.hookOnce('page:finish', () => {
|
nuxtApp.hooks.hookOnce('page:finish', () => {
|
||||||
setTimeout(() => resolve(savedPosition), 50)
|
setTimeout(() => resolve(savedPosition), 50)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to heading on click
|
// Scroll to heading on click
|
||||||
if (to.hash) {
|
if (to.hash) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (to.path === from.path) {
|
if (to.path === from.path) {
|
||||||
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
|
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
nuxtApp.hooks.hookOnce('page:finish', () => {
|
nuxtApp.hooks.hookOnce('page:finish', () => {
|
||||||
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
|
setTimeout(() => resolve(findHashPosition(to.hash)), 50)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to top of window
|
// Scroll to top of window
|
||||||
return { top: 0 }
|
return { top: 0 }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
16
auth.d.ts
vendored
16
auth.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
declare module '#auth-utils' {
|
declare module '#auth-utils' {
|
||||||
interface UserSession {
|
interface UserSession {
|
||||||
user: {
|
user: {
|
||||||
email: string
|
email: string
|
||||||
username: string
|
username: string
|
||||||
picture: string
|
picture: string
|
||||||
admin: boolean
|
admin: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,32 +3,32 @@ const { data: announce } = await useFetch('/api/announcement')
|
|||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
function getColor() {
|
function getColor() {
|
||||||
return `bg-${appConfig.ui.primary}-500`
|
return `bg-${appConfig.ui.primary}-500`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="announce"
|
v-if="announce"
|
||||||
class="w-container flex justify-center mt-8"
|
class="w-container flex justify-center mt-8"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<h1
|
<h1
|
||||||
class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10"
|
class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10"
|
||||||
v-html="announce.content"
|
v-html="announce.content"
|
||||||
/>
|
/>
|
||||||
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
|
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
|
||||||
<span
|
<span
|
||||||
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
|
class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
|
||||||
:class="getColor()"
|
:class="getColor()"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex rounded-full h-2 w-2"
|
class="relative inline-flex rounded-full h-2 w-2"
|
||||||
:class="getColor()"
|
:class="getColor()"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ const points = useState(() => Array.from({ length: 25 }).fill(0).map(() => [Math
|
|||||||
const poly = computed(() => points.value.map(([x, y]) => `${x * 100}% ${y * 100}%`).join(', '))
|
const poly = computed(() => points.value.map(([x, y]) => `${x * 100}% ${y * 100}%`).join(', '))
|
||||||
|
|
||||||
function jumpVal(val: number) {
|
function jumpVal(val: number) {
|
||||||
return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random()
|
return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random()
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeout: NodeJS.Timeout
|
let timeout: NodeJS.Timeout
|
||||||
function jumpPoints() {
|
function jumpPoints() {
|
||||||
for (let i = 0; i < points.value.length; i++)
|
for (let i = 0; i < points.value.length; i++)
|
||||||
points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])]
|
points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])]
|
||||||
|
|
||||||
timeout = setTimeout(jumpPoints, Math.random() * 1000)
|
timeout = setTimeout(jumpPoints, Math.random() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => jumpPoints())
|
onMounted(() => jumpPoints())
|
||||||
@@ -20,17 +20,17 @@ onUnmounted(() => clearTimeout(timeout))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
|
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="aspect-[2] h-2/3 w-full bg-gradient-to-r from-[rgb(var(--color-primary-DEFAULT))] to-white/10 lg:opacity-30 xs:opacity-50"
|
class="aspect-[2] h-2/3 w-full bg-gradient-to-r from-[rgb(var(--color-primary-DEFAULT))] to-white/10 lg:opacity-30 xs:opacity-50"
|
||||||
:style="{ 'clip-path': `polygon(${poly})` }"
|
:style="{ 'clip-path': `polygon(${poly})` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,37 +3,37 @@ const year = computed(() => new Date().getFullYear())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="w-full flex justify-center">
|
<footer class="w-full flex justify-center">
|
||||||
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 flex justify-between bg-white dark:bg-zinc-900 border-t border-zinc-100 dark:border-zinc-300/10">
|
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 flex justify-between bg-white dark:bg-zinc-900 border-t border-zinc-100 dark:border-zinc-300/10">
|
||||||
<div class="w-full duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
|
<div class="w-full duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
|
||||||
<p class="text-subtitle text-sm">
|
<p class="text-subtitle text-sm">
|
||||||
© {{ year }} ArtDanjProduction
|
© {{ year }} ArtDanjProduction
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
Designed & Built by
|
Designed & Built by
|
||||||
</p>
|
</p>
|
||||||
<UButton
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Arthur Danjou"
|
label="Arthur Danjou"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://twitter.com/arthurdanj"
|
to="https://twitter.com/arthurdanj"
|
||||||
variant="link"
|
variant="link"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-subtitle flex items-center">
|
<p class="text-subtitle flex items-center">
|
||||||
Made with
|
Made with
|
||||||
<UButton
|
<UButton
|
||||||
color="green"
|
color="green"
|
||||||
icon="i-vscode-icons-file-type-nuxt"
|
icon="i-vscode-icons-file-type-nuxt"
|
||||||
label="Nuxt 3"
|
label="Nuxt 3"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://nuxt.com/"
|
to="https://nuxt.com/"
|
||||||
trailing
|
trailing
|
||||||
variant="link"
|
variant="link"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Uses Section title',
|
default: 'Uses Section title',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
@@ -12,22 +12,22 @@ const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4">
|
<section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4">
|
||||||
<div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
<div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
||||||
<h2
|
<h2
|
||||||
:class="getColor"
|
:class="getColor"
|
||||||
class="relative text-sm font-semibold pl-3.5"
|
class="relative text-sm font-semibold pl-3.5"
|
||||||
>
|
>
|
||||||
<span class="md:hidden absolute inset-y-0 left-0 flex items-center">
|
<span class="md:hidden absolute inset-y-0 left-0 flex items-center">
|
||||||
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
</span>
|
</span>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="md:col-span-3">
|
<div class="md:col-span-3">
|
||||||
<ul class="space-y-8">
|
<ul class="space-y-8">
|
||||||
<slot />
|
<slot />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Uses Slot title',
|
default: 'Uses Slot title',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="group relative flex flex-col items-start">
|
<li class="group relative flex flex-col items-start">
|
||||||
<h3 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
<h3 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
<slot />
|
<slot />
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
defineProps({
|
||||||
href: {
|
href: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:href="href"
|
:href="href"
|
||||||
:target="target"
|
:target="target"
|
||||||
class="border-b border-zinc-200 dark:border-zinc-700/70 duration-300"
|
class="border-b border-zinc-200 dark:border-zinc-700/70 duration-300"
|
||||||
:class="`hover:border-${appConfig.ui.primary}-500 dark:hover:border-${appConfig.ui.primary}-500`"
|
:class="`hover:border-${appConfig.ui.primary}-500 dark:hover:border-${appConfig.ui.primary}-500`"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ const generate = computed(() => props.id && headings?.anchorLinks?.h2)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 :id="id">
|
<h2 :id="id">
|
||||||
<a
|
<a
|
||||||
v-if="id && generate"
|
v-if="id && generate"
|
||||||
:href="`#${id}`"
|
:href="`#${id}`"
|
||||||
class="pl-6 border-l border-zinc-200 dark:border-zinc-700/70 duration-300"
|
class="pl-6 border-l border-zinc-200 dark:border-zinc-700/70 duration-300"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="z-30 sticky top-0 left-0 flex justify-center w-full">
|
<header class="z-30 sticky top-0 left-0 flex justify-center w-full">
|
||||||
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 grid grid-cols-2 lg:grid-cols-3 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-300/10">
|
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 grid grid-cols-2 lg:grid-cols-3 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-300/10">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div class="hidden grow lg:flex justify-center">
|
<div class="hidden grow lg:flex justify-center">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2 items-center">
|
<div class="flex justify-end gap-2 items-center">
|
||||||
<ThemePicker />
|
<ThemePicker />
|
||||||
<MobileNavBar />
|
<MobileNavBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ const appConfig = useAppConfig()
|
|||||||
const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||||
|
|
||||||
function getGroupColor() {
|
function getGroupColor() {
|
||||||
return `group-hover:text-${appConfig.ui.primary}-500`
|
return `group-hover:text-${appConfig.ui.primary}-500`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent"
|
class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent"
|
||||||
to="/"
|
to="/"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="getTextColor"
|
:class="getTextColor"
|
||||||
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
||||||
>Arthur</span>
|
>Arthur</span>
|
||||||
<span
|
<span
|
||||||
:class="getGroupColor()"
|
:class="getGroupColor()"
|
||||||
class="font-bold text-gray-300 dark:text-neutral-600 duration-300"
|
class="font-bold text-gray-300 dark:text-neutral-600 duration-300"
|
||||||
>/</span>
|
>/</span>
|
||||||
<span
|
<span
|
||||||
:class="getTextColor"
|
:class="getTextColor"
|
||||||
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
||||||
>Danjou</span>
|
>Danjou</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,171 +9,171 @@ router.afterEach(() => isOpenSidebar.value = false)
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
function isRoute(path: string) {
|
function isRoute(path: string) {
|
||||||
return route.path === path
|
return route.path === path
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
isOpenSidebar.value = false
|
isOpenSidebar.value = false
|
||||||
isOpenModal.value = true
|
isOpenModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
|
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
<div class="p-1 rounded-md bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
<div class="p-1 rounded-md bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
||||||
<UButton
|
<UButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="i-ph-list-bold"
|
icon="i-ph-list-bold"
|
||||||
@click="isOpenSidebar = true"
|
@click="isOpenSidebar = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<USlideover v-model="isOpenSidebar">
|
<USlideover v-model="isOpenSidebar">
|
||||||
<UCard
|
<UCard
|
||||||
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
|
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
|
||||||
class="flex flex-col flex-1"
|
class="flex flex-col flex-1"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<Logo />
|
<Logo />
|
||||||
<UButton
|
<UButton
|
||||||
size="md"
|
size="md"
|
||||||
icon="i-ic-round-close"
|
icon="i-ic-round-close"
|
||||||
:ui="{ rounded: 'rounded-full' }"
|
:ui="{ rounded: 'rounded-full' }"
|
||||||
@click.prevent="isOpenSidebar = false"
|
@click.prevent="isOpenSidebar = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="nav in navs"
|
v-for="nav in navs"
|
||||||
:key="nav.label"
|
:key="nav.label"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="nav.to"
|
v-if="nav.to"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:variant="isRoute(nav.to) ? 'solid' : 'ghost'"
|
:variant="isRoute(nav.to) ? 'solid' : 'ghost'"
|
||||||
color="primary"
|
color="primary"
|
||||||
:to="nav.to"
|
:to="nav.to"
|
||||||
:icon="nav.icon"
|
:icon="nav.icon"
|
||||||
:label="nav.label"
|
:label="nav.label"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-else
|
v-else
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:icon="nav.icon"
|
:icon="nav.icon"
|
||||||
:label="nav.label"
|
:label="nav.label"
|
||||||
@click.prevent="openModal()"
|
@click.prevent="openModal()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
Footer
|
Footer
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
<UModal v-model="isOpenModal">
|
<UModal v-model="isOpenModal">
|
||||||
<UCard class="p-4">
|
<UCard class="p-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-8 flex justify-between items-center">
|
<div class="mb-8 flex justify-between items-center">
|
||||||
<h1 class="text-xl font-bold">
|
<h1 class="text-xl font-bold">
|
||||||
Contact me
|
Contact me
|
||||||
</h1>
|
</h1>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-akar-icons-cross"
|
icon="i-akar-icons-cross"
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click.prevent="isOpenModal = false"
|
@click.prevent="isOpenModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3 class="text-sm">
|
<h3 class="text-sm">
|
||||||
Email
|
Email
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-subtitle">
|
<p class="text-xs text-subtitle">
|
||||||
arthurdanjou@outlook.fr
|
arthurdanjou@outlook.fr
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup
|
<UButtonGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-mdi-note-edit-outline"
|
icon="i-mdi-note-edit-outline"
|
||||||
label="Compose"
|
label="Compose"
|
||||||
to="mailto:arthurdanjou@outlook.fr"
|
to="mailto:arthurdanjou@outlook.fr"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="copied"
|
v-if="copied"
|
||||||
color="green"
|
color="green"
|
||||||
icon="i-mdi-content-copy"
|
icon="i-mdi-content-copy"
|
||||||
label="Copied"
|
label="Copied"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-else
|
v-else
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-mdi-content-copy"
|
icon="i-mdi-content-copy"
|
||||||
label="Copy"
|
label="Copy"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@click.prevent="copy()"
|
@click.prevent="copy()"
|
||||||
/>
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UDivider label="OR" />
|
<UDivider label="OR" />
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3 class="text-sm">
|
<h3 class="text-sm">
|
||||||
Get in touch
|
Get in touch
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-subtitle">
|
<p class="text-xs text-subtitle">
|
||||||
I'm most active on Twitter
|
I'm most active on Twitter
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup
|
<UButtonGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-ph-github-logo-bold"
|
icon="i-ph-github-logo-bold"
|
||||||
label="Github"
|
label="Github"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://github.com/ArthurDanjou"
|
to="https://github.com/ArthurDanjou"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-ph-twitter-logo-bold"
|
icon="i-ph-twitter-logo-bold"
|
||||||
label="Twitter"
|
label="Twitter"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://twitter.com/ArthurDanj"
|
to="https://twitter.com/ArthurDanj"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,163 +8,163 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="hidden lg:block z-50">
|
<nav class="hidden lg:block z-50">
|
||||||
<div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
<div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
||||||
<UButton
|
<UButton
|
||||||
:class="{ 'link-active': route.path === '/' }"
|
:class="{ 'link-active': route.path === '/' }"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
to="/"
|
to="/"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
:class="{ 'link-active': route.path.includes('/about') }"
|
:class="{ 'link-active': route.path.includes('/about') }"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
to="/about"
|
to="/about"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
About
|
About
|
||||||
</UButton>
|
</UButton>
|
||||||
<!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }">
|
<!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }">
|
||||||
Articles
|
Articles
|
||||||
</UButton> -->
|
</UButton> -->
|
||||||
<UButton
|
<UButton
|
||||||
:class="{ 'link-active': route.path.includes('/work') }"
|
:class="{ 'link-active': route.path.includes('/work') }"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
to="/work"
|
to="/work"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
:class="{ 'link-active': route.path.includes('/uses') }"
|
:class="{ 'link-active': route.path.includes('/uses') }"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
to="/uses"
|
to="/uses"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Uses
|
Uses
|
||||||
</UButton>
|
</UButton>
|
||||||
<UDropdown
|
<UDropdown
|
||||||
:items="otherTab"
|
:items="otherTab"
|
||||||
:popper="{ placement: 'bottom' }"
|
:popper="{ placement: 'bottom' }"
|
||||||
mode="hover"
|
mode="hover"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
class="duration-300"
|
class="duration-300"
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Other
|
Other
|
||||||
</UButton>
|
</UButton>
|
||||||
</UDropdown>
|
</UDropdown>
|
||||||
<UButton
|
<UButton
|
||||||
color="white"
|
color="white"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="isOpenModal = true"
|
@click="isOpenModal = true"
|
||||||
>
|
>
|
||||||
Contact
|
Contact
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<UModal v-model="isOpenModal">
|
<UModal v-model="isOpenModal">
|
||||||
<UCard class="p-4">
|
<UCard class="p-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-8 flex justify-between items-center">
|
<div class="mb-8 flex justify-between items-center">
|
||||||
<h1 class="text-xl font-bold">
|
<h1 class="text-xl font-bold">
|
||||||
Contact me
|
Contact me
|
||||||
</h1>
|
</h1>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-akar-icons-cross"
|
icon="i-akar-icons-cross"
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click.prevent="isOpenModal = false"
|
@click.prevent="isOpenModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3 class="text-sm">
|
<h3 class="text-sm">
|
||||||
Email
|
Email
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-subtitle">
|
<p class="text-xs text-subtitle">
|
||||||
arthurdanjou@outlook.fr
|
arthurdanjou@outlook.fr
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup
|
<UButtonGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-mdi-note-edit-outline"
|
icon="i-mdi-note-edit-outline"
|
||||||
label="Compose"
|
label="Compose"
|
||||||
to="mailto:arthurdanjou@outlook.fr"
|
to="mailto:arthurdanjou@outlook.fr"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="copied"
|
v-if="copied"
|
||||||
color="green"
|
color="green"
|
||||||
icon="i-mdi-content-copy"
|
icon="i-mdi-content-copy"
|
||||||
label="Copied"
|
label="Copied"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-else
|
v-else
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-mdi-content-copy"
|
icon="i-mdi-content-copy"
|
||||||
label="Copy"
|
label="Copy"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@click.prevent="copy()"
|
@click.prevent="copy()"
|
||||||
/>
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UDivider label="OR" />
|
<UDivider label="OR" />
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3 class="text-sm">
|
<h3 class="text-sm">
|
||||||
Get in touch
|
Get in touch
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-subtitle">
|
<p class="text-xs text-subtitle">
|
||||||
I'm most active on Twitter
|
I'm most active on Twitter
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup
|
<UButtonGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-ph-github-logo-bold"
|
icon="i-ph-github-logo-bold"
|
||||||
label="Github"
|
label="Github"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://github.com/ArthurDanjou"
|
to="https://github.com/ArthurDanjou"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-ph-twitter-logo-bold"
|
icon="i-ph-twitter-logo-bold"
|
||||||
label="Twitter"
|
label="Twitter"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
to="https://twitter.com/ArthurDanj"
|
to="https://twitter.com/ArthurDanj"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useColorStore } from '~/store/color'
|
import {useColorStore} from '~/store/color'
|
||||||
import { ColorsTheme } from '~~/types'
|
import {ColorsTheme} from '~~/types'
|
||||||
|
|
||||||
const colors = Object.values(ColorsTheme)
|
const colors = Object.values(ColorsTheme)
|
||||||
|
|
||||||
@@ -9,80 +9,80 @@ const { getColor, setColor } = useColorStore()
|
|||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const isDark = ref(colorMode.value === 'dark')
|
const isDark = ref(colorMode.value === 'dark')
|
||||||
watch(isDark, () => {
|
watch(isDark, () => {
|
||||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UPopover
|
<UPopover
|
||||||
mode="hover"
|
mode="hover"
|
||||||
:ui="{
|
:ui="{
|
||||||
background: 'bg-white dark:bg-stone-900',
|
background: 'bg-white dark:bg-stone-900',
|
||||||
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
|
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
|
||||||
container: 'z-30',
|
container: 'z-30',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
square
|
square
|
||||||
size="lg"
|
size="lg"
|
||||||
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
|
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
|
||||||
aria-label="Color picker"
|
aria-label="Color picker"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
class="w-5 h-5 text-primary-500 dark:text-primary-400"
|
class="w-5 h-5 text-primary-500 dark:text-primary-400"
|
||||||
name="i-ph-paint-brush-bold"
|
name="i-ph-paint-brush-bold"
|
||||||
/>
|
/>
|
||||||
</UButton>
|
</UButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #panel>
|
<template #panel>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="grid grid-cols-5 gap-px">
|
<div class="grid grid-cols-5 gap-px">
|
||||||
<UTooltip
|
<UTooltip
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
:key="color"
|
:key="color"
|
||||||
:open-delay="500"
|
:open-delay="500"
|
||||||
:text="color"
|
:text="color"
|
||||||
class="capitalize"
|
class="capitalize"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
square
|
square
|
||||||
:ui="{
|
:ui="{
|
||||||
color: {
|
color: {
|
||||||
white: {
|
white: {
|
||||||
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
|
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||||
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:variant="color === getColor ? 'solid' : 'ghost'"
|
:variant="color === getColor ? 'solid' : 'ghost'"
|
||||||
@click.stop.prevent="setColor(color)"
|
@click.stop.prevent="setColor(color)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="`bg-${color}-500/80 border-${color}-500`"
|
:class="`bg-${color}-500/80 border-${color}-500`"
|
||||||
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
|
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="color === getColor"
|
v-if="color === getColor"
|
||||||
name="i-ic-round-check"
|
name="i-ic-round-check"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
<UDivider class="my-2" />
|
<UDivider class="my-2" />
|
||||||
<div>
|
<div>
|
||||||
<UToggle
|
<UToggle
|
||||||
v-model="isDark"
|
v-model="isDark"
|
||||||
on-icon="i-heroicons-moon-20-solid"
|
on-icon="i-heroicons-moon-20-solid"
|
||||||
off-icon="i-heroicons-sun-20-solid"
|
off-icon="i-heroicons-sun-20-solid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,100 +5,100 @@ const { data: activity, refresh } = await useAsyncData<Activity>('activity', ()
|
|||||||
const codingActivity = computed(() => activity.value!.data.activities.filter(activity => IDEs.some(ide => ide.name === activity.name))[0])
|
const codingActivity = computed(() => activity.value!.data.activities.filter(activity => IDEs.some(ide => ide.name === activity.name))[0])
|
||||||
|
|
||||||
function formatDate(date: number) {
|
function formatDate(date: number) {
|
||||||
return `${useDateFormat(date, 'DD MMM YYYY').value} at ${useDateFormat(date, 'HH:mm:ss').value}`
|
return `${useDateFormat(date, 'DD MMM YYYY').value} at ${useDateFormat(date, 'HH:mm:ss').value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardUi = {
|
const CardUi = {
|
||||||
footer: { padding: 'px-4 py-2' },
|
footer: { padding: 'px-4 py-2' },
|
||||||
body: { base: 'h-full flex items-center' },
|
body: { base: 'h-full flex items-center' },
|
||||||
}
|
}
|
||||||
|
|
||||||
useIntervalFn(async () => await refresh(), 5000)
|
useIntervalFn(async () => await refresh(), 5000)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UCard
|
<UCard
|
||||||
:ui="CardUi"
|
:ui="CardUi"
|
||||||
class="flex flex-col justify-between"
|
class="flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="activity && activity.data.activities"
|
v-if="activity && activity.data.activities"
|
||||||
class="flex items-center gap-x-4"
|
class="flex items-center gap-x-4"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="uppercase tracking-widest text-sm"
|
class="uppercase tracking-widest text-sm"
|
||||||
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
|
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
|
||||||
>
|
>
|
||||||
Activity
|
Activity
|
||||||
</p>
|
</p>
|
||||||
<div v-if="codingActivity">
|
<div v-if="codingActivity">
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<UIcon
|
<UIcon
|
||||||
class="h-10 w-10"
|
class="h-10 w-10"
|
||||||
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
|
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1>{{ codingActivity.name }}</h1>
|
<h1>{{ codingActivity.name }}</h1>
|
||||||
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
|
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
|
||||||
<div
|
<div
|
||||||
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
|
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
|
||||||
class="h-3 w-3 inline-flex rounded-full cursor-pointer"
|
class="h-3 w-3 inline-flex rounded-full cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
<h3 v-if="codingActivity.details === 'Idling'">
|
<h3 v-if="codingActivity.details === 'Idling'">
|
||||||
I'm Idling on my computer
|
I'm Idling on my computer
|
||||||
</h3>
|
</h3>
|
||||||
<h3 v-else>
|
<h3 v-else>
|
||||||
{{ codingActivity.details }} - {{ codingActivity.state }}
|
{{ codingActivity.details }} - {{ codingActivity.state }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="text-subtitle"
|
class="text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1>I'm currently offline</h1>
|
<h1>I'm currently offline</h1>
|
||||||
<UTooltip text="I'm offline 🫥">
|
<UTooltip text="I'm offline 🫥">
|
||||||
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
|
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
<h3>Come back later to see what I'm doing</h3>
|
<h3>Come back later to see what I'm doing</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-end w-full">
|
<div class="flex items-center justify-end w-full">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<p
|
<p
|
||||||
v-if="codingActivity"
|
v-if="codingActivity"
|
||||||
class="text-subtitle text-xs w-1/2"
|
class="text-subtitle text-xs w-1/2"
|
||||||
>
|
>
|
||||||
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
|
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
|
||||||
</p>
|
</p>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div class="flex items-center space-x-1 w-1/2 justify-end">
|
<div class="flex items-center space-x-1 w-1/2 justify-end">
|
||||||
<p class="text-subtitle text-xs">
|
<p class="text-subtitle text-xs">
|
||||||
powered by
|
powered by
|
||||||
</p>
|
</p>
|
||||||
<UButton
|
<UButton
|
||||||
size="xs"
|
size="xs"
|
||||||
:padded="false"
|
:padded="false"
|
||||||
variant="link"
|
variant="link"
|
||||||
to="https://github.com/Phineas/lanyard"
|
to="https://github.com/Phineas/lanyard"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
label="Lanyard"
|
label="Lanyard"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
class="text-subtitle"
|
class="text-subtitle"
|
||||||
name="i-jam-thunder"
|
name="i-jam-thunder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const socials = [
|
const socials = [
|
||||||
{
|
{
|
||||||
name: 'mail',
|
name: 'mail',
|
||||||
icon: 'i-material-symbols-alternate-email',
|
icon: 'i-material-symbols-alternate-email',
|
||||||
link: 'mailto:arthurdanjou@outlook.fr',
|
link: 'mailto:arthurdanjou@outlook.fr',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'twitter',
|
name: 'twitter',
|
||||||
icon: 'i-ph-twitter-logo-bold',
|
icon: 'i-ph-twitter-logo-bold',
|
||||||
link: 'https://twitter.com/ArthurDanj',
|
link: 'https://twitter.com/ArthurDanj',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'github',
|
name: 'github',
|
||||||
icon: 'i-ph-github-logo-bold',
|
icon: 'i-ph-github-logo-bold',
|
||||||
link: 'https://github.com/ArthurDanjou',
|
link: 'https://github.com/ArthurDanjou',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'linkedin',
|
name: 'linkedin',
|
||||||
icon: 'i-ph-linkedin-logo-bold',
|
icon: 'i-ph-linkedin-logo-bold',
|
||||||
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-container mt-32 mb-24">
|
<div class="w-container mt-32 mb-24">
|
||||||
<div class="flex items-center flex-col space-y-4">
|
<div class="flex items-center flex-col space-y-4">
|
||||||
<h1 class="text-center lg:text-6xl sm:text-5xl text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 !leading-tight md:w-2/3">
|
<h1 class="text-center lg:text-6xl sm:text-5xl text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 !leading-tight md:w-2/3">
|
||||||
Software engineer, mathematics lover and AI enthusiast
|
Software engineer, mathematics lover and AI enthusiast
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle text-center md:w-2/3 p-2">
|
<p class="leading-relaxed text-subtitle text-center md:w-2/3 p-2">
|
||||||
I'm Arthur, a software engineer passionate about artificial intelligence and the cloud but also a mathematics student living in France. I am currently studying mathematics at the Faculty of Sciences of Paris-Saclay.
|
I'm Arthur, a software engineer passionate about artificial intelligence and the cloud but also a mathematics student living in France. I am currently studying mathematics at the Faculty of Sciences of Paris-Saclay.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<UButton
|
<UButton
|
||||||
v-for="social in socials"
|
v-for="social in socials"
|
||||||
:key="social.name"
|
:key="social.name"
|
||||||
:icon="social.icon"
|
:icon="social.icon"
|
||||||
size="md"
|
size="md"
|
||||||
:to="social.link"
|
:to="social.link"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:ui="{ rounded: 'rounded-full' }"
|
:ui="{ rounded: 'rounded-full' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,84 +4,84 @@ import type {Stats} from '~~/types'
|
|||||||
const stats = await $fetch<Stats>('/api/stats')
|
const stats = await $fetch<Stats>('/api/stats')
|
||||||
|
|
||||||
const CardUi = {
|
const CardUi = {
|
||||||
footer: { padding: 'px-4 py-2' },
|
footer: { padding: 'px-4 py-2' },
|
||||||
body: { base: 'h-full' },
|
body: { base: 'h-full' },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UCard
|
<UCard
|
||||||
:ui="CardUi"
|
:ui="CardUi"
|
||||||
class="flex flex-col justify-between"
|
class="flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-x-4 h-full">
|
<div class="flex items-center gap-x-4 h-full">
|
||||||
<p
|
<p
|
||||||
class="uppercase tracking-widest text-sm"
|
class="uppercase tracking-widest text-sm"
|
||||||
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
|
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
|
||||||
>
|
>
|
||||||
STATS
|
STATS
|
||||||
</p>
|
</p>
|
||||||
<div v-if="stats">
|
<div v-if="stats">
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<div class="text-md">
|
<div class="text-md">
|
||||||
<div class="flex items-center gap-x-1">
|
<div class="flex items-center gap-x-1">
|
||||||
<h3>Total hours:</h3>
|
<h3>Total hours:</h3>
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
|
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-x-1 flex-wrap">
|
<div class="flex items-start gap-x-1 flex-wrap">
|
||||||
<h3>Best Editors:</h3>
|
<h3>Best Editors:</h3>
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
{{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(', ') }}
|
{{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(', ') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-x-1">
|
<div class="flex items-center gap-x-1">
|
||||||
<h3>Best OS:</h3>
|
<h3>Best OS:</h3>
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
|
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-x-1 flex-wrap">
|
<div class="flex items-start gap-x-1 flex-wrap">
|
||||||
<h3>Top languages:</h3>
|
<h3>Top languages:</h3>
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
|
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<p
|
<p
|
||||||
v-if="stats"
|
v-if="stats"
|
||||||
class="text-subtitle text-xs w-1/2"
|
class="text-subtitle text-xs w-1/2"
|
||||||
>
|
>
|
||||||
Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }}
|
Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }}
|
||||||
</p>
|
</p>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div class="flex items-center justify-end space-x-1">
|
<div class="flex items-center justify-end space-x-1">
|
||||||
<p class="text-subtitle text-xs">
|
<p class="text-subtitle text-xs">
|
||||||
powered by
|
powered by
|
||||||
</p>
|
</p>
|
||||||
<UButton
|
<UButton
|
||||||
size="xs"
|
size="xs"
|
||||||
:padded="false"
|
:padded="false"
|
||||||
variant="link"
|
variant="link"
|
||||||
to="https://wakatime.com/"
|
to="https://wakatime.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
label="Wakatime"
|
label="Wakatime"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
class="text-subtitle"
|
class="text-subtitle"
|
||||||
name="i-jam-thunder"
|
name="i-jam-thunder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
defineProps({
|
||||||
startDate: String,
|
startDate: String,
|
||||||
endDate: {
|
endDate: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatTodayDate(date: string) {
|
function formatTodayDate(date: string) {
|
||||||
const split = date.split(' ')
|
const split = date.split(' ')
|
||||||
return date === 'Today' ? 'Today' : `${split[0]} ${split[1]}`
|
return date === 'Today' ? 'Today' : `${split[0]} ${split[1]}`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UBadge
|
<UBadge
|
||||||
v-if="startDate !== endDate"
|
v-if="startDate !== endDate"
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
>
|
>
|
||||||
{{ formatTodayDate(startDate!.toString()) }} — {{ formatTodayDate(endDate) }}
|
{{ formatTodayDate(startDate!.toString()) }} — {{ formatTodayDate(endDate) }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
<UBadge
|
<UBadge
|
||||||
v-else
|
v-else
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
>
|
>
|
||||||
{{ formatTodayDate(endDate) }}
|
{{ formatTodayDate(endDate) }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
import type {Education} from '~~/types'
|
import type {Education} from '~~/types'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
education: Object as PropType<Education>,
|
education: Object as PropType<Education>,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="education"
|
v-if="education"
|
||||||
class="group relative flex flex-col items-start"
|
class="group relative flex flex-col items-start"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<DateTag
|
<DateTag
|
||||||
:end-date="education.endDate"
|
:end-date="education.endDate"
|
||||||
:start-date="education.startDate"
|
:start-date="education.startDate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
<h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
{{ education.title }}
|
{{ education.title }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{{ education.location }} — {{ education.description }}
|
{{ education.location }} — {{ education.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,56 +2,56 @@
|
|||||||
import type {WorkExperience} from '~~/types'
|
import type {WorkExperience} from '~~/types'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
experience: Object as PropType<WorkExperience>,
|
experience: Object as PropType<WorkExperience>,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="experience"
|
v-if="experience"
|
||||||
class="group relative flex flex-col items-start"
|
class="group relative flex flex-col items-start"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<DateTag
|
<DateTag
|
||||||
:end-date="experience.endDate"
|
:end-date="experience.endDate"
|
||||||
:start-date="experience.startDate"
|
:start-date="experience.startDate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center my-1">
|
<div class="flex items-center my-1">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="experience.companyLink"
|
v-if="experience.companyLink"
|
||||||
:to="experience.companyLink"
|
:to="experience.companyLink"
|
||||||
variant="link"
|
variant="link"
|
||||||
:padded="false"
|
:padded="false"
|
||||||
color="white"
|
color="white"
|
||||||
size="xl"
|
size="xl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:label="experience.company"
|
:label="experience.company"
|
||||||
class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<UIcon
|
<UIcon
|
||||||
color="gray"
|
color="gray"
|
||||||
name="i-akar-icons-link-chain"
|
name="i-akar-icons-link-chain"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UButton>
|
</UButton>
|
||||||
<h1
|
<h1
|
||||||
v-else
|
v-else
|
||||||
class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
{{ experience.company }}
|
{{ experience.company }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-subtitle text-xs">
|
<div class="text-subtitle text-xs">
|
||||||
{{ experience.location }}
|
{{ experience.location }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{{ experience.title }} — {{ experience.description }}
|
{{ experience.title }} — {{ experience.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type {Skill} from '~~/types'
|
import type {Skill} from '~~/types'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
skill: Object as PropType<Skill>,
|
skill: Object as PropType<Skill>,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $colorMode } = useNuxtApp()
|
const { $colorMode } = useNuxtApp()
|
||||||
@@ -10,25 +10,25 @@ const isLight = computed(() => $colorMode.value === 'light')
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
v-if="skill"
|
v-if="skill"
|
||||||
:href="skill.link"
|
:href="skill.link"
|
||||||
class="cursor-pointer flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
|
class="cursor-pointer flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="isLight"
|
v-if="isLight"
|
||||||
:name="skill.icon.light ? skill.icon.light : skill.icon"
|
:name="skill.icon.light ? skill.icon.light : skill.icon"
|
||||||
:dynamic="true"
|
:dynamic="true"
|
||||||
size="20"
|
size="20"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-else
|
v-else
|
||||||
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
|
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
|
||||||
:dynamic="true"
|
:dynamic="true"
|
||||||
size="20"
|
size="20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-subtitle">{{ skill.name }}</span>
|
<span class="text-sm text-subtitle">{{ skill.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import type { Education, Post, Project, Skill, WorkExperience } from '~~/types'
|
import type {Education, Post, Project, Skill, WorkExperience} from '~~/types'
|
||||||
|
|
||||||
export function getProjects() {
|
export function getProjects() {
|
||||||
return useAsyncData('content:projects', () => {
|
return useAsyncData('content:projects', () => {
|
||||||
return queryContent<Project>('projects').find()
|
return queryContent<Project>('projects').find()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEducations() {
|
export function getEducations() {
|
||||||
return useAsyncData('content:educations', () => {
|
return useAsyncData('content:educations', () => {
|
||||||
return queryContent<Education>('educations')
|
return queryContent<Education>('educations')
|
||||||
.sort({
|
.sort({
|
||||||
endDate: -1,
|
endDate: -1,
|
||||||
})
|
})
|
||||||
.find()
|
.find()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkExperiences() {
|
export function getWorkExperiences() {
|
||||||
return useAsyncData('content:experiences', async () => {
|
return useAsyncData('content:experiences', async () => {
|
||||||
const experiences = await queryContent<WorkExperience>('experiences').find()
|
const experiences = await queryContent<WorkExperience>('experiences').find()
|
||||||
return experiences.sort((a, b) => {
|
return experiences.sort((a, b) => {
|
||||||
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSkills() {
|
export function getSkills() {
|
||||||
return useAsyncData('content:skills', () => queryContent<Skill[]>('skills').findOne())
|
return useAsyncData('content:skills', () => queryContent<Skill[]>('skills').findOne())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPosts() {
|
export function getPosts() {
|
||||||
return useAsyncData('content:posts', async () => {
|
return useAsyncData('content:posts', async () => {
|
||||||
const posts = await queryContent<Post>('writing').find()
|
const posts = await queryContent<Post>('writing').find()
|
||||||
return posts.sort((a, b) => {
|
return posts.sort((a, b) => {
|
||||||
return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
|
return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {defineConfig} from 'drizzle-kit'
|
import {defineConfig} from 'drizzle-kit'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
driver: 'pg',
|
driver: 'pg',
|
||||||
schema: './server/database/schema.ts',
|
schema: './server/database/schema.ts',
|
||||||
out: './server/database/migrations',
|
out: './server/database/migrations',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
connectionString: process.env.DATABASE_URL as string,
|
connectionString: process.env.DATABASE_URL as string,
|
||||||
},
|
},
|
||||||
strict: true,
|
strict: true,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
})
|
})
|
||||||
|
|||||||
82
error.vue
82
error.vue
@@ -2,7 +2,7 @@
|
|||||||
import type {NuxtError} from 'nuxt/app'
|
import type {NuxtError} from 'nuxt/app'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
error: Object as () => NuxtError,
|
error: Object as () => NuxtError,
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
@@ -10,46 +10,46 @@ const getColor = computed(() => appConfig.ui.primary)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<NuxtLoadingIndicator :color="getColor" />
|
<NuxtLoadingIndicator :color="getColor" />
|
||||||
<section class="fixed inset-0 flex justify-center sm:px-8">
|
<section class="fixed inset-0 flex justify-center sm:px-8">
|
||||||
<div class="flex w-full max-w-9xl">
|
<div class="flex w-full max-w-9xl">
|
||||||
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="relative z-50 min-h-[100svh]">
|
<div class="relative z-50 min-h-[100svh]">
|
||||||
<Header />
|
<Header />
|
||||||
<UContainer>
|
<UContainer>
|
||||||
<div class="flex flex-col items-center gap-4 mt-12">
|
<div class="flex flex-col items-center gap-4 mt-12">
|
||||||
<h1
|
<h1
|
||||||
class="font-medium text-[8rem] md:text-[16rem] leading-none bg-error bg-clip-text tracking-wider font-error"
|
class="font-medium text-[8rem] md:text-[16rem] leading-none bg-error bg-clip-text tracking-wider font-error"
|
||||||
:class="`text-${getColor}-500`"
|
:class="`text-${getColor}-500`"
|
||||||
>
|
>
|
||||||
{{ error?.statusCode }}
|
{{ error?.statusCode }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-2xl text-subtitle text-center">
|
<p class="text-lg md:text-2xl text-subtitle text-center">
|
||||||
Sorry, {{ error?.statusCode === 404
|
Sorry, {{ error?.statusCode === 404
|
||||||
? "the page you are looking for doesn't exist or as been moved."
|
? "the page you are looking for doesn't exist or as been moved."
|
||||||
: "you have encountered a problem."
|
: "you have encountered a problem."
|
||||||
}}
|
}}
|
||||||
<br>
|
<br>
|
||||||
Let's find a better place for you to go.
|
Let's find a better place for you to go.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center mt-8 mb-12">
|
<div class="flex justify-center mt-8 mb-12">
|
||||||
<UButton
|
<UButton
|
||||||
to="/"
|
to="/"
|
||||||
size="md"
|
size="md"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
Go back to the main page
|
Go back to the main page
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
</UContainer>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
export default withNuxt({
|
export default withNuxt({
|
||||||
rules: {
|
rules: {
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,29 +3,29 @@ import {SpeedInsights} from '@vercel/speed-insights/nuxt'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
router.afterEach((route) => {
|
router.afterEach((route) => {
|
||||||
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
|
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
|
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<SpeedInsights />
|
<SpeedInsights />
|
||||||
<Background />
|
<Background />
|
||||||
<NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" />
|
<NuxtLoadingIndicator :color="$colorMode.value === 'light' ? 'black' : 'white'" />
|
||||||
<section class="fixed inset-0 flex justify-center sm:px-8">
|
<section class="fixed inset-0 flex justify-center sm:px-8">
|
||||||
<div class="flex w-full max-w-9xl">
|
<div class="flex w-full max-w-9xl">
|
||||||
<div class="w-full z-20 bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
<div class="w-full z-20 bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<main class="relative z-50 min-h-[100svh]">
|
<main class="relative z-50 min-h-[100svh]">
|
||||||
<Header />
|
<Header />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
<UNotifications />
|
<UNotifications />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ const getColor = computed(() => appConfig.ui.primary)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLoadingIndicator :color="getColor" />
|
<NuxtLoadingIndicator :color="getColor" />
|
||||||
<section class="fixed inset-0 flex justify-center sm:px-8">
|
<section class="fixed inset-0 flex justify-center sm:px-8">
|
||||||
<div class="flex w-full max-w-9xl">
|
<div class="flex w-full max-w-9xl">
|
||||||
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
<div class="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="relative z-50 min-h-[100svh]">
|
<div class="relative z-50 min-h-[100svh]">
|
||||||
<Header :navigation="false" />
|
<Header :navigation="false" />
|
||||||
<UContainer>
|
<UContainer>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</UContainer>
|
</UContainer>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
export default defineNuxtRouteMiddleware(async (to) => {
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
const isMaintenance = ref<boolean>(true)
|
const isMaintenance = ref<boolean>(true)
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/maintenance').then((maintenance) => {
|
await $fetch('/api/maintenance').then((maintenance) => {
|
||||||
isMaintenance.value = maintenance.enabled
|
isMaintenance.value = maintenance.enabled
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return navigateTo('/maintenance')
|
return navigateTo('/maintenance')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMaintenance.value && to.path !== '/maintenance') {
|
if (isMaintenance.value && to.path !== '/maintenance') {
|
||||||
return navigateTo('/maintenance', {
|
return navigateTo('/maintenance', {
|
||||||
redirectCode: 301,
|
redirectCode: 301,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMaintenance.value && to.path === '/maintenance') {
|
if (!isMaintenance.value && to.path === '/maintenance') {
|
||||||
return navigateTo('/', {
|
return navigateTo('/', {
|
||||||
redirectCode: 301,
|
redirectCode: 301,
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default defineNuxtRouteMiddleware((to) => {
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
if (to.path === '/writing' && process.env.NODE_ENV !== 'development') {
|
if (to.path === '/writing' && process.env.NODE_ENV !== 'development') {
|
||||||
return navigateTo('/', {
|
return navigateTo('/', {
|
||||||
redirectCode: 301,
|
redirectCode: 301,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import {defineNuxtModule} from 'nuxt/kit'
|
|||||||
import {addCustomTab} from '@nuxt/devtools-kit'
|
import {addCustomTab} from '@nuxt/devtools-kit'
|
||||||
|
|
||||||
export default defineNuxtModule({
|
export default defineNuxtModule({
|
||||||
meta: {
|
meta: {
|
||||||
name: 'drizzle-studio',
|
name: 'drizzle-studio',
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
addCustomTab({
|
addCustomTab({
|
||||||
name: 'drizzle-studio',
|
name: 'drizzle-studio',
|
||||||
title: 'Drizzle Studio',
|
title: 'Drizzle Studio',
|
||||||
icon: 'simple-icons:drizzle',
|
icon: 'simple-icons:drizzle',
|
||||||
view: {
|
view: {
|
||||||
type: 'iframe',
|
type: 'iframe',
|
||||||
src: 'https://local.drizzle.studio/?themeId=azX2nOTScT9U6SWEmlq7z',
|
src: 'https://local.drizzle.studio/?themeId=azX2nOTScT9U6SWEmlq7z',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
134
nuxt.config.ts
134
nuxt.config.ts
@@ -1,80 +1,80 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
css: [
|
css: [
|
||||||
'@/assets/css/main.scss',
|
'@/assets/css/main.scss',
|
||||||
],
|
],
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/content',
|
'@nuxt/content',
|
||||||
'@nuxtjs/seo',
|
'@nuxtjs/seo',
|
||||||
'nuxt-auth-utils',
|
'nuxt-auth-utils',
|
||||||
'@nuxthq/studio',
|
'@nuxthq/studio',
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
'@pinia-plugin-persistedstate/nuxt',
|
'@pinia-plugin-persistedstate/nuxt',
|
||||||
'@vueuse/nuxt',
|
'@vueuse/nuxt',
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
],
|
],
|
||||||
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'light',
|
preference: 'light',
|
||||||
fallback: 'light',
|
fallback: 'light',
|
||||||
classPrefix: '',
|
classPrefix: '',
|
||||||
classSuffix: '',
|
classSuffix: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
components: [
|
components: [
|
||||||
'components/',
|
'components/',
|
||||||
'components/header',
|
'components/header',
|
||||||
'components/resume',
|
'components/resume',
|
||||||
'components/main',
|
'components/main',
|
||||||
],
|
],
|
||||||
|
|
||||||
content: {
|
content: {
|
||||||
highlight: {
|
highlight: {
|
||||||
theme: 'github-dark',
|
theme: 'github-dark',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
eslint: {
|
eslint: {
|
||||||
config: {
|
config: {
|
||||||
stylistic: {
|
stylistic: {
|
||||||
indent: 2,
|
indent: 2,
|
||||||
semi: false,
|
semi: false,
|
||||||
blockSpacing: true,
|
blockSpacing: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
icons: 'all',
|
icons: 'all',
|
||||||
},
|
},
|
||||||
|
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
||||||
timeline: {
|
timeline: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
site: {
|
site: {
|
||||||
url: 'https://arthurdanjou.fr',
|
url: 'https://arthurdanjou.fr',
|
||||||
name: 'Arthur Danjou\'s website',
|
name: 'Arthur Danjou\'s website',
|
||||||
description: 'I\'m Arthur DANJOU, a developer enjoying Cloud Infrastructure and Artificial Intelligence. Mathematics Student at Paris-Saclay',
|
description: 'I\'m Arthur DANJOU, a developer enjoying Cloud Infrastructure and Artificial Intelligence. Mathematics Student at Paris-Saclay',
|
||||||
},
|
},
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
discordUserId: process.env.NUXT_DISCORD_USER_ID,
|
discordUserId: process.env.NUXT_DISCORD_USER_ID,
|
||||||
discordId: process.env.NUXT_DISCORD_ID,
|
discordId: process.env.NUXT_DISCORD_ID,
|
||||||
discordToken: process.env.NUXT_DISCORD_TOKEN,
|
discordToken: process.env.NUXT_DISCORD_TOKEN,
|
||||||
wakatimeUserId: process.env.NUXT_WAKATIME_USER_UD,
|
wakatimeUserId: process.env.NUXT_WAKATIME_USER_UD,
|
||||||
wakatimeCodig: process.env.NUXT_WAKATIME_CODING,
|
wakatimeCodig: process.env.NUXT_WAKATIME_CODING,
|
||||||
wakatimeEditors: process.env.NUXT_WAKATIME_EDITORS,
|
wakatimeEditors: process.env.NUXT_WAKATIME_EDITORS,
|
||||||
wakatimeLanguages: process.env.NUXT_WAKATIME_LANGUAGES,
|
wakatimeLanguages: process.env.NUXT_WAKATIME_LANGUAGES,
|
||||||
wakatimeOs: process.env.NUXT_WAKATIME_OS,
|
wakatimeOs: process.env.NUXT_WAKATIME_OS,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
194
pages/about.vue
194
pages/about.vue
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
useHead({
|
||||||
title: 'About me • Arthur Danjou',
|
title: 'About me • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: skills } = await getSkills()
|
const { data: skills } = await getSkills()
|
||||||
@@ -9,100 +9,100 @@ const { data: experiences } = await getWorkExperiences()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:my-24 my-16">
|
<section class="w-container lg:my-24 my-16">
|
||||||
<div class="px-4 grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
<div class="px-4 grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
||||||
<div class="lg:pl-20 flex justify-center">
|
<div class="lg:pl-20 flex justify-center">
|
||||||
<div class="max-w-xs px-2.5 lg:max-w-none">
|
<div class="max-w-xs px-2.5 lg:max-w-none">
|
||||||
<UTooltip
|
<UTooltip
|
||||||
:popper="{ offsetDistance: 20 }"
|
:popper="{ offsetDistance: 20 }"
|
||||||
text="It's me 👋"
|
text="It's me 👋"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="My main profile picture"
|
alt="My main profile picture"
|
||||||
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||||
src="/about.png"
|
src="/about.png"
|
||||||
>
|
>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:order-first lg:row-span-2">
|
<div class="lg:order-first lg:row-span-2">
|
||||||
<div class="max-w-2xl space-y-8 mb-16">
|
<div class="max-w-2xl space-y-8 mb-16">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
I'm Arthur, I live and study in France where I learn new things.
|
I'm Arthur, I live and study in France where I learn new things.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
As a software engineer with a passion for AI and the cloud, I have a deep understanding of emerging technologies that are transforming the way businesses and organizations operate. I am at the heart of an ever-changing and rapidly growing field. My background in mathematics also gives me an edge in understanding the mathematical concepts and theories behind these technologies as well as how to design them.
|
As a software engineer with a passion for AI and the cloud, I have a deep understanding of emerging technologies that are transforming the way businesses and organizations operate. I am at the heart of an ever-changing and rapidly growing field. My background in mathematics also gives me an edge in understanding the mathematical concepts and theories behind these technologies as well as how to design them.
|
||||||
</p>
|
</p>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
I enjoy sharing my knowledge and learning new theorems and technologies. I am a curious person and eager to continue learning and growing throughout your life. My passion and commitment to these subjects are admirable qualities and will help me succeed in my career and education.
|
I enjoy sharing my knowledge and learning new theorems and technologies. I am a curious person and eager to continue learning and growing throughout your life. My passion and commitment to these subjects are admirable qualities and will help me succeed in my career and education.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GridSection title="Interests">
|
<GridSection title="Interests">
|
||||||
<GridSlot title="Development">
|
<GridSlot title="Development">
|
||||||
Development is the passion that appeared the earliest in my life. I started developing on Minecraft and then I migrated to the broad field of the web.
|
Development is the passion that appeared the earliest in my life. I started developing on Minecraft and then I migrated to the broad field of the web.
|
||||||
</GridSlot>
|
</GridSlot>
|
||||||
<GridSlot title="Mathematics">
|
<GridSlot title="Mathematics">
|
||||||
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
|
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
|
||||||
</GridSlot>
|
</GridSlot>
|
||||||
<GridSlot title="Artificial Intelligence">
|
<GridSlot title="Artificial Intelligence">
|
||||||
We hear more and more about artificial intelligence with the evolution of our society. So I quickly got interested by doing my own research and I quickly discovered that this field is closely related to mathematics, hence my interest.
|
We hear more and more about artificial intelligence with the evolution of our society. So I quickly got interested by doing my own research and I quickly discovered that this field is closely related to mathematics, hence my interest.
|
||||||
</GridSlot>
|
</GridSlot>
|
||||||
<GridSlot title="Cloud and infrastructure">
|
<GridSlot title="Cloud and infrastructure">
|
||||||
When you're doing development and deploying projects online, you discover and are forced to touch the cloud, infrastructure, and network. It's a totally different field than the others but just as interesting.
|
When you're doing development and deploying projects online, you discover and are forced to touch the cloud, infrastructure, and network. It's a totally different field than the others but just as interesting.
|
||||||
</GridSlot>
|
</GridSlot>
|
||||||
<GridSlot title="Fitness">
|
<GridSlot title="Fitness">
|
||||||
In addition to my studies and programming, I go to the gym every day to relax and stay in shape. Sport allows me to recharge my batteries and move on to other things.
|
In addition to my studies and programming, I go to the gym every day to relax and stay in shape. Sport allows me to recharge my batteries and move on to other things.
|
||||||
</GridSlot>
|
</GridSlot>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<GridSection
|
<GridSection
|
||||||
v-if="skills"
|
v-if="skills"
|
||||||
title="Skills"
|
title="Skills"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-3 md:grid-cols-4 gap-2">
|
<div class="grid grid-cols-3 md:grid-cols-4 gap-2">
|
||||||
<Skill
|
<Skill
|
||||||
v-for="skill in skills.body"
|
v-for="skill in skills.body"
|
||||||
:key="skill.name"
|
:key="skill.name"
|
||||||
:skill="skill"
|
:skill="skill"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<GridSection
|
<GridSection
|
||||||
v-if="experiences"
|
v-if="experiences"
|
||||||
title="Work Experiences"
|
title="Work Experiences"
|
||||||
>
|
>
|
||||||
<Experience
|
<Experience
|
||||||
v-for="experience in experiences"
|
v-for="experience in experiences"
|
||||||
:key="experience.title"
|
:key="experience.title"
|
||||||
:experience="experience"
|
:experience="experience"
|
||||||
/>
|
/>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<GridSection
|
<GridSection
|
||||||
v-if="educations"
|
v-if="educations"
|
||||||
title="Educations"
|
title="Educations"
|
||||||
>
|
>
|
||||||
<Education
|
<Education
|
||||||
v-for="education in educations"
|
v-for="education in educations"
|
||||||
:key="education.title"
|
:key="education.title"
|
||||||
:education="education"
|
:education="education"
|
||||||
/>
|
/>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UTooltip
|
<UTooltip
|
||||||
:popper="{ offsetDistance: 20 }"
|
:popper="{ offsetDistance: 20 }"
|
||||||
text="Click to discover my journey"
|
text="Click to discover my journey"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
label="Download my CV"
|
label="Download my CV"
|
||||||
icon="i-material-symbols-lab-profile-outline-rounded"
|
icon="i-material-symbols-lab-profile-outline-rounded"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xl"
|
size="xl"
|
||||||
to="/resume.pdf"
|
to="/resume.pdf"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,185 +2,185 @@
|
|||||||
import {useBookmarksStore} from '~/store/bookmarks'
|
import {useBookmarksStore} from '~/store/bookmarks'
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Discover my library • Arthur Danjou',
|
title: 'Discover my library • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const categories = ref<Array<{ label: string, slug: string }>>([{ label: 'All', slug: 'all' }])
|
const categories = ref<Array<{ label: string, slug: string }>>([{ label: 'All', slug: 'all' }])
|
||||||
const { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
|
const { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
|
||||||
|
|
||||||
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
|
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
query: {
|
query: {
|
||||||
favorite: isFavorite,
|
favorite: isFavorite,
|
||||||
category: getCategory,
|
category: getCategory,
|
||||||
},
|
},
|
||||||
watch: [isFavorite, getCategory],
|
watch: [isFavorite, getCategory],
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } })
|
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } })
|
||||||
getCategories.value!.forEach(category => categories.value.push({ label: category.name, slug: category.slug }))
|
getCategories.value!.forEach(category => categories.value.push({ label: category.name, slug: category.slug }))
|
||||||
|
|
||||||
function isCategory(slug: string) {
|
function isCategory(slug: string) {
|
||||||
return getCategory.value === slug
|
return getCategory.value === slug
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMarkerStyle = computed(() => {
|
const getMarkerStyle = computed(() => {
|
||||||
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
||||||
return {
|
return {
|
||||||
top: `${selected?.offsetTop}px`,
|
top: `${selected?.offsetTop}px`,
|
||||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||||
height: `${selected?.offsetHeight}px`,
|
height: `${selected?.offsetHeight}px`,
|
||||||
width: `${selected?.offsetWidth}px`,
|
width: `${selected?.offsetWidth}px`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
function getColor() {
|
function getColor() {
|
||||||
return `text-${appConfig.ui.primary}-500`
|
return `text-${appConfig.ui.primary}-500`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:my-24 my-16">
|
<section class="w-container lg:my-24 my-16">
|
||||||
<div class="max-w-2xl space-y-8 mb-16">
|
<div class="max-w-2xl space-y-8 mb-16">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
My library where I save some resources
|
My library where I save some resources
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
You will find a selection of some of the most inspiring and complete content I have read through my research and work experience.
|
You will find a selection of some of the most inspiring and complete content I have read through my research and work experience.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="getCategories"
|
v-if="getCategories"
|
||||||
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
|
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div
|
<div
|
||||||
class="absolute duration-300 left-1 ease-out focus:outline-none"
|
class="absolute duration-300 left-1 ease-out focus:outline-none"
|
||||||
:style="[getMarkerStyle]"
|
:style="[getMarkerStyle]"
|
||||||
>
|
>
|
||||||
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
|
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:id="category.slug"
|
:id="category.slug"
|
||||||
:key="category.slug"
|
:key="category.slug"
|
||||||
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
|
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
|
||||||
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
|
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
|
||||||
@click.prevent="setCategory(category.slug)"
|
@click.prevent="setCategory(category.slug)"
|
||||||
>
|
>
|
||||||
<p class="w-full">
|
<p class="w-full">
|
||||||
{{ category.label }}
|
{{ category.label }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UPopover>
|
<UPopover>
|
||||||
<UButton
|
<UButton
|
||||||
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
|
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
<template #panel>
|
<template #panel>
|
||||||
<div
|
<div
|
||||||
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
||||||
@click.prevent="toggleFavorite()"
|
@click.prevent="toggleFavorite()"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="isFavorite"
|
v-if="isFavorite"
|
||||||
name="i-material-symbols-check-box-outline-rounded"
|
name="i-material-symbols-check-box-outline-rounded"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-else
|
v-else
|
||||||
name="i-material-symbols-check-box-outline-blank"
|
name="i-material-symbols-check-box-outline-blank"
|
||||||
/>
|
/>
|
||||||
<p>Show favorites only</p>
|
<p>Show favorites only</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</div>
|
</div>
|
||||||
<UDivider class="my-2" />
|
<UDivider class="my-2" />
|
||||||
<div
|
<div
|
||||||
v-if="bookmarks && getCategories"
|
v-if="bookmarks && getCategories"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="bookmarks.length > 0 && !pending"
|
v-if="bookmarks.length > 0 && !pending"
|
||||||
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
|
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="bookmark in bookmarks"
|
v-for="bookmark in bookmarks"
|
||||||
:key="bookmark.name.toLowerCase().trim()"
|
:key="bookmark.name.toLowerCase().trim()"
|
||||||
class="group relative flex justify-between items-center"
|
class="group relative flex justify-between items-center"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-y-1">
|
<div class="flex flex-col gap-y-1">
|
||||||
<div class="flex gap-6 items-center">
|
<div class="flex gap-6 items-center">
|
||||||
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||||
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:href="bookmark.website"
|
:href="bookmark.website"
|
||||||
external
|
external
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<h1 class="relative z-10">
|
<h1 class="relative z-10">
|
||||||
{{ bookmark.name }}
|
{{ bookmark.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<UTooltip
|
<UTooltip
|
||||||
v-if="bookmark.favorite"
|
v-if="bookmark.favorite"
|
||||||
text="You can set the filter to only show favorites."
|
text="You can set the filter to only show favorites."
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||||
name="i-ic-round-star"
|
name="i-ic-round-star"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 z-10">
|
<div class="flex gap-2 z-10">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-for="category in bookmark.bookmarkCategories"
|
v-for="category in bookmark.bookmarkCategories"
|
||||||
:key="category.category.slug"
|
:key="category.category.slug"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
{{ category.category.name }}
|
{{ category.category.name }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
:class="getColor()"
|
:class="getColor()"
|
||||||
class="relative z-10 flex text-sm font-medium items-center"
|
class="relative z-10 flex text-sm font-medium items-center"
|
||||||
>
|
>
|
||||||
<UIcon name="i-ph-link-bold" />
|
<UIcon name="i-ph-link-bold" />
|
||||||
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
|
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="bookmarks?.length === 0 && !pending"
|
v-else-if="bookmarks?.length === 0 && !pending"
|
||||||
class="my-4 text-subtitle"
|
class="my-4 text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-akar-icons-cross" />
|
<UIcon name="i-akar-icons-cross" />
|
||||||
<p>There are no bookmarks for this category. Maybe soon...</p>
|
<p>There are no bookmarks for this category. Maybe soon...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="my-4 text-subtitle"
|
class="my-4 text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-eos-icons-loading" />
|
<UIcon name="i-eos-icons-loading" />
|
||||||
<p>The bookmarks are loading...</p>
|
<p>The bookmarks are loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {providers} from '~~/types'
|
import {providers} from '~~/types'
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Sign my guestbook • Arthur Danjou',
|
title: 'Sign my guestbook • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { loggedIn, clear, user } = useUserSession()
|
const { loggedIn, clear, user } = useUserSession()
|
||||||
@@ -13,196 +13,196 @@ const isOpen = ref(false)
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const messageContent = ref<string>('')
|
const messageContent = ref<string>('')
|
||||||
async function sign() {
|
async function sign() {
|
||||||
if (messageContent.value.length < 7 || messageContent.value.length > 250)
|
if (messageContent.value.length < 7 || messageContent.value.length > 250)
|
||||||
return
|
return
|
||||||
|
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
await $fetch('/api/message', {
|
await $fetch('/api/message', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: {
|
body: {
|
||||||
message: messageContent.value,
|
message: messageContent.value,
|
||||||
},
|
},
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: `Thanks for leaving a message!`,
|
title: `Thanks for leaving a message!`,
|
||||||
description: 'Your can see it at the top of the messages.',
|
description: 'Your can see it at the top of the messages.',
|
||||||
icon: 'i-material-symbols-check-circle-outline-rounded',
|
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
})
|
})
|
||||||
await refresh()
|
await refresh()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'An error occurred when signing the book!',
|
title: 'An error occurred when signing the book!',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
messageContent.value = ''
|
messageContent.value = ''
|
||||||
}
|
}
|
||||||
async function deleteMessage(id: number) {
|
async function deleteMessage(id: number) {
|
||||||
if (!user.value.admin)
|
if (!user.value.admin)
|
||||||
return
|
return
|
||||||
|
|
||||||
await $fetch('/api/message', {
|
await $fetch('/api/message', {
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
body: {
|
body: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: `Message successfully deleted`,
|
title: `Message successfully deleted`,
|
||||||
icon: 'i-material-symbols-check-circle-outline-rounded',
|
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
})
|
})
|
||||||
await refresh()
|
await refresh()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'An error occured when deleting a message!',
|
title: 'An error occured when deleting a message!',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:mt-24 my-8">
|
<section class="w-container lg:mt-24 my-8">
|
||||||
<div class="max-w-2xl space-y-8 md:mb-16 mb-8">
|
<div class="max-w-2xl space-y-8 md:mb-16 mb-8">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
You want to leave a message ?
|
You want to leave a message ?
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
Your opinion means a lot to me. Feel free to share your impressions of my projects, explore my site, or simply leave a personalised message. Your comments are a source of inspiration and continuous improvement. Thank you for taking the time to contribute to this virtual community. I look forward to reading what you have to share!
|
Your opinion means a lot to me. Feel free to share your impressions of my projects, explore my site, or simply leave a personalised message. Your comments are a source of inspiration and continuous improvement. Thank you for taking the time to contribute to this virtual community. I look forward to reading what you have to share!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center md:justify-start">
|
<div class="flex justify-center md:justify-start">
|
||||||
<UButton
|
<UButton
|
||||||
class="mb-8 md:mb-16"
|
class="mb-8 md:mb-16"
|
||||||
label="Want to sign my book ?"
|
label="Want to sign my book ?"
|
||||||
icon="i-ph-circle-wavy-question-bold"
|
icon="i-ph-circle-wavy-question-bold"
|
||||||
@click.prevent="isOpen = true"
|
@click.prevent="isOpen = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UModal v-model="isOpen">
|
<UModal v-model="isOpen">
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1
|
<h1
|
||||||
v-if="loggedIn"
|
v-if="loggedIn"
|
||||||
class="text-md font-bold"
|
class="text-md font-bold"
|
||||||
>
|
>
|
||||||
Enter just below your message to sign my book
|
Enter just below your message to sign my book
|
||||||
</h1>
|
</h1>
|
||||||
<h1
|
<h1
|
||||||
v-else
|
v-else
|
||||||
class="text-md font-bold"
|
class="text-md font-bold"
|
||||||
>
|
>
|
||||||
Sign before writing your message
|
Sign before writing your message
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
class="-my-1"
|
class="-my-1"
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="isOpen = false"
|
@click="isOpen = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-if="loggedIn"
|
v-if="loggedIn"
|
||||||
class="flex items-center justify-between gap-4"
|
class="flex items-center justify-between gap-4"
|
||||||
>
|
>
|
||||||
<div class="w-full relative flex items-center">
|
<div class="w-full relative flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="messageContent"
|
v-model="messageContent"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
min="7"
|
min="7"
|
||||||
max="58"
|
max="58"
|
||||||
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
|
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
|
||||||
placeholder="Leave a message"
|
placeholder="Leave a message"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
class="absolute right-1 top-1 rounded-md"
|
class="absolute right-1 top-1 rounded-md"
|
||||||
label="Send"
|
label="Send"
|
||||||
:disabled="messageContent.trim().length < 7 || messageContent.trim().length > 250"
|
:disabled="messageContent.trim().length < 7 || messageContent.trim().length > 250"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@click.prevent="sign()"
|
@click.prevent="sign()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click.prevent="clear()"
|
@click.prevent="clear()"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex gap-2 justify-center"
|
class="flex gap-2 justify-center"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.slug"
|
:key="provider.slug"
|
||||||
:label="provider.label"
|
:label="provider.label"
|
||||||
color="black"
|
color="black"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:to="provider.link"
|
:to="provider.link"
|
||||||
:icon="provider.icon"
|
:icon="provider.icon"
|
||||||
:external="true"
|
:external="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
<div
|
<div
|
||||||
v-if="messages"
|
v-if="messages"
|
||||||
class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8"
|
class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="relative overflow-hidden sm:p-6 px-4 py-5 border border-zinc-100 p-6 dark:border-zinc-700/40 rounded-lg"
|
class="relative overflow-hidden sm:p-6 px-4 py-5 border border-zinc-100 p-6 dark:border-zinc-700/40 rounded-lg"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-subtitle">
|
<p class="text-sm text-subtitle">
|
||||||
“{{ message.message }}”
|
“{{ message.message }}”
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-4 mt-4">
|
<div class="flex items-center gap-4 mt-4">
|
||||||
<div class="h-8 w-8 rounded-full">
|
<div class="h-8 w-8 rounded-full">
|
||||||
<img
|
<img
|
||||||
:src="message.image"
|
:src="message.image"
|
||||||
alt="Author profile picture"
|
alt="Author profile picture"
|
||||||
class="w-full h-full rounded-full"
|
class="w-full h-full rounded-full"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-bold">
|
<p class="font-bold">
|
||||||
{{ message.username }}
|
{{ message.username }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="user && user.admin"
|
v-if="user && user.admin"
|
||||||
class="absolute top-1 right-1"
|
class="absolute top-1 right-1"
|
||||||
icon="i-material-symbols-delete-forever-outline-rounded"
|
icon="i-material-symbols-delete-forever-outline-rounded"
|
||||||
color="red"
|
color="red"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:ui="{ rounded: 'rounded-full' }"
|
:ui="{ rounded: 'rounded-full' }"
|
||||||
size="xs"
|
size="xs"
|
||||||
@click.prevent="deleteMessage(message.id)"
|
@click.prevent="deleteMessage(message.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="my-4 text-subtitle"
|
class="my-4 text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-eos-icons-loading" />
|
<UIcon name="i-eos-icons-loading" />
|
||||||
<p>The messages are loading...</p>
|
<p>The messages are loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Arthur Danjou • Software Engineer and Maths Lover',
|
title: 'Arthur Danjou • Software Engineer and Maths Lover',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<Announcement />
|
<Announcement />
|
||||||
<MainBanner />
|
<MainBanner />
|
||||||
<div class="px-4 lg:px-44 md:px-16 sm:px-8 w-full my-16 grid grid-cols-1 md:grid-cols-2 md:gap-x-16 gap-y-4 md:gap-y-16">
|
<div class="px-4 lg:px-44 md:px-16 sm:px-8 w-full my-16 grid grid-cols-1 md:grid-cols-2 md:gap-x-16 gap-y-4 md:gap-y-16">
|
||||||
<MainActivity />
|
<MainActivity />
|
||||||
<MainStats />
|
<MainStats />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'maintenance',
|
layout: 'maintenance',
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Site under maintenance • Arthur Danjou',
|
title: 'Site under maintenance • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: maintenance } = await useFetch('/api/maintenance')
|
const { data: maintenance } = await useFetch('/api/maintenance')
|
||||||
@@ -14,68 +14,68 @@ const appConfig = useAppConfig()
|
|||||||
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||||
|
|
||||||
const socials = [
|
const socials = [
|
||||||
{
|
{
|
||||||
name: 'mail',
|
name: 'mail',
|
||||||
icon: 'i-material-symbols-alternate-email',
|
icon: 'i-material-symbols-alternate-email',
|
||||||
link: 'mailto:arthurdanjou@outlook.fr',
|
link: 'mailto:arthurdanjou@outlook.fr',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'twitter',
|
name: 'twitter',
|
||||||
icon: 'i-ph-twitter-logo-bold',
|
icon: 'i-ph-twitter-logo-bold',
|
||||||
link: 'https://twitter.com/ArthurDanj',
|
link: 'https://twitter.com/ArthurDanj',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'github',
|
name: 'github',
|
||||||
icon: 'i-ph-github-logo-bold',
|
icon: 'i-ph-github-logo-bold',
|
||||||
link: 'https://github.com/ArthurDanjou',
|
link: 'https://github.com/ArthurDanjou',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'linkedin',
|
name: 'linkedin',
|
||||||
icon: 'i-ph-linkedin-logo-bold',
|
icon: 'i-ph-linkedin-logo-bold',
|
||||||
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-full min-h-[80svh] flex justify-center items-center">
|
<section class="w-full min-h-[80svh] flex justify-center items-center">
|
||||||
<div class="text-center space-y-8 max-w-5xl">
|
<div class="text-center space-y-8 max-w-5xl">
|
||||||
<h3 class="uppercase text-xs text-transparent bg-clip-text bg-origin-content bg-gradient-to-b from-gray-100 to-gray-300 dark:from-zinc-600 to-55% dark:to-zinc-800">
|
<h3 class="uppercase text-xs text-transparent bg-clip-text bg-origin-content bg-gradient-to-b from-gray-100 to-gray-300 dark:from-zinc-600 to-55% dark:to-zinc-800">
|
||||||
Coming back soon
|
Coming back soon
|
||||||
</h3>
|
</h3>
|
||||||
<h1 class="text-4xl md:text-7xl font-bold">
|
<h1 class="text-4xl md:text-7xl font-bold">
|
||||||
The website is under maintenance
|
The website is under maintenance
|
||||||
</h1>
|
</h1>
|
||||||
<div v-if="maintenance && maintenance.maintenance">
|
<div v-if="maintenance && maintenance.maintenance">
|
||||||
<p
|
<p
|
||||||
:class="getColor"
|
:class="getColor"
|
||||||
class="font-bold mb-8 text-xl"
|
class="font-bold mb-8 text-xl"
|
||||||
>
|
>
|
||||||
{{ maintenance.maintenance.reason }}
|
{{ maintenance.maintenance.reason }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-subtitle italic">
|
<p class="text-subtitle italic">
|
||||||
Maintenance planned from {{ useDateFormat(maintenance.maintenance.beginAt, format).value }} to {{ useDateFormat(maintenance.maintenance.endAt, format).value }}
|
Maintenance planned from {{ useDateFormat(maintenance.maintenance.beginAt, format).value }} to {{ useDateFormat(maintenance.maintenance.endAt, format).value }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<a
|
<a
|
||||||
v-for="social in socials"
|
v-for="social in socials"
|
||||||
:key="social.name"
|
:key="social.name"
|
||||||
:href="social.link"
|
:href="social.link"
|
||||||
class="link"
|
class="link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
:class="social.icon"
|
:class="social.icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="flex-shrink-0 h-5 w-5"
|
class="flex-shrink-0 h-5 w-5"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTalentsStore } from '~/store/talents'
|
import {useTalentsStore} from '~/store/talents'
|
||||||
import { providers } from '~~/types'
|
import {providers} from '~~/types'
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Discover new talents • Arthur Danjou',
|
title: 'Discover new talents • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const categories = ref<Array<{ label: string, slug: string, id: number }>>([{ label: 'All', slug: 'all', id: 0 }])
|
const categories = ref<Array<{ label: string, slug: string, id: number }>>([{ label: 'All', slug: 'all', id: 0 }])
|
||||||
@@ -11,297 +11,297 @@ const { getCategory, setCategory, isFavorite, toggleFavorite } = useTalentsStore
|
|||||||
const { loggedIn, clear } = useUserSession()
|
const { loggedIn, clear } = useUserSession()
|
||||||
|
|
||||||
const { data: talents, pending } = await useFetch('/api/talents', {
|
const { data: talents, pending } = await useFetch('/api/talents', {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
query: {
|
query: {
|
||||||
favorite: isFavorite,
|
favorite: isFavorite,
|
||||||
category: getCategory,
|
category: getCategory,
|
||||||
},
|
},
|
||||||
watch: [isFavorite, getCategory],
|
watch: [isFavorite, getCategory],
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } })
|
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } })
|
||||||
getCategories.value!.forEach(category => categories.value.push({
|
getCategories.value!.forEach(category => categories.value.push({
|
||||||
label: category.name,
|
label: category.name,
|
||||||
slug: category.slug,
|
slug: category.slug,
|
||||||
id: category.id,
|
id: category.id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function isCategory(slug: string) {
|
function isCategory(slug: string) {
|
||||||
return getCategory.value === slug
|
return getCategory.value === slug
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMarkerStyle = computed(() => {
|
const getMarkerStyle = computed(() => {
|
||||||
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
||||||
return {
|
return {
|
||||||
top: `${selected?.offsetTop}px`,
|
top: `${selected?.offsetTop}px`,
|
||||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||||
height: `${selected?.offsetHeight}px`,
|
height: `${selected?.offsetHeight}px`,
|
||||||
width: `${selected?.offsetWidth}px`,
|
width: `${selected?.offsetWidth}px`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
function getColor() {
|
function getColor() {
|
||||||
return `text-${appConfig.ui.primary}-500`
|
return `text-${appConfig.ui.primary}-500`
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const suggestContent = ref<string>('')
|
const suggestContent = ref<string>('')
|
||||||
async function suggest() {
|
async function suggest() {
|
||||||
if (suggestContent.value.trim().length < 4)
|
if (suggestContent.value.trim().length < 4)
|
||||||
return
|
return
|
||||||
|
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
await $fetch('/api/suggestion', {
|
await $fetch('/api/suggestion', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: {
|
body: {
|
||||||
content: suggestContent.value,
|
content: suggestContent.value,
|
||||||
},
|
},
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: `Your suggestion for '${response[0].content}' has been successfully added`,
|
title: `Your suggestion for '${response[0].content}' has been successfully added`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: 'i-material-symbols-check-circle-outline-rounded',
|
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'An error occurred when suggesting someone',
|
title: 'An error occurred when suggesting someone',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
suggestContent.value = ''
|
suggestContent.value = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:mt-24 my-8">
|
<section class="w-container lg:mt-24 my-8">
|
||||||
<div class="max-w-2xl space-y-8 md:mb-16 mb-8">
|
<div class="max-w-2xl space-y-8 md:mb-16 mb-8">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
Showcasing here, I aim to share and introduce inspiring talents.
|
Showcasing here, I aim to share and introduce inspiring talents.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
You will find a selection of some of the most inspiring web talents I have discovered through my research and work experience. These talents are creative designers, talented web developers, passionate open-source contributors, and much more.
|
You will find a selection of some of the most inspiring web talents I have discovered through my research and work experience. These talents are creative designers, talented web developers, passionate open-source contributors, and much more.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center md:justify-start">
|
<div class="flex justify-center md:justify-start">
|
||||||
<UButton
|
<UButton
|
||||||
class="mb-8 md:mb-16"
|
class="mb-8 md:mb-16"
|
||||||
label="Want to suggest someone ?"
|
label="Want to suggest someone ?"
|
||||||
icon="i-ph-circle-wavy-question-bold"
|
icon="i-ph-circle-wavy-question-bold"
|
||||||
@click.prevent="isOpen = true"
|
@click.prevent="isOpen = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UModal v-model="isOpen">
|
<UModal v-model="isOpen">
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-md font-bold">
|
<h1 class="text-md font-bold">
|
||||||
Are you a web talent? Do you want to promote your project? Do you want to launch your career or gain visibility?
|
Are you a web talent? Do you want to promote your project? Do you want to launch your career or gain visibility?
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
class="-my-1"
|
class="-my-1"
|
||||||
color="gray"
|
color="gray"
|
||||||
icon="i-heroicons-x-mark-20-solid"
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="isOpen = false"
|
@click="isOpen = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-if="loggedIn"
|
v-if="loggedIn"
|
||||||
class="flex items-center justify-between gap-4"
|
class="flex items-center justify-between gap-4"
|
||||||
>
|
>
|
||||||
<div class="w-full relative flex items-center">
|
<div class="w-full relative flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="suggestContent"
|
v-model="suggestContent"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
min="4"
|
min="4"
|
||||||
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
|
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
|
||||||
placeholder="Suggest one name"
|
placeholder="Suggest one name"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
class="absolute right-1 top-1 rounded-md"
|
class="absolute right-1 top-1 rounded-md"
|
||||||
label="Send"
|
label="Send"
|
||||||
:disabled="suggestContent.trim().length < 4"
|
:disabled="suggestContent.trim().length < 4"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@click.prevent="suggest()"
|
@click.prevent="suggest()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click.prevent="clear()"
|
@click.prevent="clear()"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex gap-2 justify-center"
|
class="flex gap-2 justify-center"
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.slug"
|
:key="provider.slug"
|
||||||
:label="provider.label"
|
:label="provider.label"
|
||||||
color="black"
|
color="black"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:to="provider.link"
|
:to="provider.link"
|
||||||
:icon="provider.icon"
|
:icon="provider.icon"
|
||||||
:external="true"
|
:external="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
<div class="sticky z-40 top-[4.55rem] left-0 z-100 bg-white dark:bg-gray-900 pt-2">
|
<div class="sticky z-40 top-[4.55rem] left-0 z-100 bg-white dark:bg-gray-900 pt-2">
|
||||||
<div
|
<div
|
||||||
v-if="getCategories"
|
v-if="getCategories"
|
||||||
class="flex gap-2 w-full items-center justify-between"
|
class="flex gap-2 w-full items-center justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div
|
<div
|
||||||
class="absolute duration-300 left-1 ease-out focus:outline-none"
|
class="absolute duration-300 left-1 ease-out focus:outline-none"
|
||||||
:style="[getMarkerStyle]"
|
:style="[getMarkerStyle]"
|
||||||
>
|
>
|
||||||
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
|
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:id="category.slug"
|
:id="category.slug"
|
||||||
:key="category.slug"
|
:key="category.slug"
|
||||||
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
|
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
|
||||||
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
|
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
|
||||||
@click.prevent="setCategory(category.slug)"
|
@click.prevent="setCategory(category.slug)"
|
||||||
>
|
>
|
||||||
<p class="w-full">
|
<p class="w-full">
|
||||||
{{ category.label }}
|
{{ category.label }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UPopover>
|
<UPopover>
|
||||||
<UButton
|
<UButton
|
||||||
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
|
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
<template #panel>
|
<template #panel>
|
||||||
<div
|
<div
|
||||||
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
||||||
@click.prevent="toggleFavorite()"
|
@click.prevent="toggleFavorite()"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="isFavorite"
|
v-if="isFavorite"
|
||||||
name="i-material-symbols-check-box-outline-rounded"
|
name="i-material-symbols-check-box-outline-rounded"
|
||||||
/>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-else
|
v-else
|
||||||
name="i-material-symbols-check-box-outline-blank"
|
name="i-material-symbols-check-box-outline-blank"
|
||||||
/>
|
/>
|
||||||
<p>Show favorites only</p>
|
<p>Show favorites only</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</div>
|
</div>
|
||||||
<UDivider class="my-2" />
|
<UDivider class="my-2" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="talents && getCategories"
|
v-if="talents && getCategories"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="talents.length > 0 && !pending"
|
v-if="talents.length > 0 && !pending"
|
||||||
class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
|
class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="talent in talents"
|
v-for="talent in talents"
|
||||||
:key="talent.name.toLowerCase().trim()"
|
:key="talent.name.toLowerCase().trim()"
|
||||||
class="group relative flex flex-col justify-between"
|
class="group relative flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex gap-6 items-center">
|
<div class="flex gap-6 items-center">
|
||||||
<img
|
<img
|
||||||
:src="talent.logo"
|
:src="talent.logo"
|
||||||
alt="Talent profile picture"
|
alt="Talent profile picture"
|
||||||
class="z-20 h-8 w-8 md:h-12 md:w-12 rounded-md"
|
class="z-20 h-8 w-8 md:h-12 md:w-12 rounded-md"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||||
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="talent.website"
|
:to="talent.website"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<h1 class="relative z-10">
|
<h1 class="relative z-10">
|
||||||
{{ talent.name }}
|
{{ talent.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<UTooltip
|
<UTooltip
|
||||||
v-if="talent.favorite"
|
v-if="talent.favorite"
|
||||||
text="You can set the filter to only show favorites."
|
text="You can set the filter to only show favorites."
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||||
name="i-ic-round-star"
|
name="i-ic-round-star"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{{ talent.work }}
|
{{ talent.work }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 mt-2">
|
<div class="flex items-center gap-4 mt-2">
|
||||||
<p
|
<p
|
||||||
:class="getColor()"
|
:class="getColor()"
|
||||||
class="relative z-10 flex text-xs md:text-sm font-medium items-center"
|
class="relative z-10 flex text-xs md:text-sm font-medium items-center"
|
||||||
>
|
>
|
||||||
<UIcon name="i-ph-link-bold" />
|
<UIcon name="i-ph-link-bold" />
|
||||||
<span class="ml-2">{{ talent.website.replace('https://', '').replace('/', '') }}</span>
|
<span class="ml-2">{{ talent.website.replace('https://', '').replace('/', '') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2 z-10 flex-wrap">
|
<div class="flex gap-2 z-10 flex-wrap">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-for="category in talent.talentCategories"
|
v-for="category in talent.talentCategories"
|
||||||
:key="category.category.slug"
|
:key="category.category.slug"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
{{ category.category.name }}
|
{{ category.category.name }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="talents?.length === 0 && !pending"
|
v-else-if="talents?.length === 0 && !pending"
|
||||||
class="my-4 text-subtitle"
|
class="my-4 text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-akar-icons-cross" />
|
<UIcon name="i-akar-icons-cross" />
|
||||||
<p>There are no talents for this category. Maybe soon...</p>
|
<p>There are no talents for this category. Maybe soon...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="my-4 text-subtitle"
|
class="my-4 text-subtitle"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-eos-icons-loading" />
|
<UIcon name="i-eos-icons-loading" />
|
||||||
<p>The talents are loading...</p>
|
<p>The talents are loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:my-24 my-16">
|
<section class="w-container lg:my-24 my-16">
|
||||||
<div class="max-w-2xl space-y-8">
|
<div class="max-w-2xl space-y-8">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
Software I use, Hardware I own, and my favorite stack
|
Software I use, Hardware I own, and my favorite stack
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
I get often asked what I use to create software, to play games or to work and learn. Here's a big list of all my favourite things.
|
I get often asked what I use to create software, to play games or to work and learn. Here's a big list of all my favourite things.
|
||||||
</p>
|
</p>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ContentDoc
|
<ContentDoc
|
||||||
class="my-16"
|
class="my-16"
|
||||||
path="/uses"
|
path="/uses"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<USkeleton class="w-full h-1/2" />
|
<USkeleton class="w-full h-1/2" />
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
112
pages/work.vue
112
pages/work.vue
@@ -1,64 +1,64 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
useHead({
|
useHead({
|
||||||
title: 'My work • Arthur Danjou',
|
title: 'My work • Arthur Danjou',
|
||||||
})
|
})
|
||||||
const { data: projects } = await getProjects()
|
const { data: projects } = await getProjects()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:my-24 my-16">
|
<section class="w-container lg:my-24 my-16">
|
||||||
<div class="px-4 max-w-3xl space-y-8">
|
<div class="px-4 max-w-3xl space-y-8">
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
All my projects can be found on GitHub and by scrolling down.
|
All my projects can be found on GitHub and by scrolling down.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
I've worked on tons of little projects over the years but these are the ones that I'm most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved.
|
I've worked on tons of little projects over the years but these are the ones that I'm most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-16 md:mt-20">
|
<div class="mt-16 md:mt-20">
|
||||||
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div
|
<div
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
:key="project.name"
|
:key="project.name"
|
||||||
class="group relative flex flex-col justify-between"
|
class="group relative flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
<div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="project.icon"
|
:name="project.icon"
|
||||||
dynamic
|
dynamic
|
||||||
size="24"
|
size="24"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||||
<div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
<div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="project.link"
|
:to="project.link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<span class="relative z-10">{{ project.title }}</span>
|
<span class="relative z-10">{{ project.title }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex gap-2 z-10 flex-wrap">
|
<div class="mt-2 flex gap-2 z-10 flex-wrap">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-for="tag in project.tags"
|
v-for="tag in project.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,169 +7,169 @@ const appConfig = useAppConfig()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne())
|
const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne())
|
||||||
const {
|
const {
|
||||||
data: post,
|
data: post,
|
||||||
} = await useFetch<PrismaPost>('/api/article', {
|
} = await useFetch<PrismaPost>('/api/article', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: {
|
body: {
|
||||||
slug: route.params.slug.toString(),
|
slug: route.params.slug.toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const likes = ref(post.value?.likes)
|
const likes = ref(post.value?.likes)
|
||||||
async function like() {
|
async function like() {
|
||||||
const data = await $fetch<PrismaPost>('/api/like', {
|
const data = await $fetch<PrismaPost>('/api/like', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: {
|
body: {
|
||||||
slug: post.value?.slug,
|
slug: post.value?.slug,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
likes.value = data.likes
|
likes.value = data.likes
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postContent.value) {
|
if (!postContent.value) {
|
||||||
throw showError({
|
throw showError({
|
||||||
statusMessage: 'The post you are looking for was not found.',
|
statusMessage: 'The post you are looking for was not found.',
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
|
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
|
||||||
useHead({
|
useHead({
|
||||||
title: `${postContent.value?.title} • Arthur Danjou's shelf`,
|
title: `${postContent.value?.title} • Arthur Danjou's shelf`,
|
||||||
})
|
})
|
||||||
|
|
||||||
function top() {
|
function top() {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { copy, copied } = useClipboard({
|
const { copy, copied } = useClipboard({
|
||||||
source: `https://arthurdanjou.fr/writing/${route.params.slug}`,
|
source: `https://arthurdanjou.fr/writing/${route.params.slug}`,
|
||||||
copiedDuring: 4000,
|
copiedDuring: 4000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, {
|
const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, {
|
||||||
maxAge: 604_800,
|
maxAge: 604_800,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleLike() {
|
async function handleLike() {
|
||||||
await like()
|
await like()
|
||||||
likeCookie.value = true
|
likeCookie.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
v-if="postContent && post"
|
v-if="postContent && post"
|
||||||
class="w-container lg:mt-24 mt-16"
|
class="w-container lg:mt-24 mt-16"
|
||||||
>
|
>
|
||||||
<div class="lg:relative">
|
<div class="lg:relative">
|
||||||
<div class="max-w-3xl space-y-8 mx-auto">
|
<div class="max-w-3xl space-y-8 mx-auto">
|
||||||
<div class="mx-auto max-w-2xl">
|
<div class="mx-auto max-w-2xl">
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-ph-arrow-circle-left-bold"
|
icon="i-ph-arrow-circle-left-bold"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="lg"
|
size="lg"
|
||||||
:ui="{ rounded: 'rounded-full' }"
|
:ui="{ rounded: 'rounded-full' }"
|
||||||
class="lg:absolute left-0 mb-8"
|
class="lg:absolute left-0 mb-8"
|
||||||
@click.prevent="useRouter().back()"
|
@click.prevent="useRouter().back()"
|
||||||
/>
|
/>
|
||||||
<article>
|
<article>
|
||||||
<header class="flex flex-col space-y-6">
|
<header class="flex flex-col space-y-6">
|
||||||
<time class="flex items-center text-base text-zinc-400 dark:text-zinc-500">
|
<time class="flex items-center text-base text-zinc-400 dark:text-zinc-500">
|
||||||
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
<div class="ml-3 flex gap-3">
|
<div class="ml-3 flex gap-3">
|
||||||
<div>
|
<div>
|
||||||
{{ format(postContent.publishedAt) }}
|
{{ format(postContent.publishedAt) }}
|
||||||
</div>
|
</div>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div>{{ postContent.readingMins }} min</div>
|
<div>{{ postContent.readingMins }} min</div>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div>{{ post.views }} {{ post.views > 1 ? 'views' : 'view' }}</div>
|
<div>{{ post.views }} {{ post.views > 1 ? 'views' : 'view' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</time>
|
</time>
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
{{ postContent.title }}
|
{{ postContent.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
{{ postContent.description }}
|
{{ postContent.description }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
v-if="postContent.cover"
|
v-if="postContent.cover"
|
||||||
class="w-full rounded-md my-8"
|
class="w-full rounded-md my-8"
|
||||||
>
|
>
|
||||||
{{ postContent.cover }}
|
{{ postContent.cover }}
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ContentRenderer
|
<ContentRenderer
|
||||||
class="mt-12 prose dark:prose-invert max-w-none prose-style"
|
class="mt-12 prose dark:prose-invert max-w-none prose-style"
|
||||||
:class="`prose-${appConfig.ui.primary}`"
|
:class="`prose-${appConfig.ui.primary}`"
|
||||||
:value="postContent"
|
:value="postContent"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="my-16 text-subtitle">
|
<div class="my-16 text-subtitle">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<UIcon name="i-eos-icons-loading" />
|
<UIcon name="i-eos-icons-loading" />
|
||||||
<p>The content of the post is loading...</p>
|
<p>The content of the post is loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<footer class="my-8 space-y-8">
|
<footer class="my-8 space-y-8">
|
||||||
<UDivider />
|
<UDivider />
|
||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
Thanks for reading this post! If you liked it, please consider sharing it with your friends. <strong>Don't forget to leave a like!</strong>
|
Thanks for reading this post! If you liked it, please consider sharing it with your friends. <strong>Don't forget to leave a like!</strong>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4 flex-wrap">
|
<div class="flex gap-4 flex-wrap">
|
||||||
<UButton
|
<UButton
|
||||||
:label="`${likes} ${likes! > 1 ? 'likes' : 'like'}`"
|
:label="`${likes} ${likes! > 1 ? 'likes' : 'like'}`"
|
||||||
icon="i-ph-heart-bold"
|
icon="i-ph-heart-bold"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.prevent="handleLike()"
|
@click.prevent="handleLike()"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
label="Go to top"
|
label="Go to top"
|
||||||
icon="i-ph-arrow-up-bold"
|
icon="i-ph-arrow-up-bold"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.prevent="top()"
|
@click.prevent="top()"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
label="Share on Twitter"
|
label="Share on Twitter"
|
||||||
icon="i-ph-twitter-logo-bold"
|
icon="i-ph-twitter-logo-bold"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.prevent="copy()"
|
@click.prevent="copy()"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="copied"
|
v-if="copied"
|
||||||
label="Link copied"
|
label="Link copied"
|
||||||
icon="i-lucide-clipboard-check"
|
icon="i-lucide-clipboard-check"
|
||||||
color="green"
|
color="green"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.prevent="copy()"
|
@click.prevent="copy()"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
v-else
|
v-else
|
||||||
label="Copy link"
|
label="Copy link"
|
||||||
icon="i-lucide-clipboard"
|
icon="i-lucide-clipboard"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@click.prevent="copy()"
|
@click.prevent="copy()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const appConfig = useAppConfig()
|
|||||||
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'My Shelf • Arthur Danjou',
|
title: 'My Shelf • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: posts } = await getPosts()
|
const { data: posts } = await getPosts()
|
||||||
@@ -11,59 +11,59 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-container lg:my-24 my-16">
|
<section class="w-container lg:my-24 my-16">
|
||||||
<div class="px-4 max-w-3xl space-y-8">
|
<div class="px-4 max-w-3xl space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||||
Writing on my life, development and my passions.
|
Writing on my life, development and my passions.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="leading-relaxed text-subtitle">
|
<p class="leading-relaxed text-subtitle">
|
||||||
All my thoughts on programming, mathematics, artificial intelligence design, etc., are put together in chronological order. I also write about my projects, my discoveries, and my thoughts. <s>It is sometimes updated.</s>
|
All my thoughts on programming, mathematics, artificial intelligence design, etc., are put together in chronological order. I also write about my projects, my discoveries, and my thoughts. <s>It is sometimes updated.</s>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-16 md:mt-20">
|
<div class="mt-16 md:mt-20">
|
||||||
<div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
|
<div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
|
||||||
<div class="flex max-w-3xl flex-col space-y-16">
|
<div class="flex max-w-3xl flex-col space-y-16">
|
||||||
<article
|
<article
|
||||||
v-for="post in posts"
|
v-for="post in posts"
|
||||||
:key="post.slug"
|
:key="post.slug"
|
||||||
class="px-6 md:grid md:grid-cols-4 md:items-baseline"
|
class="px-6 md:grid md:grid-cols-4 md:items-baseline"
|
||||||
>
|
>
|
||||||
<div class="group md:col-span-3 group relative flex flex-col items-start">
|
<div class="group md:col-span-3 group relative flex flex-col items-start">
|
||||||
<h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
<h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
<div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" />
|
<div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" />
|
||||||
<NuxtLink :to="post._path">
|
<NuxtLink :to="post._path">
|
||||||
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<span class="relative z-10">
|
<span class="relative z-10">
|
||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
</span>
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</h2>
|
</h2>
|
||||||
<time class="md:hidden relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500 pl-3.5">
|
<time class="md:hidden relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500 pl-3.5">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center">
|
<span class="absolute inset-y-0 left-0 flex items-center">
|
||||||
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
</span>
|
</span>
|
||||||
{{ format(post.publishedAt) }}
|
{{ format(post.publishedAt) }}
|
||||||
</time>
|
</time>
|
||||||
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{{ post.description }}
|
{{ post.description }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
:class="getColor"
|
:class="getColor"
|
||||||
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
|
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
|
||||||
>
|
>
|
||||||
<p>Read article</p>
|
<p>Read article</p>
|
||||||
<UIcon name="i-ph-arrow-circle-right-bold" />
|
<UIcon name="i-ph-arrow-circle-right-bold" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<time class="mt-1 md:block relative z-10 order-first mb-3 hidden text-sm text-zinc-400 dark:text-zinc-500">
|
<time class="mt-1 md:block relative z-10 order-first mb-3 hidden text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
<p>{{ format(post.publishedAt) }}</p>
|
<p>{{ format(post.publishedAt) }}</p>
|
||||||
<p>{{ post.readingMins }} min.</p>
|
<p>{{ post.readingMins }} min.</p>
|
||||||
</time>
|
</time>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { inject } from '@vercel/analytics'
|
import {inject} from '@vercel/analytics'
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
inject()
|
inject()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
return await $fetch(`https://api.lanyard.rest/v1/users/${config.discordUserId}`)
|
return await $fetch(`https://api.lanyard.rest/v1/users/${config.discordUserId}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
return useDB().query.announcements.findFirst({
|
return useDB().query.announcements.findFirst({
|
||||||
orderBy: (announcement, { asc }) => [asc(announcement.createdAt)],
|
orderBy: (announcement, { asc }) => [asc(announcement.createdAt)],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import {z} from 'zod'
|
|||||||
const PostSchema = z.object({ slug: z.string() }).parse
|
const PostSchema = z.object({ slug: z.string() }).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { slug } = await readValidatedBody(event, PostSchema)
|
const { slug } = await readValidatedBody(event, PostSchema)
|
||||||
return useDB().insert(tables.posts).values({
|
return useDB().insert(tables.posts).values({
|
||||||
slug,
|
slug,
|
||||||
}).onConflictDoUpdate({
|
}).onConflictDoUpdate({
|
||||||
target: tables.posts.id,
|
target: tables.posts.id,
|
||||||
set: {
|
set: {
|
||||||
views: sql`${tables.posts.views}
|
views: sql`${tables.posts.views}
|
||||||
+ 1`,
|
+ 1`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { favorite, category } = getQuery(event)
|
const { favorite, category } = getQuery(event)
|
||||||
|
|
||||||
const bookmarks = await useDB().query.bookmarks
|
const bookmarks = await useDB().query.bookmarks
|
||||||
.findMany({
|
.findMany({
|
||||||
orderBy: [asc(tables.talents.id)],
|
orderBy: [asc(tables.talents.id)],
|
||||||
with: {
|
with: {
|
||||||
bookmarkCategories: {
|
bookmarkCategories: {
|
||||||
with: {
|
with: {
|
||||||
category: true,
|
category: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return bookmarks.filter(bookmark =>
|
return bookmarks.filter(bookmark =>
|
||||||
(category === 'all' || bookmark.bookmarkCategories.some(cat => cat.category.slug === category))
|
(category === 'all' || bookmark.bookmarkCategories.some(cat => cat.category.slug === category))
|
||||||
&& (favorite === 'false' || bookmark.favorite),
|
&& (favorite === 'false' || bookmark.favorite),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
|
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
|
||||||
return useDB().select().from(tables.categories).where(eq(tables.categories.type, type))
|
return useDB().select().from(tables.categories).where(eq(tables.categories.type, type))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {z} from 'zod'
|
|||||||
const PostSchema = z.object({ slug: z.string() }).parse
|
const PostSchema = z.object({ slug: z.string() }).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { slug } = await readValidatedBody(event, PostSchema)
|
const { slug } = await readValidatedBody(event, PostSchema)
|
||||||
return useDB().update(tables.posts)
|
return useDB().update(tables.posts)
|
||||||
.set({
|
.set({
|
||||||
likes: sql`${tables.posts.likes}
|
likes: sql`${tables.posts.likes}
|
||||||
+ 1`,
|
+ 1`,
|
||||||
})
|
})
|
||||||
.where(eq(tables.posts.slug, slug))
|
.where(eq(tables.posts.slug, slug))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
const maintenance = await useDB().query.maintenances.findFirst({
|
const maintenance = await useDB().query.maintenances.findFirst({
|
||||||
orderBy: [asc(tables.maintenances.createdAt)],
|
orderBy: [asc(tables.maintenances.createdAt)],
|
||||||
})
|
})
|
||||||
let enabled = true
|
let enabled = true
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
enabled = !!maintenance
|
enabled = !!maintenance
|
||||||
&& maintenance.enabled
|
&& maintenance.enabled
|
||||||
&& new Date(maintenance.beginAt).getTime() < today.getTime()
|
&& new Date(maintenance.beginAt).getTime() < today.getTime()
|
||||||
&& new Date(maintenance.endAt).getTime() > today.getTime()
|
&& new Date(maintenance.endAt).getTime() > today.getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
maintenance,
|
maintenance,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
const MessageValidator = z.object({
|
const MessageValidator = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
}).parse
|
}).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { id } = await readValidatedBody(event, MessageValidator)
|
const { id } = await readValidatedBody(event, MessageValidator)
|
||||||
const { user } = await requireUserSession(event)
|
const { user } = await requireUserSession(event)
|
||||||
if (!user.admin)
|
if (!user.admin)
|
||||||
throw createError({ statusCode: 400, message: 'You need the permission to delete a message!' })
|
throw createError({ statusCode: 400, message: 'You need the permission to delete a message!' })
|
||||||
return useDB().delete(tables.guestbookMessages).where(eq(tables.guestbookMessages.id, id))
|
return useDB().delete(tables.guestbookMessages).where(eq(tables.guestbookMessages.id, id))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
const MessageValidator = z.object({
|
const MessageValidator = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
}).parse
|
}).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { message } = await readValidatedBody(event, MessageValidator)
|
const { message } = await readValidatedBody(event, MessageValidator)
|
||||||
const { user } = await requireUserSession(event)
|
const { user } = await requireUserSession(event)
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
|
|
||||||
await sendDiscordWebhookMessage(config, {
|
await sendDiscordWebhookMessage(config, {
|
||||||
title: 'New guestbook message ✨',
|
title: 'New guestbook message ✨',
|
||||||
description: `**${user.username}** has signed the book : "*${message}*"`,
|
description: `**${user.username}** has signed the book : "*${message}*"`,
|
||||||
color: 15893567,
|
color: 15893567,
|
||||||
})
|
})
|
||||||
return useDB().insert(tables.guestbookMessages)
|
return useDB().insert(tables.guestbookMessages)
|
||||||
.values({
|
.values({
|
||||||
message,
|
message,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
image: user.picture,
|
image: user.picture,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: tables.guestbookMessages.email,
|
target: tables.guestbookMessages.email,
|
||||||
set: {
|
set: {
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
return useDB().select().from(tables.guestbookMessages).orderBy(asc(tables.guestbookMessages.createdAt))
|
return useDB().select().from(tables.guestbookMessages).orderBy(asc(tables.guestbookMessages.createdAt))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export default defineCachedEventHandler(async (event) => {
|
export default defineCachedEventHandler(async (event) => {
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
const coding = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeCodig}.json`)
|
const coding = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeCodig}.json`)
|
||||||
const editors = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeEditors}.json`)
|
const editors = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeEditors}.json`)
|
||||||
const os = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeOs}.json`)
|
const os = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeOs}.json`)
|
||||||
const languages = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeLanguages}.json`)
|
const languages = await $fetch(`https://wakatime.com/share/${config.wakatimeUserId}/${config.wakatimeLanguages}.json`)
|
||||||
return {
|
return {
|
||||||
coding,
|
coding,
|
||||||
editors,
|
editors,
|
||||||
os,
|
os,
|
||||||
languages,
|
languages,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
maxAge: 60 * 60 * 3, // 3 hours,
|
maxAge: 60 * 60 * 3, // 3 hours,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
const SuggestionValidator = z.object({
|
const SuggestionValidator = z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
}).parse
|
}).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { content } = await readValidatedBody(event, SuggestionValidator)
|
const { content } = await readValidatedBody(event, SuggestionValidator)
|
||||||
const { user } = await requireUserSession(event)
|
const { user } = await requireUserSession(event)
|
||||||
const config = useRuntimeConfig(event)
|
const config = useRuntimeConfig(event)
|
||||||
|
|
||||||
await sendDiscordWebhookMessage(config, {
|
await sendDiscordWebhookMessage(config, {
|
||||||
title: 'New suggestion ✨',
|
title: 'New suggestion ✨',
|
||||||
description: `**${user.username}** has requested **${content}** for the talents page.`,
|
description: `**${user.username}** has requested **${content}** for the talents page.`,
|
||||||
color: 15237114,
|
color: 15237114,
|
||||||
})
|
})
|
||||||
|
|
||||||
return useDB().insert(tables.suggestions)
|
return useDB().insert(tables.suggestions)
|
||||||
.values({
|
.values({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: tables.suggestions.email,
|
target: tables.suggestions.email,
|
||||||
set: {
|
set: {
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
setWhere: sql`${tables.suggestions.email}
|
setWhere: sql`${tables.suggestions.email}
|
||||||
=
|
=
|
||||||
${user.email}`,
|
${user.email}`,
|
||||||
}).returning({
|
}).returning({
|
||||||
content: tables.suggestions.content,
|
content: tables.suggestions.content,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { favorite, category } = getQuery(event)
|
const { favorite, category } = getQuery(event)
|
||||||
|
|
||||||
const talents = await useDB().query.talents
|
const talents = await useDB().query.talents
|
||||||
.findMany({
|
.findMany({
|
||||||
orderBy: [asc(tables.talents.id)],
|
orderBy: [asc(tables.talents.id)],
|
||||||
with: {
|
with: {
|
||||||
talentCategories: {
|
talentCategories: {
|
||||||
with: {
|
with: {
|
||||||
category: true,
|
category: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return talents.filter(talent =>
|
return talents.filter(talent =>
|
||||||
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
|
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
|
||||||
&& (favorite === 'false' || talent.favorite),
|
&& (favorite === 'false' || talent.favorite),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { z } from 'zod'
|
import {z} from 'zod'
|
||||||
|
|
||||||
const PostSchema = z.object({ slug: z.string() }).parse
|
const PostSchema = z.object({ slug: z.string() }).parse
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { slug } = await readValidatedBody(event, PostSchema)
|
const { slug } = await readValidatedBody(event, PostSchema)
|
||||||
return useDB().update(tables.posts)
|
return useDB().update(tables.posts)
|
||||||
.set({
|
.set({
|
||||||
views: sql`${tables.posts.views}
|
views: sql`${tables.posts.views}
|
||||||
+ 1`,
|
+ 1`,
|
||||||
})
|
})
|
||||||
.where(eq(tables.posts.slug, slug))
|
.where(eq(tables.posts.slug, slug))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,125 +4,125 @@ import {relations} from 'drizzle-orm'
|
|||||||
// O B J E C T S
|
// O B J E C T S
|
||||||
|
|
||||||
export const maintenances = pgTable('maintenances', {
|
export const maintenances = pgTable('maintenances', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
reason: text('reason').default(''),
|
reason: text('reason').default(''),
|
||||||
enabled: boolean('enabled').default(false).notNull(),
|
enabled: boolean('enabled').default(false).notNull(),
|
||||||
beginAt: date('begin_at').defaultNow().notNull(),
|
beginAt: date('begin_at').defaultNow().notNull(),
|
||||||
endAt: date('end_at').defaultNow().notNull(),
|
endAt: date('end_at').defaultNow().notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const announcements = pgTable('announcements', {
|
export const announcements = pgTable('announcements', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
content: text('content').default('').notNull(),
|
content: text('content').default('').notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const posts = pgTable('posts', {
|
export const posts = pgTable('posts', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
slug: text('slug').notNull(),
|
slug: text('slug').notNull(),
|
||||||
likes: integer('likes').default(0).notNull(),
|
likes: integer('likes').default(0).notNull(),
|
||||||
views: integer('views').default(0).notNull(),
|
views: integer('views').default(0).notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const suggestions = pgTable('suggestions', {
|
export const suggestions = pgTable('suggestions', {
|
||||||
id: serial('id').notNull(),
|
id: serial('id').notNull(),
|
||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
content: text('content').notNull(),
|
content: text('content').notNull(),
|
||||||
added: boolean('added').default(false).notNull(),
|
added: boolean('added').default(false).notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
}, t => ({
|
}, t => ({
|
||||||
pk: primaryKey({ columns: [t.id, t.email] }),
|
pk: primaryKey({ columns: [t.id, t.email] }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const guestbookMessages = pgTable('guestbook_messages', {
|
export const guestbookMessages = pgTable('guestbook_messages', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
message: text('message').notNull(),
|
message: text('message').notNull(),
|
||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
username: text('username').notNull(),
|
username: text('username').notNull(),
|
||||||
image: text('image').notNull(),
|
image: text('image').notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark'])
|
export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark'])
|
||||||
|
|
||||||
export const categories = pgTable('categories', {
|
export const categories = pgTable('categories', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
slug: text('slug').unique().notNull(),
|
slug: text('slug').unique().notNull(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
type: categoriesType('type').notNull(),
|
type: categoriesType('type').notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const talents = pgTable('talents', {
|
export const talents = pgTable('talents', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
logo: text('logo').default('').notNull(),
|
logo: text('logo').default('').notNull(),
|
||||||
website: text('website').default('').notNull(),
|
website: text('website').default('').notNull(),
|
||||||
work: text('work').default('').notNull(),
|
work: text('work').default('').notNull(),
|
||||||
favorite: boolean('favorite').default(false).notNull(),
|
favorite: boolean('favorite').default(false).notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const talentsToCategories = pgTable('talents_categories', {
|
export const talentsToCategories = pgTable('talents_categories', {
|
||||||
talentId: integer('talent_id').notNull()
|
talentId: integer('talent_id').notNull()
|
||||||
.references(() => talents.id, { onDelete: 'cascade' }),
|
.references(() => talents.id, { onDelete: 'cascade' }),
|
||||||
categoryId: integer('category_id').notNull()
|
categoryId: integer('category_id').notNull()
|
||||||
.references(() => categories.id, { onDelete: 'cascade' }),
|
.references(() => categories.id, { onDelete: 'cascade' }),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const bookmarks = pgTable('bookmarks', {
|
export const bookmarks = pgTable('bookmarks', {
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
website: text('website').default('').notNull(),
|
website: text('website').default('').notNull(),
|
||||||
favorite: boolean('favorite').default(false).notNull(),
|
favorite: boolean('favorite').default(false).notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const bookmarksToCategories = pgTable('bookmarks_categories', {
|
export const bookmarksToCategories = pgTable('bookmarks_categories', {
|
||||||
bookmarkId: integer('bookmark_id').notNull()
|
bookmarkId: integer('bookmark_id').notNull()
|
||||||
.references(() => bookmarks.id, { onDelete: 'cascade' }),
|
.references(() => bookmarks.id, { onDelete: 'cascade' }),
|
||||||
categoryId: integer('category_id').notNull()
|
categoryId: integer('category_id').notNull()
|
||||||
.references(() => categories.id, { onDelete: 'cascade' }),
|
.references(() => categories.id, { onDelete: 'cascade' }),
|
||||||
}, t => ({
|
}, t => ({
|
||||||
pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }),
|
pk: primaryKey({ columns: [t.bookmarkId, t.categoryId] }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// R E L A T I O N S
|
// R E L A T I O N S
|
||||||
|
|
||||||
export const talentsRelations = relations(talents, ({ many }) => ({
|
export const talentsRelations = relations(talents, ({ many }) => ({
|
||||||
talentCategories: many(talentsToCategories),
|
talentCategories: many(talentsToCategories),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
|
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
|
||||||
bookmarkCategories: many(bookmarksToCategories),
|
bookmarkCategories: many(bookmarksToCategories),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const categoriesRelations = relations(categories, ({ many }) => ({
|
export const categoriesRelations = relations(categories, ({ many }) => ({
|
||||||
talentsToCategories: many(talentsToCategories),
|
talentsToCategories: many(talentsToCategories),
|
||||||
bookmarksToCategories: many(bookmarksToCategories),
|
bookmarksToCategories: many(bookmarksToCategories),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({
|
export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({
|
||||||
talent: one(talents, {
|
talent: one(talents, {
|
||||||
references: [talents.id],
|
references: [talents.id],
|
||||||
fields: [talentsToCategories.talentId],
|
fields: [talentsToCategories.talentId],
|
||||||
}),
|
}),
|
||||||
category: one(categories, {
|
category: one(categories, {
|
||||||
references: [categories.id],
|
references: [categories.id],
|
||||||
fields: [talentsToCategories.categoryId],
|
fields: [talentsToCategories.categoryId],
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({
|
export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({
|
||||||
bookmark: one(bookmarks, {
|
bookmark: one(bookmarks, {
|
||||||
references: [bookmarks.id],
|
references: [bookmarks.id],
|
||||||
fields: [bookmarksToCategories.bookmarkId],
|
fields: [bookmarksToCategories.bookmarkId],
|
||||||
}),
|
}),
|
||||||
category: one(categories, {
|
category: one(categories, {
|
||||||
references: [categories.id],
|
references: [categories.id],
|
||||||
fields: [bookmarksToCategories.categoryId],
|
fields: [bookmarksToCategories.categoryId],
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
export default oauth.githubEventHandler({
|
export default oauth.githubEventHandler({
|
||||||
config: {
|
config: {
|
||||||
emailRequired: true,
|
emailRequired: true,
|
||||||
},
|
},
|
||||||
async onSuccess(event, { user }) {
|
async onSuccess(event, { user }) {
|
||||||
await setUserSession(event, {
|
await setUserSession(event, {
|
||||||
user: {
|
user: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.avatar_url,
|
picture: user.avatar_url,
|
||||||
username: String(user.name).trim(),
|
username: String(user.name).trim(),
|
||||||
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
|
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return sendRedirect(event, getCookie(event, 'last-route') || '/')
|
return sendRedirect(event, getCookie(event, 'last-route') || '/')
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error('GitHub OAuth error:', error)
|
console.error('GitHub OAuth error:', error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export default oauth.googleEventHandler({
|
export default oauth.googleEventHandler({
|
||||||
async onSuccess(event, { user }) {
|
async onSuccess(event, { user }) {
|
||||||
await setUserSession(event, {
|
await setUserSession(event, {
|
||||||
user: {
|
user: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.picture,
|
picture: user.picture,
|
||||||
username: String(user.name).trim(),
|
username: String(user.name).trim(),
|
||||||
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
|
admin: user.email === process.env.NUXT_AUTH_ADMIN_EMAIL,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return sendRedirect(event, getCookie(event, 'last-route') || '/')
|
return sendRedirect(event, getCookie(event, 'last-route') || '/')
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error('Google OAuth error:', error)
|
console.error('Google OAuth error:', error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ const connectionString = process.env.DATABASE_URL as string
|
|||||||
const client = postgres(connectionString, { prepare: false })
|
const client = postgres(connectionString, { prepare: false })
|
||||||
|
|
||||||
export function useDB() {
|
export function useDB() {
|
||||||
return drizzle(client, { schema })
|
return drizzle(client, { schema })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import type {RuntimeConfig} from 'nuxt/schema'
|
import type {RuntimeConfig} from 'nuxt/schema'
|
||||||
|
|
||||||
interface WebhookContent {
|
interface WebhookContent {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
color: number
|
color: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendDiscordWebhookMessage(config: RuntimeConfig, content: WebhookContent) {
|
export async function sendDiscordWebhookMessage(config: RuntimeConfig, content: WebhookContent) {
|
||||||
await $fetch(`https://discordapp.com/api/webhooks/${config.discordId}/${config.discordToken}`, {
|
await $fetch(`https://discordapp.com/api/webhooks/${config.discordId}/${config.discordToken}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: content.title,
|
title: content.title,
|
||||||
description: content.description,
|
description: content.description,
|
||||||
color: content.color,
|
color: content.color,
|
||||||
url: 'https://arthurdanjou.fr/talents',
|
url: 'https://arthurdanjou.fr/talents',
|
||||||
footer: {
|
footer: {
|
||||||
text: 'Powered by Nuxt',
|
text: 'Powered by Nuxt',
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
username: 'ArtDanjRobot - Website',
|
username: 'ArtDanjRobot - Website',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
|
|
||||||
export const useBookmarksStore = defineStore(
|
export const useBookmarksStore = defineStore(
|
||||||
'bookmarks',
|
'bookmarks',
|
||||||
() => {
|
() => {
|
||||||
const currentCategory = ref<string>('all')
|
const currentCategory = ref<string>('all')
|
||||||
const currentFavorite = ref<boolean>(false)
|
const currentFavorite = ref<boolean>(false)
|
||||||
|
|
||||||
const getCategory = computed(() => currentCategory)
|
const getCategory = computed(() => currentCategory)
|
||||||
function setCategory(newCategory: string) {
|
function setCategory(newCategory: string) {
|
||||||
currentCategory.value = newCategory
|
currentCategory.value = newCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFavorite = computed(() => currentFavorite)
|
const isFavorite = computed(() => currentFavorite)
|
||||||
function toggleFavorite() {
|
function toggleFavorite() {
|
||||||
currentFavorite.value = !currentFavorite.value
|
currentFavorite.value = !currentFavorite.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCategory,
|
getCategory,
|
||||||
setCategory,
|
setCategory,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: true,
|
persist: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,27 +2,27 @@ import {defineStore} from 'pinia'
|
|||||||
import {ColorsTheme} from '~~/types'
|
import {ColorsTheme} from '~~/types'
|
||||||
|
|
||||||
export const useColorStore = defineStore(
|
export const useColorStore = defineStore(
|
||||||
'color',
|
'color',
|
||||||
() => {
|
() => {
|
||||||
const colorCookie = useCookie('color', { path: '/', default: () => ColorsTheme.RED })
|
const colorCookie = useCookie('color', { path: '/', default: () => ColorsTheme.RED })
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
watch(colorCookie, (newColor) => {
|
watch(colorCookie, (newColor) => {
|
||||||
appConfig.ui.primary = newColor
|
appConfig.ui.primary = newColor
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
function setColor(color: string) {
|
function setColor(color: string) {
|
||||||
colorCookie.value = color as ColorsTheme
|
colorCookie.value = color as ColorsTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
const getColor = computed(() => colorCookie)
|
const getColor = computed(() => colorCookie)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getColor,
|
getColor,
|
||||||
setColor,
|
setColor,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: true,
|
persist: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
|
|
||||||
export const useTalentsStore = defineStore(
|
export const useTalentsStore = defineStore(
|
||||||
'talents',
|
'talents',
|
||||||
() => {
|
() => {
|
||||||
const currentCategory = ref<string>('all')
|
const currentCategory = ref<string>('all')
|
||||||
const currentFavorite = ref<boolean>(false)
|
const currentFavorite = ref<boolean>(false)
|
||||||
|
|
||||||
const getCategory = computed(() => currentCategory)
|
const getCategory = computed(() => currentCategory)
|
||||||
function setCategory(newCategory: string) {
|
function setCategory(newCategory: string) {
|
||||||
currentCategory.value = newCategory
|
currentCategory.value = newCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFavorite = computed(() => currentFavorite)
|
const isFavorite = computed(() => currentFavorite)
|
||||||
function toggleFavorite() {
|
function toggleFavorite() {
|
||||||
currentFavorite.value = !currentFavorite.value
|
currentFavorite.value = !currentFavorite.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getCategory,
|
getCategory,
|
||||||
setCategory,
|
setCategory,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: true,
|
persist: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type {Config} from 'tailwindcss'
|
|||||||
import {ColorsTheme} from './types'
|
import {ColorsTheme} from './types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
'content/**/*.md',
|
'content/**/*.md',
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [
|
||||||
...Object.values(ColorsTheme).map(color => `prose-${color}`),
|
...Object.values(ColorsTheme).map(color => `prose-${color}`),
|
||||||
...Object.values(ColorsTheme).map(color => `border-${color}-500`),
|
...Object.values(ColorsTheme).map(color => `border-${color}-500`),
|
||||||
...Object.values(ColorsTheme).map(color => `hover:border-${color}-500`),
|
...Object.values(ColorsTheme).map(color => `hover:border-${color}-500`),
|
||||||
...Object.values(ColorsTheme).map(color => `dark:hover:border-${color}-500`),
|
...Object.values(ColorsTheme).map(color => `dark:hover:border-${color}-500`),
|
||||||
],
|
],
|
||||||
} satisfies Partial<Config>
|
} satisfies Partial<Config>
|
||||||
|
|||||||
278
types.ts
278
types.ts
@@ -1,191 +1,191 @@
|
|||||||
import type {MarkdownParsedContent, ParsedContent} from '@nuxt/content/dist/runtime/types'
|
import type {MarkdownParsedContent, ParsedContent} from '@nuxt/content/dist/runtime/types'
|
||||||
|
|
||||||
export enum ColorsTheme {
|
export enum ColorsTheme {
|
||||||
RED = 'red',
|
RED = 'red',
|
||||||
ORANGE = 'orange',
|
ORANGE = 'orange',
|
||||||
AMBER = 'amber',
|
AMBER = 'amber',
|
||||||
YELLOW = 'yellow',
|
YELLOW = 'yellow',
|
||||||
LIME = 'lime',
|
LIME = 'lime',
|
||||||
GREEN = 'green',
|
GREEN = 'green',
|
||||||
EMERALD = 'emerald',
|
EMERALD = 'emerald',
|
||||||
TEAL = 'teal',
|
TEAL = 'teal',
|
||||||
CYAN = 'cyan',
|
CYAN = 'cyan',
|
||||||
SKY = 'sky',
|
SKY = 'sky',
|
||||||
BLUE = 'blue',
|
BLUE = 'blue',
|
||||||
INDIGO = 'indigo',
|
INDIGO = 'indigo',
|
||||||
VIOLET = 'violet',
|
VIOLET = 'violet',
|
||||||
PURPLE = 'purple',
|
PURPLE = 'purple',
|
||||||
FUCHSIA = 'fuchsia',
|
FUCHSIA = 'fuchsia',
|
||||||
PINK = 'pink',
|
PINK = 'pink',
|
||||||
ROSE = 'rose',
|
ROSE = 'rose',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WakatimeData {
|
interface WakatimeData {
|
||||||
name: string
|
name: string
|
||||||
percent: number
|
percent: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Stats {
|
export interface Stats {
|
||||||
coding: {
|
coding: {
|
||||||
data: {
|
data: {
|
||||||
grand_total: {
|
grand_total: {
|
||||||
total_seconds_including_other_language: number
|
total_seconds_including_other_language: number
|
||||||
}
|
}
|
||||||
range: {
|
range: {
|
||||||
start: string
|
start: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editors: {
|
editors: {
|
||||||
data: Array<WakatimeData>
|
data: Array<WakatimeData>
|
||||||
}
|
}
|
||||||
os: {
|
os: {
|
||||||
data: Array<WakatimeData>
|
data: Array<WakatimeData>
|
||||||
}
|
}
|
||||||
languages: {
|
languages: {
|
||||||
data: Array<WakatimeData>
|
data: Array<WakatimeData>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LanyardActivity {
|
interface LanyardActivity {
|
||||||
name: string
|
name: string
|
||||||
state: string
|
state: string
|
||||||
details: string
|
details: string
|
||||||
timestamps: {
|
timestamps: {
|
||||||
start: number
|
start: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Activity {
|
export interface Activity {
|
||||||
data: {
|
data: {
|
||||||
activities: Array<LanyardActivity>
|
activities: Array<LanyardActivity>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Post extends MarkdownParsedContent {
|
export interface Post extends MarkdownParsedContent {
|
||||||
slug: string
|
slug: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
readingMins: number
|
readingMins: number
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
cover?: string
|
cover?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Project extends MarkdownParsedContent {
|
export interface Project extends MarkdownParsedContent {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
latest: boolean
|
latest: boolean
|
||||||
link: string
|
link: string
|
||||||
icon: string
|
icon: string
|
||||||
skills: Skill[]
|
skills: Skill[]
|
||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkExperience extends MarkdownParsedContent {
|
export interface WorkExperience extends MarkdownParsedContent {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
company: string
|
company: string
|
||||||
location: string
|
location: string
|
||||||
companyLink: string
|
companyLink: string
|
||||||
startDate: string
|
startDate: string
|
||||||
endDate: string | 'Today'
|
endDate: string | 'Today'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Education extends MarkdownParsedContent {
|
export interface Education extends MarkdownParsedContent {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
location: string
|
location: string
|
||||||
startDate: string
|
startDate: string
|
||||||
endDate: string | 'Today'
|
endDate: string | 'Today'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Skill extends ParsedContent {
|
export interface Skill extends ParsedContent {
|
||||||
name: string
|
name: string
|
||||||
icon: string & {
|
icon: string & {
|
||||||
dark: string
|
dark: string
|
||||||
light: string
|
light: string
|
||||||
}
|
}
|
||||||
color: string
|
color: string
|
||||||
link: string
|
link: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providers = [
|
export const providers = [
|
||||||
{
|
{
|
||||||
slug: 'github',
|
slug: 'github',
|
||||||
label: 'Login with Github',
|
label: 'Login with Github',
|
||||||
icon: 'i-ph-github-logo-bold',
|
icon: 'i-ph-github-logo-bold',
|
||||||
link: '/auth/github',
|
link: '/auth/github',
|
||||||
},
|
},
|
||||||
/* {
|
/* {
|
||||||
slug: 'twitter',
|
slug: 'twitter',
|
||||||
label: 'Login with Twitter',
|
label: 'Login with Twitter',
|
||||||
icon: 'i-ph-twitter-logo-bold',
|
icon: 'i-ph-twitter-logo-bold',
|
||||||
link: '/auth/twitter',
|
link: '/auth/twitter',
|
||||||
}, */
|
}, */
|
||||||
{
|
{
|
||||||
slug: 'google',
|
slug: 'google',
|
||||||
label: 'Login with Google',
|
label: 'Login with Google',
|
||||||
icon: 'i-ph-google-logo-bold',
|
icon: 'i-ph-google-logo-bold',
|
||||||
link: '/auth/google',
|
link: '/auth/google',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const otherTab = [
|
export const otherTab = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: 'Talents',
|
label: 'Talents',
|
||||||
to: '/talents',
|
to: '/talents',
|
||||||
icon: 'i-ph-users-bold',
|
icon: 'i-ph-users-bold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Guestbook',
|
label: 'Guestbook',
|
||||||
to: '/guestbook',
|
to: '/guestbook',
|
||||||
icon: 'i-material-symbols-book-2-outline',
|
icon: 'i-material-symbols-book-2-outline',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Bookmarks',
|
label: 'Bookmarks',
|
||||||
to: '/bookmarks',
|
to: '/bookmarks',
|
||||||
icon: 'i-material-symbols-bookmark-add-outline-rounded',
|
icon: 'i-material-symbols-bookmark-add-outline-rounded',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
export const navs = [
|
export const navs = [
|
||||||
{
|
{
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
to: '/',
|
to: '/',
|
||||||
icon: 'i-ph-house-bold',
|
icon: 'i-ph-house-bold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'About',
|
label: 'About',
|
||||||
to: '/about',
|
to: '/about',
|
||||||
icon: 'i-ph-person-arms-spread-bold',
|
icon: 'i-ph-person-arms-spread-bold',
|
||||||
},
|
},
|
||||||
/* {
|
/* {
|
||||||
label: 'Articles',
|
label: 'Articles',
|
||||||
to: '/writing',
|
to: '/writing',
|
||||||
icon: 'i-ph-pencil-bold',
|
icon: 'i-ph-pencil-bold',
|
||||||
}, */
|
}, */
|
||||||
{
|
{
|
||||||
label: 'Projects',
|
label: 'Projects',
|
||||||
to: '/work',
|
to: '/work',
|
||||||
icon: 'i-ph-flask-bold',
|
icon: 'i-ph-flask-bold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Uses',
|
label: 'Uses',
|
||||||
to: '/uses',
|
to: '/uses',
|
||||||
icon: 'i-ph-tree-evergreen-bold',
|
icon: 'i-ph-tree-evergreen-bold',
|
||||||
},
|
},
|
||||||
...otherTab,
|
...otherTab,
|
||||||
{
|
{
|
||||||
label: 'Contact',
|
label: 'Contact',
|
||||||
open: true,
|
open: true,
|
||||||
icon: 'i-ph-push-pin-bold',
|
icon: 'i-ph-push-pin-bold',
|
||||||
},
|
},
|
||||||
].flat()
|
].flat()
|
||||||
|
|
||||||
export const IDEs = [
|
export const IDEs = [
|
||||||
{ name: 'Visual Studio Code', icon: 'i-skill-icons-vscode-light' },
|
{ name: 'Visual Studio Code', icon: 'i-skill-icons-vscode-light' },
|
||||||
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-skill-icons-idea-light' },
|
{ name: 'IntelliJ IDEA Ultimate', icon: 'i-skill-icons-idea-light' },
|
||||||
{ name: 'WebStorm', icon: 'i-skill-icons-webstorm-light' },
|
{ name: 'WebStorm', icon: 'i-skill-icons-webstorm-light' },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user