Import drizzle replacing prisma

Signed-off-by: Arthur DANJOU <arthurdanjou@outlook.fr>
This commit is contained in:
2024-04-20 00:03:10 +02:00
parent a7f0a635ec
commit c6ba8c791b
108 changed files with 2367 additions and 1554 deletions

108
pages/about.vue Normal file
View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
useHead({
title: 'About me • Arthur Danjou'
})
const { data: skills } = await getSkills()
const { data: educations } = await getEducations()
const { data: experiences } = await getWorkExperiences()
</script>
<template>
<section class="w-container lg:my-24 my-16">
<div class="px-4 grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div class="lg:pl-20 flex justify-center">
<div class="max-w-xs px-2.5 lg:max-w-none">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="It's me 👋"
>
<img
alt="My main profile picture"
class="border dark:border-0 aspect-square rotate-3 hover:rotate-0 duration-300 transition-transform rounded-3xl bg-zinc-100 object-cover dark:bg-zinc-800"
src="/about.png"
>
</UTooltip>
</div>
</div>
<div class="lg:order-first lg:row-span-2">
<div class="max-w-2xl space-y-8 mb-16">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
I'm Arthur, I live and study in France where I learn new things.
</h1>
<p class="leading-relaxed text-subtitle">
As a software engineer with a passion for AI and the cloud, I have a deep understanding of emerging technologies that are transforming the way businesses and organizations operate. I am at the heart of an ever-changing and rapidly growing field. My background in mathematics also gives me an edge in understanding the mathematical concepts and theories behind these technologies as well as how to design them.
</p>
<p class="leading-relaxed text-subtitle">
I enjoy sharing my knowledge and learning new theorems and technologies. I am a curious person and eager to continue learning and growing throughout your life. My passion and commitment to these subjects are admirable qualities and will help me succeed in my career and education.
</p>
</div>
</div>
</div>
<GridSection title="Interests">
<GridSlot title="Development">
Development is the passion that appeared the earliest in my life. I started developing on Minecraft and then I migrated to the broad field of the web.
</GridSlot>
<GridSlot title="Mathematics">
During my studies, I loved mathematics very quickly. That's why today I continue my studies in this fabulous field.
</GridSlot>
<GridSlot title="Artificial Intelligence">
We hear more and more about artificial intelligence with the evolution of our society. So I quickly got interested by doing my own research and I quickly discovered that this field is closely related to mathematics, hence my interest.
</GridSlot>
<GridSlot title="Cloud and infrastructure">
When you're doing development and deploying projects online, you discover and are forced to touch the cloud, infrastructure, and network. It's a totally different field than the others but just as interesting.
</GridSlot>
<GridSlot title="Fitness">
In addition to my studies and programming, I go to the gym every day to relax and stay in shape. Sport allows me to recharge my batteries and move on to other things.
</GridSlot>
</GridSection>
<GridSection
v-if="skills"
title="Skills"
>
<div class="grid grid-cols-3 md:grid-cols-4 gap-2">
<Skill
v-for="skill in skills.body"
:key="skill.name"
:skill="skill"
/>
</div>
</GridSection>
<GridSection
v-if="experiences"
title="Work Experiences"
>
<Experience
v-for="experience in experiences"
:key="experience.title"
:experience="experience"
/>
</GridSection>
<GridSection
v-if="educations"
title="Educations"
>
<Education
v-for="education in educations"
:key="education.title"
:education="education"
/>
</GridSection>
<div class="flex justify-center">
<UTooltip
:popper="{ offsetDistance: 20 }"
text="Click to discover my journey"
>
<UButton
label="Download my CV"
icon="i-material-symbols-lab-profile-outline-rounded"
color="primary"
variant="outline"
size="xl"
to="/resume.pdf"
target="_blank"
/>
</UTooltip>
</div>
</section>
</template>

186
pages/bookmarks.vue Normal file
View File

