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">
|
||||
<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 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>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@ function jumpVal(val: number) {
|
||||
return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random()
|
||||
}
|
||||
|
||||
let timeout: any
|
||||
let timeout: NodeJS.Timeout
|
||||
function jumpPoints() {
|
||||
for (let i = 0; i < points.value.length; i++)
|
||||
points.value[i] = [jumpVal(points.value[i][0]), jumpVal(points.value[i][1])]
|
||||
@@ -21,7 +21,10 @@ onUnmounted(() => clearTimeout(timeout))
|
||||
|
||||
<template>
|
||||
<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
|
||||
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})` }"
|
||||
@@ -13,11 +13,25 @@ const year = computed(() => new Date().getFullYear())
|
||||
<p class="text-subtitle">
|
||||
Designed & Built by
|
||||
</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>
|
||||
<p class="text-subtitle flex items-center">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,8 +2,8 @@
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Uses Section title',
|
||||
},
|
||||
default: 'Uses Section title'
|
||||
}
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
@@ -14,7 +14,10 @@ const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||
<template>
|
||||
<section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4">
|
||||
<div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
||||
<h2 class="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="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||
</span>
|
||||
@@ -2,8 +2,8 @@
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Uses Slot title',
|
||||
},
|
||||
default: 'Uses Slot title'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
<div class="flex justify-between items-center">
|
||||
<Logo />
|
||||
@@ -47,7 +50,11 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
||||
</template>
|
||||
|
||||
<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
|
||||
v-if="nav.to"
|
||||
size="sm"
|
||||
@@ -83,7 +90,12 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
||||
<h1 class="text-xl font-bold">
|
||||
Contact me
|
||||
</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 class="flex flex-col space-y-6">
|
||||
<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>
|
||||
</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
|
||||
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>
|
||||
@@ -114,14 +148,25 @@ const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copie
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<UButtonGroup size="sm" orientation="horizontal">
|
||||
<UButtonGroup
|
||||
orientation="horizontal"
|
||||
size="sm"
|
||||
>
|
||||
<UButton
|
||||
variant="solid" color="gray" label="Github" icon="i-ph-github-logo-bold"
|
||||
to="https://github.com/ArthurDanjou" target="_blank"
|
||||
color="gray"
|
||||
icon="i-ph-github-logo-bold"
|
||||
label="Github"
|
||||
target="_blank"
|
||||
to="https://github.com/ArthurDanjou"
|
||||
variant="solid"
|
||||
/>
|
||||
<UButton
|
||||
variant="solid" color="gray" label="Twitter" icon="i-ph-twitter-logo-bold"
|
||||
to="https://twitter.com/ArthurDanj" target="_blank"
|
||||
color="gray"
|
||||
icon="i-ph-twitter-logo-bold"
|
||||
label="Twitter"
|
||||
target="_blank"
|
||||
to="https://twitter.com/ArthurDanj"
|
||||
variant="solid"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
</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="{
|
||||
background: 'bg-white dark:bg-stone-900',
|
||||
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
|
||||
container: 'z-30',
|
||||
container: 'z-30'
|
||||
}"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
@@ -31,14 +31,23 @@ watch(isDark, () => {
|
||||
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<template #panel>
|
||||
<div class="p-2">
|
||||
<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
|
||||
color="gray"
|
||||
square
|
||||
@@ -46,15 +55,21 @@ watch(isDark, () => {
|
||||
color: {
|
||||
white: {
|
||||
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
|
||||
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
||||
},
|
||||
},
|
||||
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
|
||||
}
|
||||
}
|
||||
}"
|
||||
:variant="color === getColor ? 'solid' : 'ghost'"
|
||||
@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`">
|
||||
<UIcon v-if="color === getColor" name="i-ic-round-check" />
|
||||
<span
|
||||
:class="`bg-${color}-500/80 border-${color}-500`"
|
||||
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
|
||||
>
|
||||
<UIcon
|
||||
v-if="color === getColor"
|
||||
name="i-ic-round-check"
|
||||
/>
|
||||
</span>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
@@ -1,8 +1,8 @@
|
||||
<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 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) {
|
||||
return `${useDateFormat(date, 'DD MMM YYYY').value} at ${useDateFormat(date, 'HH:mm:ss').value}`
|
||||
@@ -10,15 +10,21 @@ function formatDate(date: number) {
|
||||
|
||||
const CardUi = {
|
||||
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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCard class="flex flex-col justify-between" :ui="CardUi">
|
||||
<div v-if="activity && activity.data.activities" class="flex items-center gap-x-4">
|
||||
<UCard
|
||||
:ui="CardUi"
|
||||
class="flex flex-col justify-between"
|
||||
>
|
||||
<div
|
||||
v-if="activity && activity.data.activities"
|
||||
class="flex items-center gap-x-4"
|
||||
>
|
||||
<p
|
||||
class="uppercase tracking-widest text-sm"
|
||||
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
|
||||
@@ -29,7 +35,7 @@ useIntervalFn(async () => await refresh(), 5000)
|
||||
<div class="flex gap-4 items-center">
|
||||
<UIcon
|
||||
class="h-10 w-10"
|
||||
name="i-skill-icons-vscode-light"
|
||||
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
|
||||
/>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -45,12 +51,15 @@ useIntervalFn(async () => await refresh(), 5000)
|
||||
I'm Idling on my computer
|
||||
</h3>
|
||||
<h3 v-else>
|
||||
{{ codingActivity.details }}
|
||||
{{ codingActivity.details }} - {{ codingActivity.state }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-subtitle">
|
||||
<div
|
||||
v-else
|
||||
class="text-subtitle"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1>I'm currently offline</h1>
|
||||
<UTooltip text="I'm offline 🫥">
|
||||
@@ -63,7 +72,10 @@ useIntervalFn(async () => await refresh(), 5000)
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end w-full">
|
||||
<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) }}
|
||||
</p>
|
||||
</ClientOnly>
|
||||
@@ -79,7 +91,10 @@ useIntervalFn(async () => await refresh(), 5000)
|
||||
target="_blank"
|
||||
label="Lanyard"
|
||||
/>
|
||||
<UIcon name="i-jam-thunder" class="text-subtitle" />
|
||||
<UIcon
|
||||
class="text-subtitle"
|
||||
name="i-jam-thunder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,23 +3,23 @@ const socials = [
|
||||
{
|
||||
name: 'mail',
|
||||
icon: 'i-material-symbols-alternate-email',
|
||||
link: 'mailto:arthurdanjou@outlook.fr',
|
||||
link: 'mailto:arthurdanjou@outlook.fr'
|
||||
},
|
||||
{
|
||||
name: 'twitter',
|
||||
icon: 'i-ph-twitter-logo-bold',
|
||||
link: 'https://twitter.com/ArthurDanj',
|
||||
link: 'https://twitter.com/ArthurDanj'
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
icon: 'i-ph-github-logo-bold',
|
||||
link: 'https://github.com/ArthurDanjou',
|
||||
link: 'https://github.com/ArthurDanjou'
|
||||
},
|
||||
{
|
||||
name: 'linkedin',
|
||||
icon: 'i-ph-linkedin-logo-bold',
|
||||
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
||||
},
|
||||
link: 'https://www.linkedin.com/in/arthurdanjou/'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -5,12 +5,15 @@ const stats = await $fetch<Stats>('/api/stats')
|
||||
|
||||
const CardUi = {
|
||||
footer: { padding: 'px-4 py-2' },
|
||||
body: { base: 'h-full' },
|
||||
body: {base: 'h-full'}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<p
|
||||
class="uppercase tracking-widest text-sm"
|
||||
@@ -23,7 +26,9 @@ const CardUi = {
|
||||
<div class="text-md">
|
||||
<div class="flex items-center gap-x-1">
|
||||
<h3>Total hours:</h3>
|
||||
<p class="text-subtitle">{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours</p>
|
||||
<p class="text-subtitle">
|
||||
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-x-1 flex-wrap">
|
||||
<h3>Best Editors:</h3>
|
||||
@@ -33,11 +38,15 @@ const CardUi = {
|
||||
</div>
|
||||
<div class="flex items-center gap-x-1">
|
||||
<h3>Best OS:</h3>
|
||||
<p class="text-subtitle">{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%</p>
|
||||
<p class="text-subtitle">
|
||||
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-x-1 flex-wrap">
|
||||
<h3>Top languages:</h3>
|
||||
<p class="text-subtitle">{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}</p>
|
||||
<p class="text-subtitle">
|
||||
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +55,10 @@ const CardUi = {
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<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 }}
|
||||
</p>
|
||||
</ClientOnly>
|
||||
@@ -62,7 +74,10 @@ const CardUi = {
|
||||
target="_blank"
|
||||
label="Wakatime"
|
||||
/>
|
||||
<UIcon name="i-jam-thunder" class="text-subtitle" />
|
||||
<UIcon
|
||||
class="text-subtitle"
|
||||
name="i-jam-thunder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,8 +3,8 @@ defineProps({
|
||||
startDate: String,
|
||||
endDate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function formatTodayDate(date: string) {
|
||||
@@ -14,10 +14,18 @@ function formatTodayDate(date: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UBadge v-if="startDate !== endDate" variant="soft" size="xs">
|
||||
<UBadge
|
||||
v-if="startDate !== endDate"
|
||||
size="xs"
|
||||
variant="soft"
|
||||
>
|
||||
{{ formatTodayDate(startDate!.toString()) }} — {{ formatTodayDate(endDate) }}
|
||||
</UBadge>
|
||||
<UBadge v-else variant="soft" size="xs">
|
||||
<UBadge
|
||||
v-else
|
||||
size="xs"
|
||||
variant="soft"
|
||||
>
|
||||
{{ formatTodayDate(endDate) }}
|
||||
</UBadge>
|
||||
</template>
|
||||
@@ -2,15 +2,21 @@
|
||||
import type { Education } from '~~/types'
|
||||
|
||||
defineProps({
|
||||
education: Object as PropType<Education>,
|
||||
education: Object as PropType<Education>
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="education" class="group relative flex flex-col items-start">
|
||||
<div
|
||||
v-if="education"
|
||||
class="group relative flex flex-col items-start"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<DateTag :start-date="education.startDate" :end-date="education.endDate" />
|
||||
<DateTag
|
||||
:end-date="education.endDate"
|
||||
:start-date="education.startDate"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
{{ education.title }}
|
||||
@@ -2,16 +2,22 @@
|
||||
import type { WorkExperience } from '~~/types'
|
||||
|
||||
defineProps({
|
||||
experience: Object as PropType<WorkExperience>,
|
||||
experience: Object as PropType<WorkExperience>
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="experience" class="group relative flex flex-col items-start">
|
||||
<div
|
||||
v-if="experience"
|
||||
class="group relative flex flex-col items-start"
|
||||
>
|
||||
<div>
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<DateTag :start-date="experience.startDate" :end-date="experience.endDate" />
|
||||
<DateTag
|
||||
:end-date="experience.endDate"
|
||||
:start-date="experience.startDate"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center my-1">
|
||||
<UButton
|
||||
@@ -26,10 +32,16 @@ defineProps({
|
||||
class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-akar-icons-link-chain" color="gray" />
|
||||
<UIcon
|
||||
color="gray"
|
||||
name="i-akar-icons-link-chain"
|
||||
/>
|
||||
</template>
|
||||
</UButton>
|
||||
<h1 v-else class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
<h1
|
||||
v-else
|
||||
class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
|
||||
>
|
||||
{{ experience.company }}
|
||||
</h1>
|
||||
<div class="text-subtitle text-xs">
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Skill } from '~~/types'
|
||||
|
||||
defineProps({
|
||||
skill: Object as PropType<Skill>,
|
||||
skill: Object as PropType<Skill>
|
||||
})
|
||||
|
||||
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"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<UIcon v-if="isLight" :name="skill.icon.light ? skill.icon.light : skill.icon" size="20" dynamic />
|
||||
<UIcon v-else :name="skill.icon.dark ? skill.icon.dark : skill.icon" size="20" dynamic />
|
||||
<UIcon
|
||||
v-if="isLight"
|
||||
:name="skill.icon.light ? skill.icon.light : skill.icon"
|
||||
dynamic
|
||||
size="20"
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
|
||||
dynamic
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-subtitle">{{ skill.name }}</span>
|
||||
</li>
|
||||
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) => {
|
||||
useCookie('last-route', { path: '/', default: () => '/' }).value = route.fullPath
|
||||
})
|
||||
|
||||
useHead({
|
||||
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.jpeg' }],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1,7 +1,7 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const isMaintenance = ref<boolean>(true)
|
||||
try {
|
||||
await $fetch('/api/maintenance').then((maintenance: any) => {
|
||||
await $fetch('/api/maintenance').then((maintenance) => {
|
||||
isMaintenance.value = maintenance.enabled
|
||||
})
|
||||
}
|
||||
@@ -11,14 +11,14 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
|
||||
if (isMaintenance.value && to.path !== '/maintenance') {
|
||||
return navigateTo('/maintenance', {
|
||||
redirectCode: 301,
|
||||
redirectCode: 301
|
||||
})
|
||||
}
|
||||
|
||||
if (!isMaintenance.value && to.path === '/maintenance') {
|
||||
return navigateTo('/', {
|
||||
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 */
|
||||
export default defineNuxtConfig({
|
||||
srcDir: 'src',
|
||||
|
||||
css: [
|
||||
'@/assets/css/main.scss',
|
||||
],
|
||||
|
||||
27
package.json
27
package.json
@@ -8,7 +8,7 @@
|
||||
"dev": "nuxt dev --host",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "prisma generate && nuxt prepare",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
@@ -16,30 +16,29 @@
|
||||
"@nuxt/content": "2.12.1",
|
||||
"@nuxt/ui": "2.14.2",
|
||||
"@pinia/nuxt": "0.5.1",
|
||||
"@prisma/client": "5.11.0",
|
||||
"@vercel/analytics": "1.2.2",
|
||||
"@vercel/speed-insights": "1.0.10",
|
||||
"drizzle-kit": "0.20.14",
|
||||
"drizzle-orm": "0.30.8",
|
||||
"nuxt": "3.10.3",
|
||||
"nuxt-auth-utils": "0.0.20",
|
||||
"pinia": "2.1.7",
|
||||
"postcss-custom-properties": "13.3.5",
|
||||
"prisma": "5.11.0",
|
||||
"sass": "1.71.1",
|
||||
"tailwindcss": "3.4.1",
|
||||
"zod": "3.22.4"
|
||||
"postcss-custom-properties": "13.3.7",
|
||||
"postgres": "3.4.4",
|
||||
"sass": "1.75.0",
|
||||
"tailwindcss": "3.4.3",
|
||||
"zod": "3.22.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "2.8.1",
|
||||
"@iconify/json": "2.2.191",
|
||||
"@nuxt/eslint-config": "^0.2.0",
|
||||
"@antfu/eslint-config": "2.15.0",
|
||||
"@iconify/json": "2.2.202",
|
||||
"@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",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"@types/node": "20.11.26",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/nuxt": "10.9.0",
|
||||
"eslint": "8.57.0",
|
||||
"typescript": "5.4.2"
|
||||
"eslint": "9.1.0",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: 'About me • Arthur Danjou',
|
||||
title: 'About me • Arthur Danjou'
|
||||
})
|
||||
|
||||
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="lg:pl-20 flex justify-center">
|
||||
<div class="max-w-xs px-2.5 lg:max-w-none">
|
||||
<UTooltip text="It's me 👋" :popper="{ offsetDistance: 20 }">
|
||||
<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">
|
||||
<UTooltip
|
||||
:popper="{ offsetDistance: 20 }"
|
||||
text="It's me 👋"
|
||||
>
|
||||
<img
|
||||
alt="My main profile picture"
|
||||
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||
src="/about.png"
|
||||
>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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.
|
||||
</GridSlot>
|
||||
</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">
|
||||
<Skill
|
||||
v-for="skill in skills.body"
|
||||
@@ -58,14 +68,31 @@ const { data: experiences } = await getWorkExperiences()
|
||||
/>
|
||||
</div>
|
||||
</GridSection>
|
||||
<GridSection v-if="experiences" title="Work Experiences">
|
||||
<Experience v-for="experience in experiences" :key="experience.title" :experience="experience" />
|
||||
<GridSection
|
||||
v-if="experiences"
|
||||
title="Work Experiences"
|
||||
>
|
||||
<Experience
|
||||
v-for="experience in experiences"
|
||||
:key="experience.title"
|
||||
:experience="experience"
|
||||
/>
|
||||
</GridSection>
|
||||
<GridSection v-if="educations" title="Educations">
|
||||
<Education v-for="education in educations" :key="education.title" :education="education" />
|
||||
<GridSection
|
||||
v-if="educations"
|
||||
title="Educations"
|
||||
>
|
||||
<Education
|
||||
v-for="education in educations"
|
||||
:key="education.title"
|
||||
:education="education"
|
||||
/>
|
||||
</GridSection>
|
||||
<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
|
||||
label="Download my CV"
|
||||
icon="i-material-symbols-lab-profile-outline-rounded"
|
||||
@@ -1,30 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Bookmark, Category } from '@prisma/client'
|
||||
import { useBookmarksStore } from '~/store/bookmarks'
|
||||
|
||||
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 { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
|
||||
|
||||
const {
|
||||
data: bookmarks,
|
||||
pending,
|
||||
} = await useFetch<Array<Bookmark & { categories: Array<{ category: Category }> }>>('/api/bookmarks', {
|
||||
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
|
||||
method: 'get',
|
||||
query: {
|
||||
favorite: isFavorite,
|
||||
category: getCategory,
|
||||
category: getCategory
|
||||
},
|
||||
watch: [isFavorite, getCategory],
|
||||
watch: [isFavorite, getCategory]
|
||||
})
|
||||
|
||||
const {
|
||||
data: getCategories,
|
||||
} = 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 }))
|
||||
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'bookmark' } })
|
||||
getCategories.value!.forEach(category => categories.value.push({label: category.name, slug: category.slug}))
|
||||
|
||||
function isCategory(slug: string) {
|
||||
return getCategory.value === slug
|
||||
@@ -33,10 +27,10 @@ function isCategory(slug: string) {
|
||||
const getMarkerStyle = computed(() => {
|
||||
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
||||
return {
|
||||
top: `${selected?.offsetTop}px` || '0px',
|
||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px` || '4px',
|
||||
height: `${selected?.offsetHeight}px` || '0px',
|
||||
width: `${selected?.offsetWidth}px` || '0px',
|
||||
top: `${selected?.offsetTop}px`,
|
||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||
height: `${selected?.offsetHeight}px`,
|
||||
width: `${selected?.offsetWidth}px`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -50,13 +44,16 @@ function getColor() {
|
||||
<section class="w-container lg:my-24 my-16">
|
||||
<div class="max-w-2xl space-y-8 mb-16">
|
||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
|
||||
My librairy where I save some ressources
|
||||
My library where I save some resources
|
||||
</h1>
|
||||
<p class="leading-relaxed text-subtitle">
|
||||
You will find a selection of some of the most inspiring and complete content I have read through my research and work experience.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="getCategories" class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between">
|
||||
<div
|
||||
v-if="getCategories"
|
||||
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
|
||||
>
|
||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
||||
<ClientOnly>
|
||||
<div
|
||||
@@ -91,16 +88,28 @@ function getColor() {
|
||||
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
||||
@click.prevent="toggleFavorite()"
|
||||
>
|
||||
<UIcon v-if="isFavorite" name="i-material-symbols-check-box-outline-rounded" />
|
||||
<UIcon v-else name="i-material-symbols-check-box-outline-blank" />
|
||||
<UIcon
|
||||
v-if="isFavorite"
|
||||
name="i-material-symbols-check-box-outline-rounded"
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
name="i-material-symbols-check-box-outline-blank"
|
||||
/>
|
||||
<p>Show favorites only</p>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
<UDivider class="my-2" />
|
||||
<div v-if="bookmarks && getCategories" class="mt-8">
|
||||
<div v-if="bookmarks.length > 0 && !pending" class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div
|
||||
v-if="bookmarks && getCategories"
|
||||
class="mt-8"
|
||||
>
|
||||
<div
|
||||
v-if="bookmarks.length > 0 && !pending"
|
||||
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="bookmark in bookmarks"
|
||||
:key="bookmark.name.toLowerCase().trim()"
|
||||
@@ -109,7 +118,7 @@ function getColor() {
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<div class="flex gap-6 items-center">
|
||||
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<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
|
||||
:href="bookmark.website"
|
||||
external
|
||||
@@ -120,8 +129,14 @@ function getColor() {
|
||||
<h1 class="relative z-10">
|
||||
{{ bookmark.name }}
|
||||
</h1>
|
||||
<UTooltip v-if="bookmark.favorite" text="You can set the filter to only show favorites.">
|
||||
<UIcon name="i-ic-round-star" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" />
|
||||
<UTooltip
|
||||
v-if="bookmark.favorite"
|
||||
text="You can set the filter to only show favorites."
|
||||
>
|
||||
<UIcon
|
||||
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||
name="i-ic-round-star"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
@@ -129,7 +144,7 @@ function getColor() {
|
||||
</div>
|
||||
<div class="flex gap-2 z-10">
|
||||
<UBadge
|
||||
v-for="category in bookmark.categories"
|
||||
v-for="category in bookmark.bookmarkCategories"
|
||||
:key="category.category.slug"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
@@ -139,19 +154,28 @@ function getColor() {
|
||||
</UBadge>
|
||||
</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" />
|
||||
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="bookmarks?.length === 0 && !pending" class="my-4 text-subtitle">
|
||||
<div
|
||||
v-else-if="bookmarks?.length === 0 && !pending"
|
||||
class="my-4 text-subtitle"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<UIcon name="i-akar-icons-cross" />
|
||||
<p>There are no talents for this category. Maybe soon...</p>
|
||||
<p>There are no bookmarks for this category. Maybe soon...</p>
|
||||
</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">
|
||||
<UIcon name="i-eos-icons-loading" />
|
||||
<p>The bookmarks are loading...</p>
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GuestbookMessage } from '@prisma/client'
|
||||
import { providers } from '~~/types'
|
||||
|
||||
useHead({
|
||||
title: 'Sign my guestbook • Arthur Danjou',
|
||||
title: 'Sign my guestbook • Arthur Danjou'
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@@ -21,20 +20,20 @@ async function sign() {
|
||||
await $fetch('/api/message', {
|
||||
method: 'post',
|
||||
body: {
|
||||
message: messageContent.value,
|
||||
},
|
||||
message: messageContent.value
|
||||
}
|
||||
}).then(async () => {
|
||||
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.',
|
||||
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||
timeout: 4000,
|
||||
timeout: 4000
|
||||
})
|
||||
await refresh()
|
||||
}).catch(() => {
|
||||
toast.add({
|
||||
title: 'An error occured when signing the book!',
|
||||
color: 'red',
|
||||
title: 'An error occurred when signing the book!',
|
||||
color: 'red'
|
||||
})
|
||||
})
|
||||
messageContent.value = ''
|
||||
@@ -46,20 +45,20 @@ async function deleteMessage(id: number) {
|
||||
await $fetch('/api/message', {
|
||||
method: 'delete',
|
||||
body: {
|
||||
id,
|
||||
},
|
||||
id
|
||||
}
|
||||
}).then(async () => {
|
||||
toast.add({
|
||||
title: `Message successfully deleted`,
|
||||
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||
color: 'green',
|
||||
timeout: 4000,
|
||||
timeout: 4000
|
||||
})
|
||||
await refresh()
|
||||
}).catch(() => {
|
||||
toast.add({
|
||||
title: 'An error occured when deleting a message!',
|
||||
color: 'red',
|
||||
color: 'red'
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -88,18 +87,33 @@ async function deleteMessage(id: number) {
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 v-if="loggedIn" class="text-md font-bold">
|
||||
<h1
|
||||
v-if="loggedIn"
|
||||
class="text-md font-bold"
|
||||
>
|
||||
Enter just below your message to sign my book
|
||||
</h1>
|
||||
<h1 v-else class="text-md font-bold">
|
||||
<h1
|
||||
v-else
|
||||
class="text-md font-bold"
|
||||
>
|
||||
Sign before writing your message
|
||||
</h1>
|
||||
</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>
|
||||
</template>
|
||||
<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">
|
||||
<input
|
||||
v-model="messageContent"
|
||||
@@ -125,7 +139,10 @@ async function deleteMessage(id: number) {
|
||||
Logout
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 justify-center">
|
||||
<div
|
||||
v-else
|
||||
class="flex gap-2 justify-center"
|
||||
>
|
||||
<UButton
|
||||
v-for="provider in providers"
|
||||
:key="provider.slug"
|
||||
@@ -134,13 +151,16 @@ async function deleteMessage(id: number) {
|
||||
variant="solid"
|
||||
:to="provider.link"
|
||||
:icon="provider.icon"
|
||||
external
|
||||
:external="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
<div v-if="messages" class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8">
|
||||
<div
|
||||
v-if="messages"
|
||||
class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8"
|
||||
>
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
@@ -151,7 +171,11 @@ async function deleteMessage(id: number) {
|
||||
</p>
|
||||
<div class="flex items-center gap-4 mt-4">
|
||||
<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>
|
||||
<p class="font-bold">
|
||||
{{ message.username }}
|
||||
@@ -169,7 +193,10 @@ async function deleteMessage(id: number) {
|
||||
/>
|
||||
</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">
|
||||
<UIcon name="i-eos-icons-loading" />
|
||||
<p>The messages are loading...</p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: 'Arthur Danjou • Software Engineer and Maths Lover',
|
||||
title: 'Arthur Danjou • Software Engineer and Maths Lover'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
layout: 'maintenance',
|
||||
layout: 'maintenance'
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Site under maintenance • Arthur Danjou',
|
||||
title: 'Site under maintenance • Arthur Danjou'
|
||||
})
|
||||
|
||||
const { data: maintenance } = await useFetch('/api/maintenance')
|
||||
@@ -17,23 +17,23 @@ const socials = [
|
||||
{
|
||||
name: 'mail',
|
||||
icon: 'i-material-symbols-alternate-email',
|
||||
link: 'mailto:arthurdanjou@outlook.fr',
|
||||
link: 'mailto:arthurdanjou@outlook.fr'
|
||||
},
|
||||
{
|
||||
name: 'twitter',
|
||||
icon: 'i-ph-twitter-logo-bold',
|
||||
link: 'https://twitter.com/ArthurDanj',
|
||||
link: 'https://twitter.com/ArthurDanj'
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
icon: 'i-ph-github-logo-bold',
|
||||
link: 'https://github.com/ArthurDanjou',
|
||||
link: 'https://github.com/ArthurDanjou'
|
||||
},
|
||||
{
|
||||
name: 'linkedin',
|
||||
icon: 'i-ph-linkedin-logo-bold',
|
||||
link: 'https://www.linkedin.com/in/arthurdanjou/',
|
||||
},
|
||||
link: 'https://www.linkedin.com/in/arthurdanjou/'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -47,7 +47,10 @@ const socials = [
|
||||
The website is under maintenance
|
||||
</h1>
|
||||
<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 }}
|
||||
</p>
|
||||
<div>
|
||||
@@ -64,7 +67,11 @@ const socials = [
|
||||
class="link"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,32 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Category, Suggestion, Talent } from '@prisma/client'
|
||||
import { useTalentsStore } from '~/store/talents'
|
||||
import { providers } from '~~/types'
|
||||
|
||||
useHead({
|
||||
title: 'Discover new talents • Arthur Danjou',
|
||||
title: 'Discover new talents • Arthur Danjou'
|
||||
})
|
||||
|
||||
const categories = ref<Array<{ label: string, slug: string, id: number }>>([{ label: 'All', slug: 'all', id: 0 }])
|
||||
const { getCategory, setCategory, isFavorite, toggleFavorite } = useTalentsStore()
|
||||
const { loggedIn, clear } = useUserSession()
|
||||
|
||||
const {
|
||||
data: talents,
|
||||
pending,
|
||||
} = await useFetch<Array<Talent & { categories: Array<{ category: Category }> }>>('/api/talents', {
|
||||
const { data: talents, pending } = await useFetch('/api/talents', {
|
||||
method: 'get',
|
||||
query: {
|
||||
favorite: isFavorite,
|
||||
category: getCategory,
|
||||
category: getCategory
|
||||
},
|
||||
watch: [isFavorite, getCategory],
|
||||
watch: [isFavorite, getCategory]
|
||||
})
|
||||
|
||||
const {
|
||||
data: getCategories,
|
||||
} = await useFetch<Array<Category>>('/api/categories', { method: 'GET', query: { type: 'TALENT' } })
|
||||
getCategories.value!.forEach((category: any) => categories.value.push({ label: category.name, slug: category.slug, id: category.id }))
|
||||
const { data: getCategories } = await useFetch('/api/categories', { method: 'GET', query: { type: 'talent' } })
|
||||
getCategories.value!.forEach(category => categories.value.push({
|
||||
label: category.name,
|
||||
slug: category.slug,
|
||||
id: category.id
|
||||
}))
|
||||
|
||||
function isCategory(slug: string) {
|
||||
return getCategory.value === slug
|
||||
@@ -35,10 +33,10 @@ function isCategory(slug: string) {
|
||||
const getMarkerStyle = computed(() => {
|
||||
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
|
||||
return {
|
||||
top: `${selected?.offsetTop}px` || '0px',
|
||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px` || '4px',
|
||||
height: `${selected?.offsetHeight}px` || '0px',
|
||||
width: `${selected?.offsetWidth}px` || '0px',
|
||||
top: `${selected?.offsetTop}px`,
|
||||
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
|
||||
height: `${selected?.offsetHeight}px`,
|
||||
width: `${selected?.offsetWidth}px`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -55,21 +53,22 @@ async function suggest() {
|
||||
return
|
||||
|
||||
isOpen.value = false
|
||||
await $fetch<Suggestion>('/api/suggestion', {
|
||||
await $fetch('/api/suggestion', {
|
||||
method: 'post',
|
||||
body: {
|
||||
content: suggestContent.value,
|
||||
},
|
||||
}).then((suggestion) => {
|
||||
content: suggestContent.value
|
||||
}
|
||||
}).then((response) => {
|
||||
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',
|
||||
timeout: 4000,
|
||||
timeout: 4000
|
||||
})
|
||||
}).catch(() => {
|
||||
toast.add({
|
||||
title: 'You already have suggested someone',
|
||||
color: 'red',
|
||||
title: 'An error occurred when suggesting someone',
|
||||
color: 'red'
|
||||
})
|
||||
})
|
||||
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?
|
||||
</h1>
|
||||
</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>
|
||||
</template>
|
||||
<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">
|
||||
<input
|
||||
v-model="suggestContent"
|
||||
@@ -132,7 +140,10 @@ async function suggest() {
|
||||
Logout
|
||||
</UButton>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 justify-center">
|
||||
<div
|
||||
v-else
|
||||
class="flex gap-2 justify-center"
|
||||
>
|
||||
<UButton
|
||||
v-for="provider in providers"
|
||||
:key="provider.slug"
|
||||
@@ -141,14 +152,17 @@ async function suggest() {
|
||||
variant="solid"
|
||||
:to="provider.link"
|
||||
:icon="provider.icon"
|
||||
external
|
||||
:external="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</UModal>
|
||||
<div class="sticky z-40 top-[4.55rem] left-0 z-100 bg-white dark:bg-gray-900 pt-2">
|
||||
<div v-if="getCategories" class="flex gap-2 w-full items-center justify-between">
|
||||
<div
|
||||
v-if="getCategories"
|
||||
class="flex gap-2 w-full items-center justify-between"
|
||||
>
|
||||
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
|
||||
<ClientOnly>
|
||||
<div
|
||||
@@ -183,8 +197,14 @@ async function suggest() {
|
||||
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
|
||||
@click.prevent="toggleFavorite()"
|
||||
>
|
||||
<UIcon v-if="isFavorite" name="i-material-symbols-check-box-outline-rounded" />
|
||||
<UIcon v-else name="i-material-symbols-check-box-outline-blank" />
|
||||
<UIcon
|
||||
v-if="isFavorite"
|
||||
name="i-material-symbols-check-box-outline-rounded"
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
name="i-material-symbols-check-box-outline-blank"
|
||||
/>
|
||||
<p>Show favorites only</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -192,8 +212,14 @@ async function suggest() {
|
||||
</div>
|
||||
<UDivider class="my-2" />
|
||||
</div>
|
||||
<div v-if="talents && getCategories" class="mt-8">
|
||||
<div v-if="talents.length > 0 && !pending" class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-if="talents && getCategories"
|
||||
class="mt-8"
|
||||
>
|
||||
<div
|
||||
v-if="talents.length > 0 && !pending"
|
||||
class="grid grid-cols-1 gap-y-4 md:gap-x-12 md:gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="talent in talents"
|
||||
:key="talent.name.toLowerCase().trim()"
|
||||
@@ -201,18 +227,31 @@ async function suggest() {
|
||||
>
|
||||
<div class="flex">
|
||||
<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>
|
||||
<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" />
|
||||
<NuxtLink :to="talent.website" target="_blank">
|
||||
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<NuxtLink
|
||||
:to="talent.website"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="absolute -inset-y-2 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<div class="flex gap-2 items-center">
|
||||
<h1 class="relative z-10">
|
||||
{{ talent.name }}
|
||||
</h1>
|
||||
<UTooltip v-if="talent.favorite" text="You can set the filter to only show favorites.">
|
||||
<UIcon name="i-ic-round-star" class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300" />
|
||||
<UTooltip
|
||||
v-if="talent.favorite"
|
||||
text="You can set the filter to only show favorites."
|
||||
>
|
||||
<UIcon
|
||||
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
|
||||
name="i-ic-round-star"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
@@ -224,13 +263,16 @@ async function suggest() {
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<span class="ml-2">{{ talent.website.replace('https://', '').replace('/', '') }}</span>
|
||||
</p>
|
||||
<div class="flex gap-2 z-10 flex-wrap">
|
||||
<UBadge
|
||||
v-for="category in talent.categories"
|
||||
v-for="category in talent.talentCategories"
|
||||
:key="category.category.slug"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
@@ -242,13 +284,19 @@ async function suggest() {
|
||||
</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">
|
||||
<UIcon name="i-akar-icons-cross" />
|
||||
<p>There are no talents for this category. Maybe soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="my-4 text-subtitle">
|
||||
<div
|
||||
v-else
|
||||
class="my-4 text-subtitle"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<UIcon name="i-eos-icons-loading" />
|
||||
<p>The talents are loading...</p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: 'My work • Arthur Danjou',
|
||||
title: 'My work • Arthur Danjou'
|
||||
})
|
||||
const { data: projects } = await getProjects()
|
||||
</script>
|
||||
@@ -24,12 +24,19 @@ const { data: projects } = await getProjects()
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
||||
<UIcon :name="project.icon" size="24" dynamic />
|
||||
<UIcon
|
||||
:name="project.icon"
|
||||
dynamic
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||
<div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<NuxtLink :to="project.link" target="_blank">
|
||||
<NuxtLink
|
||||
:to="project.link"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<span class="relative z-10">{{ project.title }}</span>
|
||||
</NuxtLink>
|
||||
@@ -7,12 +7,12 @@ const appConfig = useAppConfig()
|
||||
const route = useRoute()
|
||||
const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne())
|
||||
const {
|
||||
data: post,
|
||||
data: post
|
||||
} = await useFetch<PrismaPost>('/api/article', {
|
||||
method: 'post',
|
||||
body: {
|
||||
slug: route.params.slug.toString(),
|
||||
},
|
||||
slug: route.params.slug.toString()
|
||||
}
|
||||
})
|
||||
|
||||
const likes = ref(post.value?.likes)
|
||||
@@ -20,8 +20,8 @@ async function like() {
|
||||
const data = await $fetch<PrismaPost>('/api/like', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
slug: post.value?.slug,
|
||||
},
|
||||
slug: post.value?.slug
|
||||
}
|
||||
})
|
||||
likes.value = data.likes
|
||||
}
|
||||
@@ -29,30 +29,30 @@ async function like() {
|
||||
if (!postContent.value) {
|
||||
throw showError({
|
||||
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('"', '')
|
||||
useHead({
|
||||
title: `${postContent.value?.title} • Arthur Danjou's shelf`,
|
||||
title: `${postContent.value?.title} • Arthur Danjou's shelf`
|
||||
})
|
||||
|
||||
function top() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
const { copy, copied } = useClipboard({
|
||||
source: `https://arthurdanjou.fr/writing/${route.params.slug}`,
|
||||
copiedDuring: 4000,
|
||||
copiedDuring: 4000
|
||||
})
|
||||
|
||||
const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, {
|
||||
maxAge: 604_800,
|
||||
maxAge: 604_800
|
||||
})
|
||||
|
||||
async function handleLike() {
|
||||
@@ -62,7 +62,10 @@ async function handleLike() {
|
||||
</script>
|
||||
|
||||
<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="max-w-3xl space-y-8 mx-auto">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
@@ -95,7 +98,10 @@ async function handleLike() {
|
||||
{{ postContent.description }}
|
||||
</p>
|
||||
</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 }}
|
||||
</div>
|
||||
<ClientOnly>
|
||||
@@ -3,7 +3,7 @@ const appConfig = useAppConfig()
|
||||
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
|
||||
|
||||
useHead({
|
||||
title: 'My Shelf • Arthur Danjou',
|
||||
title: 'My Shelf • Arthur Danjou'
|
||||
})
|
||||
|
||||
const { data: posts } = await getPosts()
|
||||
@@ -25,7 +25,11 @@ const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replac
|
||||
<div class="mt-16 md:mt-20">
|
||||
<div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
|
||||
<div class="flex max-w-3xl flex-col space-y-16">
|
||||
<article v-for="post in posts" :key="post.slug" class="px-6 md:grid md:grid-cols-4 md:items-baseline">
|
||||
<article
|
||||
v-for="post in posts"
|
||||
:key="post.slug"
|
||||
class="px-6 md:grid md:grid-cols-4 md:items-baseline"
|
||||
>
|
||||
<div class="group md:col-span-3 group relative flex flex-col items-start">
|
||||
<h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
<div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" />
|
||||
@@ -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">
|
||||
{{ post.description }}
|
||||
</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>
|
||||
<UIcon name="i-ph-arrow-circle-right-bold" />
|
||||
</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) => {
|
||||
const { slug } = await readValidatedBody(event, PostSchema)
|
||||
return await usePrisma().post.update({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
data: {
|
||||
likes: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
return useDB().update(tables.posts)
|
||||
.set({
|
||||
likes: sql`${tables.posts.likes}
|
||||
+ 1`
|
||||
})
|
||||
.where(eq(tables.posts.slug, slug))
|
||||
})
|
||||
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'
|
||||
|
||||
const MessageValidator = z.object({
|
||||
id: z.number(),
|
||||
id: z.number()
|
||||
}).parse
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -9,10 +9,5 @@ export default defineEventHandler(async (event) => {
|
||||
const { user } = await requireUserSession(event)
|
||||
if (!user.admin)
|
||||
throw createError({ statusCode: 400, message: 'You need the permission to delete a message!' })
|
||||
|
||||
return await usePrisma().guestbookMessage.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
return useDB().delete(tables.guestbookMessages).where(eq(tables.guestbookMessages.id, id))
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const MessageValidator = z.object({
|
||||
message: z.string(),
|
||||
message: z.string()
|
||||
}).parse
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -11,22 +11,20 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
await sendDiscordWebhookMessage(config, {
|
||||
title: 'New guestbook message ✨',
|
||||
description: `**${user.username}** as signed the book : "*${message}*"`,
|
||||
color: 15893567,
|
||||
description: `**${user.username}** has signed the book : "*${message}*"`,
|
||||
color: 15893567
|
||||
})
|
||||
|
||||
return await usePrisma().guestbookMessage.upsert({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
update: {
|
||||
return useDB().insert(tables.guestbookMessages)
|
||||
.values({
|
||||
message,
|
||||
},
|
||||
create: {
|
||||
email: user.email,
|
||||
image: user.picture,
|
||||
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,
|
||||
editors,
|
||||
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) => {
|
||||
const { slug } = await readValidatedBody(event, PostSchema)
|
||||
return await usePrisma().post.update({
|
||||
where: {
|
||||
slug,
|
||||
},
|
||||
data: {
|
||||
views: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
return useDB().update(tables.posts)
|
||||
.set({
|
||||
views: sql`${tables.posts.views}
|
||||
+ 1`
|
||||
})
|
||||
.where(eq(tables.posts.slug, slug))
|
||||
})
|
||||
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({
|
||||
config: {
|
||||
emailRequired: true,
|
||||
emailRequired: true
|
||||
},
|
||||
async onSuccess(event: any, { user }: any) {
|
||||
async onSuccess(event, {user}) {
|
||||
await setUserSession(event, {
|
||||
user: {
|
||||
email: user.email,
|
||||
picture: user.avatar_url,
|
||||
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') || '/')
|
||||
},
|
||||
onError(error: any) {
|
||||
onError(error) {
|
||||
console.error('GitHub OAuth error:', error)
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,17 +1,16 @@
|
||||
export default oauth.googleEventHandler({
|
||||
async onSuccess(event: any, { user }: any) {
|
||||
async onSuccess(event, {user}) {
|
||||
await setUserSession(event, {
|
||||
user: {
|
||||
email: user.email,
|
||||
picture: user.picture,
|
||||
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') || '/')
|
||||
},
|
||||
onError(error: any) {
|
||||
onError(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,
|
||||
url: 'https://arthurdanjou.fr/talents',
|
||||
footer: {
|
||||
text: 'Powered by Nuxt',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
text: 'Powered by Nuxt'
|
||||
},
|
||||
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