mirror of
https://github.com/ArthurDanjou/website.git
synced 2026-01-14 12:14:42 +01:00
Import drizzle replacing prisma
Signed-off-by: Arthur DANJOU <arthurdanjou@outlook.fr>
This commit is contained in:
61
.vscode/settings.json
vendored
61
.vscode/settings.json
vendored
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
// TailwindCSS
|
|
||||||
"files.associations": {
|
|
||||||
"*.css": "tailwindcss"
|
|
||||||
},
|
|
||||||
"editor.quickSuggestions": {
|
|
||||||
"strings": true
|
|
||||||
},
|
|
||||||
"tailwindCSS.experimental.configFile": "tailwind.config.ts",
|
|
||||||
"tailwindCSS.experimental.classRegex": [
|
|
||||||
["ui:\\s*{([^)]*)\\s*}", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
|
||||||
["/\\*ui\\*/\\s*{([^;]*)}", ":\\s*[\"'`]([^\"'`]*).*?[\"'`]"]
|
|
||||||
],
|
|
||||||
"tailwindCSS.classAttributes": [
|
|
||||||
"class",
|
|
||||||
"ui"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Enable the ESlint flat config support
|
|
||||||
"eslint.experimental.useFlatConfig": true,
|
|
||||||
|
|
||||||
// Disable the default formatter, use eslint instead
|
|
||||||
"prettier.enable": false,
|
|
||||||
"editor.formatOnSave": false,
|
|
||||||
|
|
||||||
// Auto fix
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit",
|
|
||||||
"source.organizeImports": "never"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
|
||||||
"eslint.rules.customizations": [
|
|
||||||
{ "rule": "style/*", "severity": "off" },
|
|
||||||
{ "rule": "*-indent", "severity": "off" },
|
|
||||||
{ "rule": "*-spacing", "severity": "off" },
|
|
||||||
{ "rule": "*-spaces", "severity": "off" },
|
|
||||||
{ "rule": "*-order", "severity": "off" },
|
|
||||||
{ "rule": "*-dangle", "severity": "off" },
|
|
||||||
{ "rule": "*-newline", "severity": "off" },
|
|
||||||
{ "rule": "*quotes", "severity": "off" },
|
|
||||||
{ "rule": "*semi", "severity": "off" }
|
|
||||||
],
|
|
||||||
|
|
||||||
// Enable eslint for all supported languages
|
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact",
|
|
||||||
"vue",
|
|
||||||
"html",
|
|
||||||
"markdown",
|
|
||||||
"json",
|
|
||||||
"jsonc",
|
|
||||||
"yaml"
|
|
||||||
],
|
|
||||||
|
|
||||||
// Grammarly
|
|
||||||
"grammarly.files.include": ["**/*.txt", "**/*.md"]
|
|
||||||
}
|
|
||||||
0
src/auth.d.ts → auth.d.ts
vendored
0
src/auth.d.ts → auth.d.ts
vendored
@@ -12,7 +12,7 @@ function getColor() {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<h1 class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10" v-html="announce.content" />
|
<h1 class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10" v-html="announce.content" />
|
||||||
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
|
<span class="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="getColor()" />
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="getColor()" />
|
||||||
<span class="relative inline-flex rounded-full h-2 w-2" :class="getColor()" />
|
<span class="relative inline-flex rounded-full h-2 w-2" :class="getColor()" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -7,7 +7,7 @@ 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: any
|
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])]
|
||||||
@@ -21,7 +21,10 @@ onUnmounted(() => clearTimeout(timeout))
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden" aria-hidden="true">
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
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})` }"
|
||||||
@@ -13,11 +13,25 @@ const year = computed(() => new Date().getFullYear())
|
|||||||
<p class="text-subtitle">
|
<p class="text-subtitle">
|
||||||
Designed & Built by
|
Designed & Built by
|
||||||
</p>
|
</p>
|
||||||
<UButton variant="link" color="primary" label="Arthur Danjou" to="https://twitter.com/arthurdanj" target="_blank" />
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
label="Arthur Danjou"
|
||||||
|
target="_blank"
|
||||||
|
to="https://twitter.com/arthurdanj"
|
||||||
|
variant="link"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-subtitle flex items-center">
|
<p class="text-subtitle flex items-center">
|
||||||
Made with
|
Made with
|
||||||
<UButton variant="link" color="green" label="Nuxt 3" to="https://nuxt.com/" target="_blank" icon="i-vscode-icons-file-type-nuxt" trailing />
|
<UButton
|
||||||
|
color="green"
|
||||||
|
icon="i-vscode-icons-file-type-nuxt"
|
||||||
|
label="Nuxt 3"
|
||||||
|
target="_blank"
|
||||||
|
to="https://nuxt.com/"
|
||||||
|
trailing
|
||||||
|
variant="link"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
defineProps({
|
defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Uses Section title',
|
default: 'Uses Section title'
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
@@ -14,7 +14,10 @@ const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
|||||||
<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 class="relative text-sm font-semibold pl-3.5" :class="getColor">
|
<h2
|
||||||
|
:class="getColor"
|
||||||
|
class="relative text-sm font-semibold pl-3.5"
|
||||||
|
>
|
||||||
<span class="md:hidden absolute inset-y-0 left-0 flex items-center">
|
<span class="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>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
defineProps({
|
defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Uses Slot title',
|
default: 'Uses Slot title'
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
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()
|
||||||
28
components/header/Logo.vue
Normal file
28
components/header/Logo.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||||
|
|
||||||
|
function getGroupColor() {
|
||||||
|
return `group-hover:text-${appConfig.ui.primary}-500`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="getTextColor"
|
||||||
|
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
||||||
|
>Arthur</span>
|
||||||
|
<span
|
||||||
|
:class="getGroupColor()"
|
||||||
|
class="font-bold text-gray-300 dark:text-neutral-600 duration-300"
|
||||||
|
>/</span>
|
||||||
|
<span
|
||||||
|
:class="getTextColor"
|
||||||
|
class="font-black group-hover:text-black dark:group-hover:text-white duration-300"
|
||||||
|
>Danjou</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
@@ -33,7 +33,10 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<USlideover v-model="isOpenSidebar">
|
<USlideover v-model="isOpenSidebar">
|
||||||
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
<UCard
|
||||||
|
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<Logo />
|
<Logo />
|
||||||
@@ -47,7 +50,11 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<div v-for="nav in navs" :key="nav.label" class="w-full">
|
<div
|
||||||
|
v-for="nav in navs"
|
||||||
|
:key="nav.label"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="nav.to"
|
v-if="nav.to"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -83,7 +90,12 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
<h1 class="text-xl font-bold">
|
<h1 class="text-xl font-bold">
|
||||||
Contact me
|
Contact me
|
||||||
</h1>
|
</h1>
|
||||||
<UButton size="xs" icon="i-akar-icons-cross" variant="ghost" @click.prevent="isOpenModal = false" />
|
<UButton
|
||||||
|
icon="i-akar-icons-cross"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
@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">
|
||||||
@@ -96,10 +108,32 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup size="sm" orientation="horizontal">
|
<UButtonGroup
|
||||||
<UButton variant="solid" color="gray" label="Compose" to="mailto:arthurdanjou@outlook.fr" icon="i-mdi-note-edit-outline" />
|
orientation="horizontal"
|
||||||
<UButton v-if="copied" variant="solid" color="green" label="Copied" icon="i-mdi-content-copy" />
|
size="sm"
|
||||||
<UButton v-else variant="solid" color="gray" label="Copy" icon="i-mdi-content-copy" @click.prevent="copy()" />
|
>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
icon="i-mdi-note-edit-outline"
|
||||||
|
label="Compose"
|
||||||
|
to="mailto:arthurdanjou@outlook.fr"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="copied"
|
||||||
|
color="green"
|
||||||
|
icon="i-mdi-content-copy"
|
||||||
|
label="Copied"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-else
|
||||||
|
color="gray"
|
||||||
|
icon="i-mdi-content-copy"
|
||||||
|
label="Copy"
|
||||||
|
variant="solid"
|
||||||
|
@click.prevent="copy()"
|
||||||
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,14 +148,25 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UButtonGroup size="sm" orientation="horizontal">
|
<UButtonGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
variant="solid" color="gray" label="Github" icon="i-ph-github-logo-bold"
|
color="gray"
|
||||||
to="https://github.com/ArthurDanjou" target="_blank"
|
icon="i-ph-github-logo-bold"
|
||||||
|
label="Github"
|
||||||
|
target="_blank"
|
||||||
|
to="https://github.com/ArthurDanjou"
|
||||||
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
variant="solid" color="gray" label="Twitter" icon="i-ph-twitter-logo-bold"
|
color="gray"
|
||||||
to="https://twitter.com/ArthurDanj" target="_blank"
|
icon="i-ph-twitter-logo-bold"
|
||||||
|
label="Twitter"
|
||||||
|
target="_blank"
|
||||||
|
to="https://twitter.com/ArthurDanj"
|
||||||
|
variant="solid"
|
||||||
/>
|
/>
|
||||||
</UButtonGroup>
|
</UButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
174
components/header/NavBar.vue
Normal file
174
components/header/NavBar.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { otherTab } from '~~/types'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const isOpenModal = ref(false)
|
||||||
|
|
||||||
|
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="hidden lg:block z-50">
|
||||||
|
<div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
||||||
|
<UButton
|
||||||
|
:class="{ 'link-active': route.path === '/' }"
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
to="/"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
:class="{ 'link-active': route.path.includes('/about') }"
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
to="/about"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</UButton>
|
||||||
|
<!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }">
|
||||||
|
Articles
|
||||||
|
</UButton> -->
|
||||||
|
<UButton
|
||||||
|
:class="{ 'link-active': route.path.includes('/work') }"
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
to="/work"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
:class="{ 'link-active': route.path.includes('/uses') }"
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
to="/uses"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Uses
|
||||||
|
</UButton>
|
||||||
|
<UDropdown
|
||||||
|
:items="otherTab"
|
||||||
|
:popper="{ placement: 'bottom' }"
|
||||||
|
mode="hover"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
class="duration-300"
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Other
|
||||||
|
</UButton>
|
||||||
|
</UDropdown>
|
||||||
|
<UButton
|
||||||
|
color="white"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="isOpenModal = true"
|
||||||
|
>
|
||||||
|
Contact
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<UModal v-model="isOpenModal">
|
||||||
|
<UCard class="p-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-8 flex justify-between items-center">
|
||||||
|
<h1 class="text-xl font-bold">
|
||||||
|
Contact me
|
||||||
|
</h1>
|
||||||
|
<UButton
|
||||||
|
icon="i-akar-icons-cross"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
@click.prevent="isOpenModal = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-6">
|
||||||
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3 class="text-sm">
|
||||||
|
Email
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-subtitle">
|
||||||
|
arthurdanjou@outlook.fr
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UButtonGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
icon="i-mdi-note-edit-outline"
|
||||||
|
label="Compose"
|
||||||
|
to="mailto:arthurdanjou@outlook.fr"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="copied"
|
||||||
|
color="green"
|
||||||
|
icon="i-mdi-content-copy"
|
||||||
|
label="Copied"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-else
|
||||||
|
color="gray"
|
||||||
|
icon="i-mdi-content-copy"
|
||||||
|
label="Copy"
|
||||||
|
variant="solid"
|
||||||
|
@click.prevent="copy()"
|
||||||
|
/>
|
||||||
|
</UButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UDivider label="OR" />
|
||||||
|
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3 class="text-sm">
|
||||||
|
Get in touch
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-subtitle">
|
||||||
|
I'm most active on Twitter
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UButtonGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
icon="i-ph-github-logo-bold"
|
||||||
|
label="Github"
|
||||||
|
target="_blank"
|
||||||
|
to="https://github.com/ArthurDanjou"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
icon="i-ph-twitter-logo-bold"
|
||||||
|
label="Twitter"
|
||||||
|
target="_blank"
|
||||||
|
to="https://twitter.com/ArthurDanj"
|
||||||
|
variant="solid"
|
||||||
|
/>
|
||||||
|
</UButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.link-active {
|
||||||
|
@apply bg-white/60 dark:bg-black
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -19,7 +19,7 @@ watch(isDark, () => {
|
|||||||
: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 }">
|
||||||
@@ -31,14 +31,23 @@ watch(isDark, () => {
|
|||||||
: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 name="i-ph-paint-brush-bold" class="w-5 h-5 text-primary-500 dark:text-primary-400" />
|
<UIcon
|
||||||
|
class="w-5 h-5 text-primary-500 dark:text-primary-400"
|
||||||
|
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 v-for="color in colors" :key="color" :text="color" class="capitalize" :open-delay="500">
|
<UTooltip
|
||||||
|
v-for="color in colors"
|
||||||
|
:key="color"
|
||||||
|
:open-delay="500"
|
||||||
|
:text="color"
|
||||||
|
class="capitalize"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
color="gray"
|
color="gray"
|
||||||
square
|
square
|
||||||
@@ -46,15 +55,21 @@ watch(isDark, () => {
|
|||||||
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 class="flex items-center justify-center w-3 h-3 rounded-full border text-white" :class="`bg-${color}-500/80 border-${color}-500`">
|
<span
|
||||||
<UIcon v-if="color === getColor" name="i-ic-round-check" />
|
:class="`bg-${color}-500/80 border-${color}-500`"
|
||||||
|
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
v-if="color === getColor"
|
||||||
|
name="i-ic-round-check"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</UButton>
|
</UButton>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Activity } from '~~/types'
|
import {type Activity, IDEs} from '~~/types'
|
||||||
|
|
||||||
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'))
|
const { data: activity, refresh } = await useAsyncData<Activity>('activity', () => $fetch('/api/activity'))
|
||||||
const codingActivity = computed(() => activity.value!.data.activities.filter(activity => activity.name === 'Visual Studio Code')[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}`
|
||||||
@@ -10,15 +10,21 @@ function formatDate(date: number) {
|
|||||||
|
|
||||||
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 class="flex flex-col justify-between" :ui="CardUi">
|
<UCard
|
||||||
<div v-if="activity && activity.data.activities" class="flex items-center gap-x-4">
|
:ui="CardUi"
|
||||||
|
class="flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="activity && activity.data.activities"
|
||||||
|
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' }"
|
||||||
@@ -29,7 +35,7 @@ useIntervalFn(async () => await refresh(), 5000)
|
|||||||
<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="i-skill-icons-vscode-light"
|
: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">
|
||||||
@@ -45,12 +51,15 @@ useIntervalFn(async () => await refresh(), 5000)
|
|||||||
I'm Idling on my computer
|
I'm Idling on my computer
|
||||||
</h3>
|
</h3>
|
||||||
<h3 v-else>
|
<h3 v-else>
|
||||||
{{ codingActivity.details }}
|
{{ codingActivity.details }} - {{ codingActivity.state }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-subtitle">
|
<div
|
||||||
|
v-else
|
||||||
|
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 🫥">
|
||||||
@@ -63,7 +72,10 @@ useIntervalFn(async () => await refresh(), 5000)
|
|||||||
<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 v-if="codingActivity" class="text-subtitle text-xs w-1/2">
|
<p
|
||||||
|
v-if="codingActivity"
|
||||||
|
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>
|
||||||
@@ -79,7 +91,10 @@ useIntervalFn(async () => await refresh(), 5000)
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
label="Lanyard"
|
label="Lanyard"
|
||||||
/>
|
/>
|
||||||
<UIcon name="i-jam-thunder" class="text-subtitle" />
|
<UIcon
|
||||||
|
class="text-subtitle"
|
||||||
|
name="i-jam-thunder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,23 +3,23 @@ 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>
|
||||||
|
|
||||||
@@ -5,12 +5,15 @@ 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 class="flex flex-col justify-between" :ui="CardUi">
|
<UCard
|
||||||
|
:ui="CardUi"
|
||||||
|
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"
|
||||||
@@ -23,7 +26,9 @@ const CardUi = {
|
|||||||
<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">{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours</p>
|
<p class="text-subtitle">
|
||||||
|
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
|
||||||
|
</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>
|
||||||
@@ -33,11 +38,15 @@ const CardUi = {
|
|||||||
</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">{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%</p>
|
<p class="text-subtitle">
|
||||||
|
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
|
||||||
|
</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">{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}</p>
|
<p class="text-subtitle">
|
||||||
|
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +55,10 @@ const CardUi = {
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<p v-if="stats" class="text-subtitle text-xs w-1/2">
|
<p
|
||||||
|
v-if="stats"
|
||||||
|
class="text-subtitle text-xs w-1/2"
|
||||||
|
>
|
||||||
Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }}
|
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>
|
||||||
@@ -62,7 +74,10 @@ const CardUi = {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
label="Wakatime"
|
label="Wakatime"
|
||||||
/>
|
/>
|
||||||
<UIcon name="i-jam-thunder" class="text-subtitle" />
|
<UIcon
|
||||||
|
class="text-subtitle"
|
||||||
|
name="i-jam-thunder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,8 +3,8 @@ defineProps({
|
|||||||
startDate: String,
|
startDate: String,
|
||||||
endDate: {
|
endDate: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatTodayDate(date: string) {
|
function formatTodayDate(date: string) {
|
||||||
@@ -14,10 +14,18 @@ function formatTodayDate(date: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UBadge v-if="startDate !== endDate" variant="soft" size="xs">
|
<UBadge
|
||||||
|
v-if="startDate !== endDate"
|
||||||
|
size="xs"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
{{ formatTodayDate(startDate!.toString()) }} — {{ formatTodayDate(endDate) }}
|
{{ formatTodayDate(startDate!.toString()) }} — {{ formatTodayDate(endDate) }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
<UBadge v-else variant="soft" size="xs">
|
<UBadge
|
||||||
|
v-else
|
||||||
|
size="xs"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
{{ formatTodayDate(endDate) }}
|
{{ formatTodayDate(endDate) }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,15 +2,21 @@
|
|||||||
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 v-if="education" class="group relative flex flex-col items-start">
|
<div
|
||||||
|
v-if="education"
|
||||||
|
class="group relative flex flex-col items-start"
|
||||||
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<DateTag :start-date="education.startDate" :end-date="education.endDate" />
|
<DateTag
|
||||||
|
:end-date="education.endDate"
|
||||||
|
: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 }}
|
||||||
@@ -2,16 +2,22 @@
|
|||||||
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 v-if="experience" class="group relative flex flex-col items-start">
|
<div
|
||||||
|
v-if="experience"
|
||||||
|
class="group relative flex flex-col items-start"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<DateTag :start-date="experience.startDate" :end-date="experience.endDate" />
|
<DateTag
|
||||||
|
:end-date="experience.endDate"
|
||||||
|
:start-date="experience.startDate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center my-1">
|
<div class="flex items-center my-1">
|
||||||
<UButton
|
<UButton
|
||||||
@@ -26,10 +32,16 @@ defineProps({
|
|||||||
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 name="i-akar-icons-link-chain" color="gray" />
|
<UIcon
|
||||||
|
color="gray"
|
||||||
|
name="i-akar-icons-link-chain"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UButton>
|
</UButton>
|
||||||
<h1 v-else class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
<h1
|
||||||
|
v-else
|
||||||
|
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">
|
||||||
@@ -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()
|
||||||
@@ -15,8 +15,18 @@ const isLight = computed(() => $colorMode.value === 'light')
|
|||||||
class="flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
|
class="flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<UIcon v-if="isLight" :name="skill.icon.light ? skill.icon.light : skill.icon" size="20" dynamic />
|
<UIcon
|
||||||
<UIcon v-else :name="skill.icon.dark ? skill.icon.dark : skill.icon" size="20" dynamic />
|
v-if="isLight"
|
||||||
|
:name="skill.icon.light ? skill.icon.light : skill.icon"
|
||||||
|
dynamic
|
||||||
|
size="20"
|
||||||
|
/>
|
||||||
|
<UIcon
|
||||||
|
v-else
|
||||||
|
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
|
||||||
|
dynamic
|
||||||
|
size="20"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-subtitle">{{ skill.name }}</span>
|
<span class="text-sm text-subtitle">{{ skill.name }}</span>
|
||||||
</li>
|
</li>
|
||||||
12
drizzle.config.ts
Normal file
12
drizzle.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
driver: 'pg',
|
||||||
|
schema: './server/database/schema.ts',
|
||||||
|
out: './server/database/migrations',
|
||||||
|
dbCredentials: {
|
||||||
|
connectionString: process.env.DATABASE_URL as string
|
||||||
|
},
|
||||||
|
strict: true,
|
||||||
|
verbose: true
|
||||||
|
})
|
||||||
@@ -5,6 +5,10 @@ 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({
|
||||||
|
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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: any) => {
|
await $fetch('/api/maintenance').then((maintenance) => {
|
||||||
isMaintenance.value = maintenance.enabled
|
isMaintenance.value = maintenance.enabled
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -11,14 +11,14 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|||||||
|
|
||||||
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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
20
modules/drizzle-studio.ts
Normal file
20
modules/drizzle-studio.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineNuxtModule } from 'nuxt/kit'
|
||||||
|
import { addCustomTab } from '@nuxt/devtools-kit'
|
||||||
|
|
||||||
|
export default defineNuxtModule({
|
||||||
|
meta: {
|
||||||
|
name: 'drizzle-studio',
|
||||||
|
version: '0.0.1'
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
addCustomTab({
|
||||||
|
name: 'drizzle-studio',
|
||||||
|
title: 'Drizzle Studio',
|
||||||
|
icon: 'simple-icons:drizzle',
|
||||||
|
view: {
|
||||||
|
type: 'iframe',
|
||||||
|
src: 'https://local.drizzle.studio/?themeId=azX2nOTScT9U6SWEmlq7z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/* eslint-disable node/prefer-global/process */
|
/* eslint-disable node/prefer-global/process */
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
srcDir: 'src',
|
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
'@/assets/css/main.scss',
|
'@/assets/css/main.scss',
|
||||||
],
|
],
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -8,7 +8,7 @@
|
|||||||
"dev": "nuxt dev --host",
|
"dev": "nuxt dev --host",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "prisma generate && nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix"
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
@@ -16,30 +16,29 @@
|
|||||||
"@nuxt/content": "2.12.1",
|
"@nuxt/content": "2.12.1",
|
||||||
"@nuxt/ui": "2.14.2",
|
"@nuxt/ui": "2.14.2",
|
||||||
"@pinia/nuxt": "0.5.1",
|
"@pinia/nuxt": "0.5.1",
|
||||||
"@prisma/client": "5.11.0",
|
|
||||||
"@vercel/analytics": "1.2.2",
|
"@vercel/analytics": "1.2.2",
|
||||||
"@vercel/speed-insights": "1.0.10",
|
"@vercel/speed-insights": "1.0.10",
|
||||||
|
"drizzle-kit": "0.20.14",
|
||||||
|
"drizzle-orm": "0.30.8",
|
||||||
"nuxt": "3.10.3",
|
"nuxt": "3.10.3",
|
||||||
"nuxt-auth-utils": "0.0.20",
|
"nuxt-auth-utils": "0.0.20",
|
||||||
"pinia": "2.1.7",
|
"pinia": "2.1.7",
|
||||||
"postcss-custom-properties": "13.3.5",
|
"postcss-custom-properties": "13.3.7",
|
||||||
"prisma": "5.11.0",
|
"postgres": "3.4.4",
|
||||||
"sass": "1.71.1",
|
"sass": "1.75.0",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.3",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "2.8.1",
|
"@antfu/eslint-config": "2.15.0",
|
||||||
"@iconify/json": "2.2.191",
|
"@iconify/json": "2.2.202",
|
||||||
"@nuxt/eslint-config": "^0.2.0",
|
|
||||||
"@nuxthq/studio": "1.0.13",
|
"@nuxthq/studio": "1.0.13",
|
||||||
"@nuxtjs/seo": "2.0.0-rc.9",
|
"@nuxtjs/seo": "2.0.0-rc.10",
|
||||||
"@pinia-plugin-persistedstate/nuxt": "1.2.0",
|
"@pinia-plugin-persistedstate/nuxt": "1.2.0",
|
||||||
"@tailwindcss/typography": "0.5.10",
|
|
||||||
"@types/node": "20.11.26",
|
"@types/node": "20.11.26",
|
||||||
"@vueuse/core": "10.9.0",
|
"@vueuse/core": "10.9.0",
|
||||||
"@vueuse/nuxt": "10.9.0",
|
"@vueuse/nuxt": "10.9.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "9.1.0",
|
||||||
"typescript": "5.4.2"
|
"typescript": "5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -13,8 +13,15 @@ const { data: experiences } = await getWorkExperiences()
|
|||||||
<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 text="It's me 👋" :popper="{ offsetDistance: 20 }">
|
<UTooltip
|
||||||
<img src="/about.png" 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">
|
:popper="{ offsetDistance: 20 }"
|
||||||
|
text="It's me 👋"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="My main profile picture"
|
||||||
|
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||||
|
src="/about.png"
|
||||||
|
>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +56,10 @@ const { data: experiences } = await getWorkExperiences()
|
|||||||
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 v-if="skills" title="Skills">
|
<GridSection
|
||||||
|
v-if="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"
|
||||||
@@ -58,14 +68,31 @@ const { data: experiences } = await getWorkExperiences()
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<GridSection v-if="experiences" title="Work Experiences">
|
<GridSection
|
||||||
<Experience v-for="experience in experiences" :key="experience.title" :experience="experience" />
|
v-if="experiences"
|
||||||
|
title="Work Experiences"
|
||||||
|
>
|
||||||
|
<Experience
|
||||||
|
v-for="experience in experiences"
|
||||||
|
:key="experience.title"
|
||||||
|
:experience="experience"
|
||||||
|
/>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<GridSection v-if="educations" title="Educations">
|
<GridSection
|
||||||
<Education v-for="education in educations" :key="education.title" :education="education" />
|
v-if="educations"
|
||||||
|
title="Educations"
|
||||||
|
>
|
||||||
|
<Education
|
||||||
|
v-for="education in educations"
|
||||||
|
:key="education.title"
|
||||||
|
:education="education"
|
||||||
|
/>
|
||||||
</GridSection>
|
</GridSection>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<UTooltip text="Click to discover my journey" :popper="{ offsetDistance: 20 }">
|
<UTooltip
|
||||||
|
:popper="{ offsetDistance: 20 }"
|
||||||
|
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"
|
||||||
@@ -1,30 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Bookmark, Category } from '@prisma/client'
|
|
||||||
import { useBookmarksStore } from '~/store/bookmarks'
|
import { useBookmarksStore } from '~/store/bookmarks'
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Discover new talents • 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 {
|
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
|
||||||
data: bookmarks,
|
|
||||||
pending,
|
|
||||||
} = await useFetch<Array<Bookmark & { categories: Array<{ category: Category }> }>>('/api/bookmarks', {
|
|
||||||
method: 'get',
|
method: 'get',
|
||||||
query: {
|
query: {
|
||||||
favorite: isFavorite,
|
favorite: isFavorite,
|
||||||
category: getCategory,
|
category: getCategory
|
||||||
},
|
},
|
||||||
watch: [isFavorite, getCategory],
|
watch: [isFavorite, getCategory]
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } })
|
||||||
data: getCategories,
|
getCategories.value!.forEach(category => categories.value.push({label: category.name, slug: category.slug}))
|
||||||
} = await useFetch<Array<Category>>('/api/categories', { method: 'GET', query: { type: 'BOOKMARK' } })
|
|
||||||
getCategories.value!.forEach((category: any) => 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
|
||||||
@@ -33,10 +27,10 @@ function isCategory(slug: string) {
|
|||||||
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` || '0px',
|
top: `${selected?.offsetTop}px`,
|
||||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px` || '4px',
|
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||||
height: `${selected?.offsetHeight}px` || '0px',
|
height: `${selected?.offsetHeight}px`,
|
||||||
width: `${selected?.offsetWidth}px` || '0px',
|
width: `${selected?.offsetWidth}px`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,13 +44,16 @@ function getColor() {
|
|||||||
<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 librairy where I save some ressources
|
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 v-if="getCategories" class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between">
|
<div
|
||||||
|
v-if="getCategories"
|
||||||
|
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
|
||||||
|
>
|
||||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
<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
|
||||||
@@ -91,16 +88,28 @@ function getColor() {
|
|||||||
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 v-if="isFavorite" name="i-material-symbols-check-box-outline-rounded" />
|
<UIcon
|
||||||
<UIcon v-else name="i-material-symbols-check-box-outline-blank" />
|
v-if="isFavorite"
|
||||||
|
name="i-material-symbols-check-box-outline-rounded"
|
||||||
|
/>
|
||||||
|
<UIcon
|
||||||
|
v-else
|
||||||
|
name="i-material-symbols-check-box-outline-blank"
|
||||||
|
/>
|
||||||
<p>Show favorites only</p>
|
<p>Show favorites only</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UPopover>
|
||||||
</div>
|
</div>
|
||||||
<UDivider class="my-2" />
|
<UDivider class="my-2" />
|
||||||
<div v-if="bookmarks && getCategories" class="mt-8">
|
<div
|
||||||
<div v-if="bookmarks.length > 0 && !pending" class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3">
|
v-if="bookmarks && getCategories"
|
||||||
|
class="mt-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="bookmarks.length > 0 && !pending"
|
||||||
|
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="bookmark in bookmarks"
|
v-for="bookmark in bookmarks"
|
||||||
:key="bookmark.name.toLowerCase().trim()"
|
:key="bookmark.name.toLowerCase().trim()"
|
||||||
@@ -109,7 +118,7 @@ function getColor() {
|
|||||||
<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">
|
||||||
<div 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
|
||||||
@@ -120,8 +129,14 @@ function getColor() {
|
|||||||
<h1 class="relative z-10">
|
<h1 class="relative z-10">
|
||||||
{{ bookmark.name }}
|
{{ bookmark.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<UTooltip v-if="bookmark.favorite" text="You can set the filter to only show favorites.">
|
<UTooltip
|
||||||
<UIcon name="i-ic-round-star" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" />
|
v-if="bookmark.favorite"
|
||||||
|
text="You can set the filter to only show favorites."
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||||
|
name="i-ic-round-star"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -129,7 +144,7 @@ function getColor() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 z-10">
|
<div class="flex gap-2 z-10">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-for="category in bookmark.categories"
|
v-for="category in bookmark.bookmarkCategories"
|
||||||
:key="category.category.slug"
|
:key="category.category.slug"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@@ -139,19 +154,28 @@ function getColor() {
|
|||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="relative z-10 flex text-sm font-medium items-center" :class="getColor()">
|
<p
|
||||||
|
:class="getColor()"
|
||||||
|
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 v-else-if="bookmarks?.length === 0 && !pending" class="my-4 text-subtitle">
|
<div
|
||||||
|
v-else-if="bookmarks?.length === 0 && !pending"
|
||||||
|
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 bookmarks for this category. Maybe soon...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="my-4 text-subtitle">
|
<div
|
||||||
|
v-else
|
||||||
|
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>
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GuestbookMessage } from '@prisma/client'
|
|
||||||
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()
|
||||||
const { data: messages, refresh } = useFetch<Array<GuestbookMessage>>('/api/messages', { method: 'get' })
|
const { data: messages, refresh } = useFetch('/api/messages', { method: 'get' })
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
|
||||||
@@ -21,20 +20,20 @@ async function sign() {
|
|||||||
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: `Thank's 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 occured when signing the book!',
|
title: 'An error occurred when signing the book!',
|
||||||
color: 'red',
|
color: 'red'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
messageContent.value = ''
|
messageContent.value = ''
|
||||||
@@ -46,20 +45,20 @@ async function deleteMessage(id: number) {
|
|||||||
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'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -88,18 +87,33 @@ async function deleteMessage(id: number) {
|
|||||||
<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 v-if="loggedIn" class="text-md font-bold">
|
<h1
|
||||||
|
v-if="loggedIn"
|
||||||
|
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 v-else class="text-md font-bold">
|
<h1
|
||||||
|
v-else
|
||||||
|
class="text-md font-bold"
|
||||||
|
>
|
||||||
Sign before writing your message
|
Sign before writing your message
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
<UButton
|
||||||
|
class="-my-1"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
variant="ghost"
|
||||||
|
@click="isOpen = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="loggedIn" class="flex items-center justify-between gap-4">
|
<div
|
||||||
|
v-if="loggedIn"
|
||||||
|
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"
|
||||||
@@ -125,7 +139,10 @@ async function deleteMessage(id: number) {
|
|||||||
Logout
|
Logout
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex gap-2 justify-center">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex gap-2 justify-center"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.slug"
|
:key="provider.slug"
|
||||||
@@ -134,13 +151,16 @@ async function deleteMessage(id: number) {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
:to="provider.link"
|
:to="provider.link"
|
||||||
:icon="provider.icon"
|
:icon="provider.icon"
|
||||||
external
|
:external="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
<div v-if="messages" class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8">
|
<div
|
||||||
|
v-if="messages"
|
||||||
|
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"
|
||||||
@@ -151,7 +171,11 @@ async function deleteMessage(id: number) {
|
|||||||
</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 class="w-full h-full rounded-full" :src="message.image" alt="Nature">
|
<img
|
||||||
|
:src="message.image"
|
||||||
|
alt="Author profile picture"
|
||||||
|
class="w-full h-full rounded-full"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-bold">
|
<p class="font-bold">
|
||||||
{{ message.username }}
|
{{ message.username }}
|
||||||
@@ -169,7 +193,10 @@ async function deleteMessage(id: number) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="my-4 text-subtitle">
|
<div
|
||||||
|
v-else
|
||||||
|
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>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -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')
|
||||||
@@ -17,23 +17,23 @@ 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>
|
||||||
|
|
||||||
@@ -47,7 +47,10 @@ const socials = [
|
|||||||
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 :class="getColor" class="font-bold mb-8 text-xl">
|
<p
|
||||||
|
:class="getColor"
|
||||||
|
class="font-bold mb-8 text-xl"
|
||||||
|
>
|
||||||
{{ maintenance.maintenance.reason }}
|
{{ maintenance.maintenance.reason }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
@@ -64,7 +67,11 @@ const socials = [
|
|||||||
class="link"
|
class="link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="flex-shrink-0 h-5 w-5" aria-hidden="true" :class="social.icon" />
|
<span
|
||||||
|
:class="social.icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="flex-shrink-0 h-5 w-5"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,32 +1,30 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Category, Suggestion, Talent } from '@prisma/client'
|
|
||||||
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 }])
|
||||||
const { getCategory, setCategory, isFavorite, toggleFavorite } = useTalentsStore()
|
const { getCategory, setCategory, isFavorite, toggleFavorite } = useTalentsStore()
|
||||||
const { loggedIn, clear } = useUserSession()
|
const { loggedIn, clear } = useUserSession()
|
||||||
|
|
||||||
const {
|
const { data: talents, pending } = await useFetch('/api/talents', {
|
||||||
data: talents,
|
|
||||||
pending,
|
|
||||||
} = await useFetch<Array<Talent & { categories: Array<{ category: Category }> }>>('/api/talents', {
|
|
||||||
method: 'get',
|
method: 'get',
|
||||||
query: {
|
query: {
|
||||||
favorite: isFavorite,
|
favorite: isFavorite,
|
||||||
category: getCategory,
|
category: getCategory
|
||||||
},
|
},
|
||||||
watch: [isFavorite, getCategory],
|
watch: [isFavorite, getCategory]
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } })
|
||||||
data: getCategories,
|
getCategories.value!.forEach(category => categories.value.push({
|
||||||
} = await useFetch<Array<Category>>('/api/categories', { method: 'GET', query: { type: 'TALENT' } })
|
label: category.name,
|
||||||
getCategories.value!.forEach((category: any) => categories.value.push({ label: category.name, slug: category.slug, id: category.id }))
|
slug: category.slug,
|
||||||
|
id: category.id
|
||||||
|
}))
|
||||||
|
|
||||||
function isCategory(slug: string) {
|
function isCategory(slug: string) {
|
||||||
return getCategory.value === slug
|
return getCategory.value === slug
|
||||||
@@ -35,10 +33,10 @@ function isCategory(slug: string) {
|
|||||||
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` || '0px',
|
top: `${selected?.offsetTop}px`,
|
||||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px` || '4px',
|
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||||
height: `${selected?.offsetHeight}px` || '0px',
|
height: `${selected?.offsetHeight}px`,
|
||||||
width: `${selected?.offsetWidth}px` || '0px',
|
width: `${selected?.offsetWidth}px`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,21 +53,22 @@ async function suggest() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
await $fetch<Suggestion>('/api/suggestion', {
|
await $fetch('/api/suggestion', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: {
|
body: {
|
||||||
content: suggestContent.value,
|
content: suggestContent.value
|
||||||
},
|
}
|
||||||
}).then((suggestion) => {
|
}).then((response) => {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: `Your suggestion for '${suggestion.content}'' has been successfully added`,
|
title: `Your suggestion for '${response[0].content}' has been successfully added`,
|
||||||
|
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: 'You already have suggested someone',
|
title: 'An error occurred when suggesting someone',
|
||||||
color: 'red',
|
color: 'red'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
suggestContent.value = ''
|
suggestContent.value = ''
|
||||||
@@ -103,11 +102,20 @@ async function suggest() {
|
|||||||
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 color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
|
<UButton
|
||||||
|
class="-my-1"
|
||||||
|
color="gray"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
variant="ghost"
|
||||||
|
@click="isOpen = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="loggedIn" class="flex items-center justify-between gap-4">
|
<div
|
||||||
|
v-if="loggedIn"
|
||||||
|
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"
|
||||||
@@ -132,7 +140,10 @@ async function suggest() {
|
|||||||
Logout
|
Logout
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex gap-2 justify-center">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex gap-2 justify-center"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.slug"
|
:key="provider.slug"
|
||||||
@@ -141,14 +152,17 @@ async function suggest() {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
:to="provider.link"
|
:to="provider.link"
|
||||||
:icon="provider.icon"
|
:icon="provider.icon"
|
||||||
external
|
: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 v-if="getCategories" class="flex gap-2 w-full items-center justify-between">
|
<div
|
||||||
|
v-if="getCategories"
|
||||||
|
class="flex gap-2 w-full items-center justify-between"
|
||||||
|
>
|
||||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
<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
|
||||||
@@ -183,8 +197,14 @@ async function suggest() {
|
|||||||
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 v-if="isFavorite" name="i-material-symbols-check-box-outline-rounded" />
|
<UIcon
|
||||||
<UIcon v-else name="i-material-symbols-check-box-outline-blank" />
|
v-if="isFavorite"
|
||||||
|
name="i-material-symbols-check-box-outline-rounded"
|
||||||
|
/>
|
||||||
|
<UIcon
|
||||||
|
v-else
|
||||||
|
name="i-material-symbols-check-box-outline-blank"
|
||||||
|
/>
|
||||||
<p>Show favorites only</p>
|
<p>Show favorites only</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -192,8 +212,14 @@ async function suggest() {
|
|||||||
</div>
|
</div>
|
||||||
<UDivider class="my-2" />
|
<UDivider class="my-2" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="talents && getCategories" class="mt-8">
|
<div
|
||||||
<div v-if="talents.length > 0 && !pending" class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3">
|
v-if="talents && getCategories"
|
||||||
|
class="mt-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="talents.length > 0 && !pending"
|
||||||
|
class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="talent in talents"
|
v-for="talent in talents"
|
||||||
:key="talent.name.toLowerCase().trim()"
|
:key="talent.name.toLowerCase().trim()"
|
||||||
@@ -201,18 +227,31 @@ async function suggest() {
|
|||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex gap-6 items-center">
|
<div class="flex gap-6 items-center">
|
||||||
<img :src="talent.logo" class="z-20 h-8 w-8 md:h-12 md:w-12 rounded-md">
|
<img
|
||||||
|
:src="talent.logo"
|
||||||
|
alt="Talent profile picture"
|
||||||
|
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">
|
||||||
<div 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 :to="talent.website" target="_blank">
|
<NuxtLink
|
||||||
|
:to="talent.website"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<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 v-if="talent.favorite" text="You can set the filter to only show favorites.">
|
<UTooltip
|
||||||
<UIcon name="i-ic-round-star" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" />
|
v-if="talent.favorite"
|
||||||
|
text="You can set the filter to only show favorites."
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||||
|
name="i-ic-round-star"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -224,13 +263,16 @@ async function suggest() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 mt-2">
|
<div class="flex items-center gap-4 mt-2">
|
||||||
<p class="relative z-10 flex text-xs md:text-sm font-medium items-center" :class="getColor()">
|
<p
|
||||||
|
:class="getColor()"
|
||||||
|
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.categories"
|
v-for="category in talent.talentCategories"
|
||||||
:key="category.category.slug"
|
:key="category.category.slug"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
@@ -242,13 +284,19 @@ async function suggest() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="talents?.length === 0 && !pending" class="my-4 text-subtitle">
|
<div
|
||||||
|
v-else-if="talents?.length === 0 && !pending"
|
||||||
|
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 v-else class="my-4 text-subtitle">
|
<div
|
||||||
|
v-else
|
||||||
|
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>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
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>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<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>
|
||||||
@@ -24,12 +24,19 @@ const { data: projects } = await getProjects()
|
|||||||
>
|
>
|
||||||
<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 :name="project.icon" size="24" dynamic />
|
<UIcon
|
||||||
|
:name="project.icon"
|
||||||
|
dynamic
|
||||||
|
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 :to="project.link" target="_blank">
|
<NuxtLink
|
||||||
|
:to="project.link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span class="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>
|
||||||
@@ -7,12 +7,12 @@ 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)
|
||||||
@@ -20,8 +20,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -29,30 +29,30 @@ async function like() {
|
|||||||
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() {
|
||||||
@@ -62,7 +62,10 @@ async function handleLike() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-if="postContent && post" class="w-container lg:mt-24 mt-16">
|
<section
|
||||||
|
v-if="postContent && post"
|
||||||
|
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">
|
||||||
@@ -95,7 +98,10 @@ async function handleLike() {
|
|||||||
{{ postContent.description }}
|
{{ postContent.description }}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="postContent.cover" class="w-full rounded-md my-8">
|
<div
|
||||||
|
v-if="postContent.cover"
|
||||||
|
class="w-full rounded-md my-8"
|
||||||
|
>
|
||||||
{{ postContent.cover }}
|
{{ postContent.cover }}
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
@@ -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()
|
||||||
@@ -25,7 +25,11 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
|
|||||||
<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 v-for="post in posts" :key="post.slug" class="px-6 md:grid md:grid-cols-4 md:items-baseline">
|
<article
|
||||||
|
v-for="post in posts"
|
||||||
|
:key="post.slug"
|
||||||
|
class="px-6 md:grid md:grid-cols-4 md:items-baseline"
|
||||||
|
>
|
||||||
<div class="group md:col-span-3 group relative flex flex-col items-start">
|
<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" />
|
||||||
@@ -45,7 +49,10 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
|
|||||||
<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 class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium" :class="getColor">
|
<div
|
||||||
|
:class="getColor"
|
||||||
|
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>
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
directUrl = env("DIRECT_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Maintenance {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
reason String @default("")
|
|
||||||
beginAt DateTime @default(now())
|
|
||||||
endAt DateTime @default(now())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
enabled Boolean @default(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
model Announcement {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
content String @default("")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CategoryType {
|
|
||||||
TALENT
|
|
||||||
BOOKMARK
|
|
||||||
}
|
|
||||||
|
|
||||||
model Category {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
slug String @default("")
|
|
||||||
name String @default("")
|
|
||||||
type CategoryType @default(TALENT)
|
|
||||||
talents CategoriesOnTalents[]
|
|
||||||
bookmarks CategoriesOnBookmarks[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model Talent {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
logo String @default("")
|
|
||||||
name String @unique @default("")
|
|
||||||
website String @default("")
|
|
||||||
work String @default("")
|
|
||||||
favorite Boolean @default(false)
|
|
||||||
categories CategoriesOnTalents[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model Bookmark {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
name String @unique @default("")
|
|
||||||
website String @default("")
|
|
||||||
favorite Boolean @default(false)
|
|
||||||
categories CategoriesOnBookmarks[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model CategoriesOnTalents {
|
|
||||||
talentId Int
|
|
||||||
categoryId Int
|
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
|
||||||
talent Talent @relation(fields: [talentId], references: [id])
|
|
||||||
|
|
||||||
@@id([talentId, categoryId])
|
|
||||||
@@index([talentId])
|
|
||||||
@@index([categoryId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model CategoriesOnBookmarks {
|
|
||||||
bookmarkId Int
|
|
||||||
categoryId Int
|
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
|
||||||
bookmark Bookmark @relation(fields: [bookmarkId], references: [id])
|
|
||||||
|
|
||||||
@@id([bookmarkId, categoryId])
|
|
||||||
@@index([bookmarkId])
|
|
||||||
@@index([categoryId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Post {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
slug String @unique @default("")
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
views Int @default(0)
|
|
||||||
likes Int @default(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
model Suggestion {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
email String @unique @default("")
|
|
||||||
content String @default("")
|
|
||||||
added Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model Form {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
name String @default("")
|
|
||||||
email String @default("")
|
|
||||||
content String @default("")
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model GuestbookMessage {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
message String @default("")
|
|
||||||
email String @unique @default("")
|
|
||||||
image String @default("")
|
|
||||||
username String @default("")
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
5
server/api/announcement.get.ts
Normal file
5
server/api/announcement.get.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineEventHandler(async () => {
|
||||||
|
return useDB().query.announcements.findFirst({
|
||||||
|
orderBy: (announcement, {asc}) => [asc(announcement.createdAt)]
|
||||||
|
})
|
||||||
|
})
|
||||||
16
server/api/article.post.ts
Normal file
16
server/api/article.post.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {z} from 'zod'
|
||||||
|
|
||||||
|
const PostSchema = z.object({ slug: z.string() }).parse
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { slug } = await readValidatedBody(event, PostSchema)
|
||||||
|
return useDB().insert(tables.posts).values({
|
||||||
|
slug
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
target: tables.posts.id,
|
||||||
|
set: {
|
||||||
|
views: sql`${tables.posts.views}
|
||||||
|
+ 1`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
20
server/api/bookmarks.get.ts
Normal file
20
server/api/bookmarks.get.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { favorite, category } = getQuery(event)
|
||||||
|
|
||||||
|
const bookmarks = await useDB().query.bookmarks
|
||||||
|
.findMany({
|
||||||
|
orderBy: [asc(tables.talents.id)],
|
||||||
|
with: {
|
||||||
|
bookmarkCategories: {
|
||||||
|
with: {
|
||||||
|
category: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return bookmarks.filter(bookmark =>
|
||||||
|
(category === 'all' || bookmark.bookmarkCategories.some(cat => cat.category.slug === category))
|
||||||
|
&& (favorite === 'false' || bookmark.favorite)
|
||||||
|
)
|
||||||
|
})
|
||||||
4
server/api/categories.get.ts
Normal file
4
server/api/categories.get.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { type } = getQuery<{ type: 'talent' | 'bookmark' }>(event)
|
||||||
|
return useDB().select().from(tables.categories).where(eq(tables.categories.type, type))
|
||||||
|
})
|
||||||
@@ -4,14 +4,10 @@ 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 await usePrisma().post.update({
|
return useDB().update(tables.posts)
|
||||||
where: {
|
.set({
|
||||||
slug,
|
likes: sql`${tables.posts.likes}
|
||||||
},
|
+ 1`
|
||||||
data: {
|
})
|
||||||
likes: {
|
.where(eq(tables.posts.slug, slug))
|
||||||
increment: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
22
server/api/maintenance.get.ts
Normal file
22
server/api/maintenance.get.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const maintenance = await useDB().query.maintenances.findFirst({
|
||||||
|
orderBy: [asc(tables.maintenances.createdAt)]
|
||||||
|
})
|
||||||
|
let enabled = true
|
||||||
|
// eslint-disable-next-line node/prefer-global/process
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const today = new Date()
|
||||||
|
enabled = !!maintenance
|
||||||
|
&& maintenance.enabled
|
||||||
|
&& new Date(maintenance.beginAt).getTime() < today.getTime()
|
||||||
|
&& new Date(maintenance.endAt).getTime() > today.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
maintenance
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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) => {
|
||||||
@@ -9,10 +9,5 @@ export default defineEventHandler(async (event) => {
|
|||||||
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 await usePrisma().guestbookMessage.delete({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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) => {
|
||||||
@@ -11,22 +11,20 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
await sendDiscordWebhookMessage(config, {
|
await sendDiscordWebhookMessage(config, {
|
||||||
title: 'New guestbook message ✨',
|
title: 'New guestbook message ✨',
|
||||||
description: `**${user.username}** as signed the book : "*${message}*"`,
|
description: `**${user.username}** has signed the book : "*${message}*"`,
|
||||||
color: 15893567,
|
color: 15893567
|
||||||
})
|
})
|
||||||
|
return useDB().insert(tables.guestbookMessages)
|
||||||
return await usePrisma().guestbookMessage.upsert({
|
.values({
|
||||||
where: {
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
message,
|
message,
|
||||||
},
|
|
||||||
create: {
|
|
||||||
email: user.email,
|
email: user.email,
|
||||||
image: user.picture,
|
|
||||||
username: user.username,
|
username: user.username,
|
||||||
message,
|
image: user.picture
|
||||||
},
|
})
|
||||||
})
|
.onConflictDoUpdate({
|
||||||
|
target: tables.guestbookMessages.email,
|
||||||
|
set: {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
3
server/api/messages.get.ts
Normal file
3
server/api/messages.get.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default defineEventHandler(async () => {
|
||||||
|
return useDB().select().from(tables.guestbookMessages).orderBy(asc(tables.guestbookMessages.createdAt))
|
||||||
|
})
|
||||||
@@ -8,8 +8,8 @@ export default defineCachedEventHandler(async (event) => {
|
|||||||
coding,
|
coding,
|
||||||
editors,
|
editors,
|
||||||
os,
|
os,
|
||||||
languages,
|
languages
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
maxAge: 60 * 60 * 3, // 3 hours,
|
maxAge: 60 * 60 * 3 // 3 hours,
|
||||||
})
|
})
|
||||||
34
server/api/suggestion.post.ts
Normal file
34
server/api/suggestion.post.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const SuggestionValidator = z.object({
|
||||||
|
content: z.string()
|
||||||
|
}).parse
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { content } = await readValidatedBody(event, SuggestionValidator)
|
||||||
|
const { user } = await requireUserSession(event)
|
||||||
|
const config = useRuntimeConfig(event)
|
||||||
|
|
||||||
|
await sendDiscordWebhookMessage(config, {
|
||||||
|
title: 'New suggestion ✨',
|
||||||
|
description: `**${user.username}** has requested **${content}** for the talents page.`,
|
||||||
|
color: 15237114
|
||||||
|
})
|
||||||
|
|
||||||
|
return useDB().insert(tables.suggestions)
|
||||||
|
.values({
|
||||||
|
email: user.email,
|
||||||
|
content
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: tables.suggestions.email,
|
||||||
|
set: {
|
||||||
|
content
|
||||||
|
},
|
||||||
|
setWhere: sql`${tables.suggestions.email}
|
||||||
|
=
|
||||||
|
${user.email}`
|
||||||
|
}).returning({
|
||||||
|
content: tables.suggestions.content
|
||||||
|
})
|
||||||
|
})
|
||||||
20
server/api/talents.get.ts
Normal file
20
server/api/talents.get.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { favorite, category } = getQuery(event)
|
||||||
|
|
||||||
|
const talents = await useDB().query.talents
|
||||||
|
.findMany({
|
||||||
|
orderBy: [asc(tables.talents.id)],
|
||||||
|
with: {
|
||||||
|
talentCategories: {
|
||||||
|
with: {
|
||||||
|
category: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return talents.filter(talent =>
|
||||||
|
(category === 'all' || talent.talentCategories.some(cat => cat.category.slug === category))
|
||||||
|
&& (favorite === 'false' || talent.favorite)
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -4,14 +4,10 @@ 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 await usePrisma().post.update({
|
return useDB().update(tables.posts)
|
||||||
where: {
|
.set({
|
||||||
slug,
|
views: sql`${tables.posts.views}
|
||||||
},
|
+ 1`
|
||||||
data: {
|
})
|
||||||
views: {
|
.where(eq(tables.posts.slug, slug))
|
||||||
increment: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
128
server/database/schema.ts
Normal file
128
server/database/schema.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { boolean, date, integer, pgEnum, pgTable, primaryKey, serial, text, timestamp } from 'drizzle-orm/pg-core'
|
||||||
|
import { relations } from 'drizzle-orm'
|
||||||
|
|
||||||
|
// O B J E C T S
|
||||||
|
|
||||||
|
export const maintenances = pgTable('maintenances', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
reason: text('reason').default(''),
|
||||||
|
enabled: boolean('enabled').default(false).notNull(),
|
||||||
|
beginAt: date('begin_at').defaultNow().notNull(),
|
||||||
|
endAt: date('end_at').defaultNow().notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const announcements = pgTable('announcements', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
content: text('content').default('').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const posts = pgTable('posts', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
slug: text('slug').notNull(),
|
||||||
|
likes: integer('likes').default(0).notNull(),
|
||||||
|
views: integer('views').default(0).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const suggestions = pgTable('suggestions', {
|
||||||
|
id: serial('id').notNull(),
|
||||||
|
email: text('email').notNull().unique(),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
added: boolean('added').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
||||||
|
}, t => ({
|
||||||
|
pk: primaryKey({columns: [t.id, t.email]})
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const guestbookMessages = pgTable('guestbook_messages', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
message: text('message').notNull(),
|
||||||
|
email: text('email').notNull().unique(),
|
||||||
|
username: text('username').notNull(),
|
||||||
|
image: text('image').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const categoriesType = pgEnum('categoryType', ['talent', 'bookmark'])
|
||||||
|
|
||||||
|
export const categories = pgTable('categories', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
slug: text('slug').unique().notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
type: categoriesType('type').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const talents = pgTable('talents', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
logo: text('logo').default('').notNull(),
|
||||||
|
website: text('website').default('').notNull(),
|
||||||
|
work: text('work').default('').notNull(),
|
||||||
|
favorite: boolean('favorite').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const talentsToCategories = pgTable('talents_categories', {
|
||||||
|
talentId: integer('talent_id').notNull()
|
||||||
|
.references(() => talents.id, { onDelete: 'cascade' }),
|
||||||
|
categoryId: integer('category_id').notNull()
|
||||||
|
.references(() => categories.id, {onDelete: 'cascade'})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const bookmarks = pgTable('bookmarks', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
website: text('website').default('').notNull(),
|
||||||
|
favorite: boolean('favorite').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const bookmarksToCategories = pgTable('bookmarks_categories', {
|
||||||
|
bookmarkId: integer('bookmark_id').notNull()
|
||||||
|
.references(() => bookmarks.id, { onDelete: 'cascade' }),
|
||||||
|
categoryId: integer('category_id').notNull()
|
||||||
|
.references(() => categories.id, {onDelete: 'cascade'})
|
||||||
|
}, t => ({
|
||||||
|
pk: primaryKey({columns: [t.bookmarkId, t.categoryId]})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// R E L A T I O N S
|
||||||
|
|
||||||
|
export const talentsRelations = relations(talents, ({ many }) => ({
|
||||||
|
talentCategories: many(talentsToCategories)
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const bookmarksRelations = relations(bookmarks, ({ many }) => ({
|
||||||
|
bookmarkCategories: many(bookmarksToCategories)
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const categoriesRelations = relations(categories, ({ many }) => ({
|
||||||
|
talentsToCategories: many(talentsToCategories),
|
||||||
|
bookmarksToCategories: many(bookmarksToCategories)
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const talentsToCategoriesRelations = relations(talentsToCategories, ({ one }) => ({
|
||||||
|
talent: one(talents, {
|
||||||
|
references: [talents.id],
|
||||||
|
fields: [talentsToCategories.talentId]
|
||||||
|
}),
|
||||||
|
category: one(categories, {
|
||||||
|
references: [categories.id],
|
||||||
|
fields: [talentsToCategories.categoryId]
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const bookmarksToCategoriesRelations = relations(bookmarksToCategories, ({ one }) => ({
|
||||||
|
bookmark: one(bookmarks, {
|
||||||
|
references: [bookmarks.id],
|
||||||
|
fields: [bookmarksToCategories.bookmarkId]
|
||||||
|
}),
|
||||||
|
category: one(categories, {
|
||||||
|
references: [categories.id],
|
||||||
|
fields: [bookmarksToCategories.categoryId]
|
||||||
|
})
|
||||||
|
}))
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
export default oauth.githubEventHandler({
|
export default oauth.githubEventHandler({
|
||||||
config: {
|
config: {
|
||||||
emailRequired: true,
|
emailRequired: true
|
||||||
},
|
},
|
||||||
async onSuccess(event: any, { user }: any) {
|
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(),
|
||||||
// eslint-disable-next-line node/prefer-global/process
|
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: any) {
|
onError(error) {
|
||||||
console.error('GitHub OAuth error:', error)
|
console.error('GitHub OAuth error:', error)
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
export default oauth.googleEventHandler({
|
export default oauth.googleEventHandler({
|
||||||
async onSuccess(event: any, { user }: any) {
|
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(),
|
||||||
// eslint-disable-next-line node/prefer-global/process
|
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: any) {
|
onError(error) {
|
||||||
console.error('Google OAuth error:', error)
|
console.error('Google OAuth error:', error)
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
14
server/utils/db.ts
Normal file
14
server/utils/db.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
|
import postgres from 'postgres'
|
||||||
|
import * as schema from '../database/schema'
|
||||||
|
|
||||||
|
export const tables = schema
|
||||||
|
export { sql, eq, and, or, asc, desc, sum, inArray } from 'drizzle-orm'
|
||||||
|
|
||||||
|
// eslint-disable-next-line node/prefer-global/process
|
||||||
|
const connectionString = process.env.DATABASE_URL as string
|
||||||
|
const client = postgres(connectionString, { prepare: false })
|
||||||
|
|
||||||
|
export function useDB() {
|
||||||
|
return drizzle(client, { schema })
|
||||||
|
}
|
||||||
@@ -17,12 +17,12 @@ export async function sendDiscordWebhookMessage(config: RuntimeConfig, content:
|
|||||||
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,16 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
const appConfig = useAppConfig()
|
|
||||||
const getTextColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
|
||||||
|
|
||||||
function getGroupColor() {
|
|
||||||
return `group-hover:text-${appConfig.ui.primary}-500`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NuxtLink to="/" class="flex gap-1 items-center rounded-xl group text-xl !bg-transparent !dark:bg-transparent">
|
|
||||||
<span :class="getTextColor" class="font-black group-hover:text-black dark:group-hover:text-white duration-300">Arthur</span>
|
|
||||||
<span :class="getGroupColor()" class="font-bold text-gray-300 dark:text-neutral-600 duration-300">/</span>
|
|
||||||
<span :class="getTextColor" class="font-black group-hover:text-black dark:group-hover:text-white duration-300">Danjou</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { otherTab } from '~~/types'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const isOpenModal = ref(false)
|
|
||||||
|
|
||||||
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<nav class="hidden lg:block z-50">
|
|
||||||
<div class="flex items-center h-10 rounded-md p-1 gap-1 relative bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
|
|
||||||
<UButton to="/" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path === '/' }">
|
|
||||||
Home
|
|
||||||
</UButton>
|
|
||||||
<UButton to="/about" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/about') }">
|
|
||||||
About
|
|
||||||
</UButton>
|
|
||||||
<!-- <UButton to="/writing" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/writing') }">
|
|
||||||
Articles
|
|
||||||
</UButton> -->
|
|
||||||
<UButton to="/work" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/work') }">
|
|
||||||
Projects
|
|
||||||
</UButton>
|
|
||||||
<UButton to="/uses" size="sm" variant="ghost" color="white" :class="{ 'link-active': route.path.includes('/uses') }">
|
|
||||||
Uses
|
|
||||||
</UButton>
|
|
||||||
<UDropdown mode="hover" :items="otherTab" :popper="{ placement: 'bottom' }">
|
|
||||||
<UButton size="sm" variant="ghost" color="white" class="duration-300">
|
|
||||||
Other
|
|
||||||
</UButton>
|
|
||||||
</UDropdown>
|
|
||||||
<UButton size="sm" variant="ghost" color="white" @click="isOpenModal = true">
|
|
||||||
Contact
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
<UModal v-model="isOpenModal">
|
|
||||||
<UCard class="p-4">
|
|
||||||
<div>
|
|
||||||
<div class="mb-8 flex justify-between items-center">
|
|
||||||
<h1 class="text-xl font-bold">
|
|
||||||
Contact me
|
|
||||||
</h1>
|
|
||||||
<UButton size="xs" icon="i-akar-icons-cross" variant="ghost" @click.prevent="isOpenModal = false" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-6">
|
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<h3 class="text-sm">
|
|
||||||
Email
|
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-subtitle">
|
|
||||||
arthurdanjou@outlook.fr
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<UButtonGroup size="sm" orientation="horizontal">
|
|
||||||
<UButton variant="solid" color="gray" label="Compose" to="mailto:arthurdanjou@outlook.fr" icon="i-mdi-note-edit-outline" />
|
|
||||||
<UButton v-if="copied" variant="solid" color="green" label="Copied" icon="i-mdi-content-copy" />
|
|
||||||
<UButton v-else variant="solid" color="gray" label="Copy" icon="i-mdi-content-copy" @click.prevent="copy()" />
|
|
||||||
</UButtonGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UDivider label="OR" />
|
|
||||||
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<h3 class="text-sm">
|
|
||||||
Get in touch
|
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-subtitle">
|
|
||||||
I'm most active on Twitter
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<UButtonGroup size="sm" orientation="horizontal">
|
|
||||||
<UButton
|
|
||||||
variant="solid" color="gray" label="Github" icon="i-ph-github-logo-bold"
|
|
||||||
to="https://github.com/ArthurDanjou" target="_blank"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
variant="solid" color="gray" label="Twitter" icon="i-ph-twitter-logo-bold"
|
|
||||||
to="https://twitter.com/ArthurDanj" target="_blank"
|
|
||||||
/>
|
|
||||||
</UButtonGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</UModal>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.link-active {
|
|
||||||
@apply bg-white/60 dark:bg-black
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,7 +0,0 @@
|
|||||||
export default defineEventHandler(async () => {
|
|
||||||
return await usePrisma().announcement.findFirst({
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const PostSchema = z.object({ slug: z.string() }).parse
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { slug } = await readValidatedBody(event, PostSchema)
|
|
||||||
return await usePrisma().post.upsert({
|
|
||||||
where: {
|
|
||||||
slug,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
views: {
|
|
||||||
increment: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
slug,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { favorite, category } = getQuery(event)
|
|
||||||
const prisma = usePrisma()
|
|
||||||
|
|
||||||
let whereClause: any
|
|
||||||
|
|
||||||
if (favorite === 'true') {
|
|
||||||
category === 'all'
|
|
||||||
? whereClause = {
|
|
||||||
favorite: true,
|
|
||||||
categories: { every: { category: {} } },
|
|
||||||
}
|
|
||||||
: whereClause = {
|
|
||||||
favorite: true,
|
|
||||||
categories: { some: { category: { slug: category } } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
category === 'all'
|
|
||||||
? whereClause = {
|
|
||||||
categories: { every: { category: {} } },
|
|
||||||
}
|
|
||||||
: whereClause = {
|
|
||||||
categories: { some: { category: { slug: category } } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await prisma.bookmark.findMany({
|
|
||||||
where: whereClause,
|
|
||||||
orderBy: {
|
|
||||||
name: 'asc',
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
categories: {
|
|
||||||
include: {
|
|
||||||
category: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
category: {
|
|
||||||
name: 'asc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { CategoryType } from '@prisma/client'
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { type } = getQuery<{ type: CategoryType }>(event)
|
|
||||||
return await usePrisma().category.findMany({
|
|
||||||
where: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export default defineEventHandler(async () => {
|
|
||||||
const maintenance = await usePrisma().maintenance.findFirst({
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
let enabled = true
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const today = new Date()
|
|
||||||
enabled = !!maintenance
|
|
||||||
&& maintenance.enabled
|
|
||||||
&& maintenance.beginAt.getTime() < today.getTime()
|
|
||||||
&& maintenance.endAt.getTime() > today.getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
maintenance,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default defineEventHandler(async () => {
|
|
||||||
return await usePrisma().guestbookMessage.findMany({
|
|
||||||
orderBy: {
|
|
||||||
updatedAt: 'desc',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const SuggestionValidator = z.object({
|
|
||||||
content: z.string(),
|
|
||||||
}).parse
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const { content } = await readValidatedBody(event, SuggestionValidator)
|
|
||||||
const { user } = await requireUserSession(event)
|
|
||||||
const config = useRuntimeConfig(event)
|
|
||||||
|
|
||||||
await sendDiscordWebhookMessage(config, {
|
|
||||||
title: 'New suggestion ✨',
|
|
||||||
description: `**${user.username}** as requested **${content}** for the talents page.`,
|
|
||||||
color: 15237114,
|
|
||||||
})
|
|
||||||
|
|
||||||
return await usePrisma().suggestion.upsert({
|
|
||||||
where: {
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
content,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
email: user.email,
|
|
||||||
content,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user