@@ -0,0 +1,186 @@
<script lang="ts" setup>
import { useBookmarksStore } from '~/store/bookmarks'
useHead({
title: 'Discover my library • Arthur Danjou'
})
const categories = ref<Array<{ label: string, slug: string }>>([{ label: 'All', slug: 'all' }])
const { getCategory, setCategory, isFavorite, toggleFavorite } = useBookmarksStore()
const { data: bookmarks, pending } = await useFetch('/api/bookmarks', {
method: 'get',
query: {
favorite: isFavorite,
category: getCategory
},
watch: [isFavorite, getCategory]
})
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
}
const getMarkerStyle = computed(() => {
const selected = window.document.getElementById(categories.value.find(category => category.slug === getCategory.value)?.slug || 'all')
return {
top: `${selected?.offsetTop}px`,
left: `${selected?.offsetLeft === 12 ? 4 : selected?.offsetLeft}px`,
height: `${selected?.offsetHeight}px`,
width: `${selected?.offsetWidth}px`
}
})
const appConfig = useAppConfig()
function getColor() {
return `text-${appConfig.ui.primary}-500`
}
</script>
<template>
<section class="w-container lg:my-24 my-16">
<div class="max-w-2xl space-y-8 mb-16">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
My library where I save some resources
</h1>
<p class="leading-relaxed text-subtitle">
You will find a selection of some of the most inspiring and complete content I have read through my research and work experience.
</p>
</div>
<div
v-if="getCategories"
class="sticky z-40 top-[4.8rem] left-0 z-100 flex gap-2 w-full items-center justify-between"
>
<div class="flex gap-2 overflow-x-scroll sm:overflow-x-hidden bg-gray-100 dark:bg-gray-800 rounded-lg p-1 relative">
<ClientOnly>
<div
class="absolute duration-300 left-1 ease-out focus:outline-none"
:style="[getMarkerStyle]"
>
<div class="w-full h-full bg-white dark:bg-gray-900 rounded-md shadow-sm" />
</div>
</ClientOnly>
<div
v-for="category in categories"
:id="category.slug"
:key="category.slug"
class="relative px-3 py-1 text-sm font-medium rounded-md h-8 text-gray-500 dark:text-gray-400 min-w-fit flex items-center justify-center w-full focus:outline-none transition-colors duration-200 ease-out cursor-pointer hover:text-black dark:hover:text-white"
:class="{ 'text-gray-900 dark:text-white relative': isCategory(category.slug) }"
@click.prevent="setCategory(category.slug)"
>
<p class="w-full">
{{ category.label }}
</p>
</div>
</div>
<UPopover>
<UButton
:icon="isFavorite ? 'i-mdi-filter-variant-remove' : 'i-mdi-filter-variant'"
color="primary"
variant="soft"
size="lg"
/>
<template #panel>
<div
class="flex p-2 gap-2 items-center cursor-pointer select-none text-subtitle"
@click.prevent="toggleFavorite()"
>
<UIcon
v-if="isFavorite"
name="i-material-symbols-check-box-outline-rounded"
/>
<UIcon
v-else
name="i-material-symbols-check-box-outline-blank"
/>
<p>Show favorites only</p>
</div>
</template>
</UPopover>
</div>
<UDivider class="my-2" />
<div
v-if="bookmarks && getCategories"
class="mt-8"
>
<div
v-if="bookmarks.length > 0 && !pending"
class="grid grid-cols-1 gap-4 md:gap-x-16 md:gap-y-12 sm:grid-cols-2 md:grid-cols-3"
>
<div
v-for="bookmark in bookmarks"
:key="bookmark.name.toLowerCase().trim()"
class="group relative flex justify-between items-center"
>
<div class="flex flex-col gap-y-1">
<div class="flex gap-6 items-center">
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<NuxtLink
:href="bookmark.website"
external
target="_blank"
>
<span class="absolute -inset-y-2 md:-inset-y-4 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<div class="flex gap-2 items-center">
<h1 class="relative z-10">
{{ bookmark.name }}
</h1>
<UTooltip
v-if="bookmark.favorite"
text="You can set the filter to only show favorites."
>
<UIcon
class="z-20 text-amber-500 text-xl font-bold hover:rotate-[143deg] duration-300"
name="i-ic-round-star"
/>
</UTooltip>
</div>
</NuxtLink>
</h2>
</div>
<div class="flex gap-2 z-10">
<UBadge
v-for="category in bookmark.bookmarkCategories"
:key="category.category.slug"
color="primary"
variant="soft"
size="xs"
>
{{ category.category.name }}
</UBadge>
</div>
</div>
<p
:class="getColor()"
class="relative z-10 flex text-sm font-medium items-center"
>
<UIcon name="i-ph-link-bold" />
<span class="ml-2">{{ bookmark.website.replace('https://', '').replace('www.', '').replace('/', '') }}</span>
</p>
</div>
</div>
<div
v-else-if="bookmarks?.length === 0 && !pending"
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-akar-icons-cross" />
<p>There are no bookmarks for this category. Maybe soon...</p>
</div>
</div>
<div
v-else
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" />
<p>The bookmarks are loading...</p>
</div>
</div>
</div>
</section>
</template>

210
pages/guestbook.vue Normal file
View File

@@ -0,0 +1,210 @@
<script lang="ts" setup>
import { providers } from '~~/types'
useHead({
title: 'Sign my guestbook • Arthur Danjou'
})
const { loggedIn, clear, user } = useUserSession()
const { data: messages, refresh } = useFetch('/api/messages', { method: 'get' })
const isOpen = ref(false)
const toast = useToast()
const messageContent = ref<string>('')
async function sign() {
if (messageContent.value.length < 7 || messageContent.value.length > 250)
return
isOpen.value = false
await $fetch('/api/message', {
method: 'post',
body: {
message: messageContent.value
}
}).then(async () => {
toast.add({
title: `Thanks for leaving a message!`,
description: 'Your can see it at the top of the messages.',
icon: 'i-material-symbols-check-circle-outline-rounded',
timeout: 4000
})
await refresh()
}).catch(() => {
toast.add({
title: 'An error occurred when signing the book!',
color: 'red'
})
})
messageContent.value = ''
}
async function deleteMessage(id: number) {
if (!user.value.admin)
return
await $fetch('/api/message', {
method: 'delete',
body: {
id
}
}).then(async () => {
toast.add({
title: `Message successfully deleted`,
icon: 'i-material-symbols-check-circle-outline-rounded',
color: 'green',
timeout: 4000
})
await refresh()
}).catch(() => {
toast.add({
title: 'An error occured when deleting a message!',
color: 'red'
})
})
}
</script>
<template>
<section class="w-container lg:mt-24 my-8">
<div class="max-w-2xl space-y-8 md:mb-16 mb-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
You want to leave a message ?
</h1>
<p class="leading-relaxed text-subtitle">
Your opinion means a lot to me. Feel free to share your impressions of my projects, explore my site, or simply leave a personalised message. Your comments are a source of inspiration and continuous improvement. Thank you for taking the time to contribute to this virtual community. I look forward to reading what you have to share!
</p>
</div>
<div class="flex justify-center md:justify-start">
<UButton
class="mb-8 md:mb-16"
label="Want to sign my book ?"
icon="i-ph-circle-wavy-question-bold"
@click.prevent="isOpen = true"
/>
</div>
<UModal v-model="isOpen">
<UCard>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h1
v-if="loggedIn"
class="text-md font-bold"
>
Enter just below your message to sign my book
</h1>
<h1
v-else
class="text-md font-bold"
>
Sign before writing your message
</h1>
</div>
<UButton
class="-my-1"
color="gray"
icon="i-heroicons-x-mark-20-solid"
variant="ghost"
@click="isOpen = false"
/>
</div>
</template>
<div>
<div
v-if="loggedIn"
class="flex items-center justify-between gap-4"
>
<div class="w-full relative flex items-center">
<input
v-model="messageContent"
type="text"
required
min="7"
max="58"
class="w-full rounded-lg p-2 h-10 focus:outline-none bg-gray-100 dark:bg-gray-800"
placeholder="Leave a message"
>
<UButton
class="absolute right-1 top-1 rounded-md"
label="Send"
:disabled="messageContent.trim().length < 7 || messageContent.trim().length > 250"
variant="solid"
@click.prevent="sign()"
/>
</div>
<UButton
variant="outline"
@click.prevent="clear()"
>
Logout
</UButton>
</div>
<div
v-else
class="flex gap-2 justify-center"
>
<UButton
v-for="provider in providers"
:key="provider.slug"
:label="provider.label"
color="black"
variant="solid"
:to="provider.link"
:icon="provider.icon"
:external="true"
/>
</div>
</div>
</UCard>
</UModal>
<div
v-if="messages"
class="columns-1 md:columns-2 lg:columns-4 gap-8 space-y-8"
>
<div
v-for="message in messages"
:key="message.id"
class="relative overflow-hidden sm:p-6 px-4 py-5 border border-zinc-100 p-6 dark:border-zinc-700/40 rounded-lg"
>
<p class="text-sm text-subtitle">
{{ message.message }}
</p>
<div class="flex items-center gap-4 mt-4">
<div class="h-8 w-8 rounded-full">
<img
:src="message.image"
alt="Author profile picture"
class="w-full h-full rounded-full"
>
</div>
<p class="font-bold">
{{ message.username }}
</p>
</div>
<UButton
v-if="user && user.admin"
class="absolute top-1 right-1"
icon="i-material-symbols-delete-forever-outline-rounded"
color="red"
variant="ghost"
:ui="{ rounded: 'rounded-full' }"
size="xs"
@click.prevent="deleteMessage(message.id)"
/>
</div>
</div>
<div
v-else
class="my-4 text-subtitle"
>
<div class="flex gap-2 items-center">
<UIcon name="i-eos-icons-loading" />
<p>The messages are loading...</p>
</div>
</div>
</section>
</template>
<style>
</style>

16
pages/index.vue Normal file
View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
useHead({
title: 'Arthur Danjou • Software Engineer and Maths Lover'
})
</script>
<template>
<section>
<Announcement />
<MainBanner />
<div class="px-4 lg:px-44 md:px-16 sm:px-8 w-full my-16 grid grid-cols-1 md:grid-cols-2 md:gap-x-16 gap-y-4 md:gap-y-16">
<MainActivity />
<MainStats />
</div>
</section>
</template>

85
pages/maintenance.vue Normal file
View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
definePageMeta({
layout: 'maintenance'
})
useHead({
title: 'Site under maintenance • Arthur Danjou'
})
const { data: maintenance } = await useFetch('/api/maintenance')
const format = 'DD MMMM YYYY, HH:mm'
const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
const socials = [
{
name: 'mail',
icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr'
},
{
name: 'twitter',
icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj'
},
{
name: 'github',
icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou'
},
{
name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/'
}
]
</script>
<template>
<section class="w-full min-h-[80svh] flex justify-center items-center">
<div class="text-center space-y-8 max-w-5xl">
<h3 class="uppercase text-xs text-transparent bg-clip-text bg-origin-content bg-gradient-to-b from-gray-100 to-gray-300 dark:from-zinc-600 to-55% dark:to-zinc-800">
Coming back soon
</h3>
<h1 class="text-4xl md:text-7xl font-bold">
The website is under maintenance
</h1>
<div v-if="maintenance && maintenance.maintenance">
<p
:class="getColor"
class="font-bold mb-8 text-xl"
>
{{ maintenance.maintenance.reason }}
</p>
<div>
<p class="text-subtitle italic">
Maintenance planned from {{ useDateFormat(maintenance.maintenance.beginAt, format).value }} to {{ useDateFormat(maintenance.maintenance.endAt, format).value }}
</p>
</div>
</div>
<div class="flex justify-center items-center gap-4">
<a
v-for="social in socials"
:key="social.name"
:href="social.link"
class="link"
target="_blank"
>
<span
:class="social.icon"
aria-hidden="true"
class="flex-shrink-0 h-5 w-5"
/>
</a>
</div>
</div>
</section>
</template>
<style scoped>
.link {
@apply cursor-pointer duration-300 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 font-medium rounded-full text-sm gap-x-2.5 p-2.5 text-primary-500 dark:text-primary-400 hover:bg-primary-50 disabled:bg-transparent dark:hover:bg-primary-950 dark:disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center;
}
</style>

307
pages/talents.vue Normal file
View File

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

21
pages/uses.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<section class="w-container lg:my-24 my-16">
<div class="max-w-2xl space-y-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
Software I use, Hardware I own, and my favorite stack
</h1>
<p class="leading-relaxed text-subtitle">
I get often asked what I use to create software, to play games or to work and learn. Here's a big list of all my favourite things.
</p>
<ClientOnly>
<ContentDoc
class="my-16"
path="/uses"
/>
<template #fallback>
<USkeleton class="w-full h-1/2" />
</template>
</ClientOnly>
</div>
</section>
</template>

64
pages/work.vue Normal file
View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
useHead({
title: 'My work • Arthur Danjou'
})
const { data: projects } = await getProjects()
</script>
<template>
<section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8">
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
All my projects can be found on GitHub and by scrolling down.
</h1>
<p class="leading-relaxed text-subtitle">
I've worked on tons of little projects over the years but these are the ones that I'm most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved.
</p>
</div>
<div class="mt-16 md:mt-20">
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="project in projects"
:key="project.name"
class="group relative flex flex-col justify-between"
>
<div class="flex items-start gap-4">
<div class="relative z-10 flex p-2 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<UIcon
:name="project.icon"
dynamic
size="24"
/>
</div>
<div>
<h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100">
<div class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<NuxtLink
:to="project.link"
target="_blank"
>
<span class="absolute -inset-y-4 md:-inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10">{{ project.title }}</span>
</NuxtLink>
</h2>
<p class="relative z-10 text-sm text-zinc-600 dark:text-zinc-400">
{{ project.description }}
</p>
</div>
</div>
<div class="mt-2 flex gap-2 z-10 flex-wrap">
<UBadge
v-for="tag in project.tags"
:key="tag"
color="primary"
variant="soft"
size="xs"
>
{{ tag }}
</UBadge>
</div>
</div>
</div>
</div>
</section>
</template>

179
pages/writing/[slug].vue Normal file
View File

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

69
pages/writing/index.vue Normal file
View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
useHead({
title: 'My Shelf • Arthur Danjou'
})
const { data: posts } = await getPosts()
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
</script>
<template>
<section class="w-container lg:my-24 my-16">
<div class="px-4 max-w-3xl space-y-8">
<div>
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl !leading-tight">
Writing on my life, development and my passions.
</h1>
<p class="leading-relaxed text-subtitle">
All my thoughts on programming, mathematics, artificial intelligence design, etc., are put together in chronological order. I also write about my projects, my discoveries, and my thoughts. <s>It is sometimes updated.</s>
</p>
</div>
</div>
<div class="mt-16 md:mt-20">
<div class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div class="flex max-w-3xl flex-col space-y-16">
<article
v-for="post in posts"
:key="post.slug"
class="px-6 md:grid md:grid-cols-4 md:items-baseline"
>
<div class="group md:col-span-3 group relative flex flex-col items-start">
<h2 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
<div class="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 md:rounded-2xl" />
<NuxtLink :to="post._path">
<span class="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span class="relative z-10">
{{ post.title }}
</span>
</NuxtLink>
</h2>
<time class="md:hidden relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500 pl-3.5">
<span class="absolute inset-y-0 left-0 flex items-center">
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span>
{{ format(post.publishedAt) }}
</time>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{{ post.description }}
</p>
<div
:class="getColor"
class="relative z-10 mt-4 flex items-center gap-2 justify-center text-sm font-medium"
>
<p>Read article</p>
<UIcon name="i-ph-arrow-circle-right-bold" />
</div>
</div>
<time class="mt-1 md:block relative z-10 order-first mb-3 hidden text-sm text-zinc-400 dark:text-zinc-500">
<p>{{ format(post.publishedAt) }}</p>
<p>{{ post.readingMins }} min.</p>
</time>
</article>
</div>
</div>
</div>
</section>
</template>