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

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
const { data: announce } = await useFetch('/api/announcement')
const appConfig = useAppConfig()
function getColor() {
return `bg-${appConfig.ui.primary}-500`
}
</script>
<template>
<div v-if="announce" class="w-container flex justify-center mt-8">
<div class="relative">
<h1 class="px-4 py-2 bg-white dark:bg-zinc-900 rounded-md border border-zinc-100 dark:border-zinc-300/10" v-html="announce.content" />
<span class="absolute -top-0.5 -right-0.5 flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75" :class="getColor()" />
<span class="relative inline-flex rounded-full h-2 w-2" :class="getColor()" />
</span>
</div>
</div>
</template>
<style>
</style>

45
components/Background.vue Normal file
View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
const points = useState(() => Array.from({ length: 25 }).fill(0).map(() => [Math.random(), Math.random()]))
const poly = computed(() => points.value.map(([x, y]) => `${x * 100}% ${y * 100}%`).join(', '))
function jumpVal(val: number) {
return Math.random() > 0.5 ? val + (Math.random() - 0.5) / 2 : Math.random()
}
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])]
timeout = setTimeout(jumpPoints, Math.random() * 1000)
}
onMounted(() => jumpPoints())
onUnmounted(() => clearTimeout(timeout))
</script>
<template>
<ClientOnly>
<div
aria-hidden="true"
class="bg sm:mx-8 absolute inset-0 z-20 transform-gpu blur-3xl overflow-hidden"
>
<div
class="aspect-[2] h-2/3 w-full bg-gradient-to-r from-[rgb(var(--color-primary-DEFAULT))] to-white/10 lg:opacity-30 xs:opacity-50"
:style="{ 'clip-path': `polygon(${poly})` }"
/>
</div>
</ClientOnly>
</template>
<style scoped>
.bg > div {
clip-path: circle(75%);
transition: clip-path 3s;
}
.light .bg > div {
opacity: 1 !important;
}
</style>

39
components/Footer.vue Normal file
View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
const year = computed(() => new Date().getFullYear())
</script>
<template>
<footer class="w-full flex justify-center">
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 flex justify-between bg-white dark:bg-zinc-900 border-t border-zinc-100 dark:border-zinc-300/10">
<div class="w-full duration-300 text-center flex flex-col md:flex-row md:justify-between items-center gap-y-2">
<p class="text-subtitle text-sm">
© {{ year }} ArtDanjProduction
</p>
<div class="flex items-center">
<p class="text-subtitle">
Designed & Built by
</p>
<UButton
color="primary"
label="Arthur Danjou"
target="_blank"
to="https://twitter.com/arthurdanj"
variant="link"
/>
</div>
<p class="text-subtitle flex items-center">
Made with
<UButton
color="green"
icon="i-vscode-icons-file-type-nuxt"
label="Nuxt 3"
target="_blank"
to="https://nuxt.com/"
trailing
variant="link"
/>
</p>
</div>
</div>
</footer>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
defineProps({
title: {
type: String,
default: 'Uses Section title'
}
})
const appConfig = useAppConfig()
const getColor = computed(() => `text-${appConfig.ui.primary}-500`)
</script>
<template>
<section class="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40 mb-24 px-4">
<div class="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
<h2
:class="getColor"
class="relative text-sm font-semibold pl-3.5"
>
<span class="md:hidden absolute inset-y-0 left-0 flex items-center">
<span class="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
</span>
{{ title }}
</h2>
<div class="md:col-span-3">
<ul class="space-y-8">
<slot />
</ul>
</div>
</div>
</section>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
defineProps({
title: {
type: String,
default: 'Uses Slot title'
}
})
</script>
<template>
<li class="group relative flex flex-col items-start">
<h3 class="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{{ title }}
</h3>
<p class="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
<slot />
</p>
</li>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
defineProps({
href: {
type: String,
default: ''
},
target: {
type: String,
default: undefined,
required: false
}
})
const appConfig = useAppConfig()
</script>
<template>
<NuxtLink
:href="href"
:target="target"
class="border-b border-zinc-200 dark:border-zinc-700/70 duration-300"
:class="`hover:border-${appConfig.ui.primary}-500 dark:hover:border-${appConfig.ui.primary}-500`"
>
<slot />
</NuxtLink>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { computed, useRuntimeConfig } from '#imports'
const props = defineProps<{ id?: string }>()
const { headings } = useRuntimeConfig().public.mdc
const generate = computed(() => props.id && headings?.anchorLinks?.h2)
</script>
<template>
<h2 :id="id">
<a
v-if="id && generate"
:href="`#${id}`"
class="pl-6 border-l border-zinc-200 dark:border-zinc-700/70 duration-300"
>
<slot />
</a>
<slot v-else />
</h2>
</template>

View File

@@ -0,0 +1,14 @@
<template>
<header class="z-30 sticky top-0 left-0 flex justify-center w-full">
<div class="w-full px-4 sm:px-6 lg:px-8 sm:mx-8 max-w-9xl py-4 grid grid-cols-2 lg:grid-cols-3 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-300/10">
<Logo />
<div class="hidden grow lg:flex justify-center">
<NavBar />
</div>
<div class="flex justify-end gap-2 items-center">
<ThemePicker />
<MobileNavBar />
</div>
</div>
</header>
</template>

View 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>

View File

@@ -0,0 +1,179 @@
<script lang="ts" setup>
import { navs } from '~~/types'
const isOpenSidebar = ref(false)
const isOpenModal = ref(false)
const router = useRouter()
router.afterEach(() => isOpenSidebar.value = false)
const route = useRoute()
function isRoute(path: string) {
return route.path === path
}
function openModal() {
isOpenSidebar.value = false
isOpenModal.value = true
}
const { copy, copied } = useClipboard({ source: 'arthurdanjou@outlook.fr', copiedDuring: 3000 })
</script>
<template>
<div class="lg:hidden">
<div class="p-1 rounded-md bg-black/5 text-sm font-medium text-zinc-700 dark:bg-zinc-800/90 dark:text-zinc-300">
<UButton
variant="ghost"
color="primary"
size="sm"
icon="i-ph-list-bold"
@click="isOpenSidebar = true"
/>
</div>
<USlideover v-model="isOpenSidebar">
<UCard
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
class="flex flex-col flex-1"
>
<template #header>
<div class="flex justify-between items-center">
<Logo />
<UButton
size="md"
icon="i-ic-round-close"
:ui="{ rounded: 'rounded-full' }"
@click.prevent="isOpenSidebar = false"
/>
</div>
</template>
<div class="flex flex-col space-y-2">
<div
v-for="nav in navs"
:key="nav.label"
class="w-full"
>
<UButton
v-if="nav.to"
size="sm"
class="w-full"
:variant="isRoute(nav.to) ? 'solid' : 'ghost'"
color="primary"
:to="nav.to"
:icon="nav.icon"
:label="nav.label"
/>
<UButton
v-else
class="w-full"
size="sm"
color="primary"
variant="ghost"
:icon="nav.icon"
:label="nav.label"
@click.prevent="openModal()"
/>
</div>
</div>
<template #footer>
Footer
</template>
</UCard>
</USlideover>
<UModal v-model="isOpenModal">
<UCard class="p-4">
<div>
<div class="mb-8 flex justify-between items-center">
<h1 class="text-xl font-bold">
Contact me
</h1>
<UButton
icon="i-akar-icons-cross"
size="xs"
variant="ghost"
@click.prevent="isOpenModal = false"
/>
</div>
<div class="flex flex-col space-y-6">
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col">
<h3 class="text-sm">
Email
</h3>
<p class="text-xs text-subtitle">
arthurdanjou@outlook.fr
</p>
</div>
<div>
<UButtonGroup
orientation="horizontal"
size="sm"
>
<UButton
color="gray"
icon="i-mdi-note-edit-outline"
label="Compose"
to="mailto:arthurdanjou@outlook.fr"
variant="solid"
/>
<UButton
v-if="copied"
color="green"
icon="i-mdi-content-copy"
label="Copied"
variant="solid"
/>
<UButton
v-else
color="gray"
icon="i-mdi-content-copy"
label="Copy"
variant="solid"
@click.prevent="copy()"
/>
</UButtonGroup>
</div>
</div>
<UDivider label="OR" />
<div class="flex flex-col md:flex-row justify-between md:items-center space-y-2">
<div class="flex flex-col">
<h3 class="text-sm">
Get in touch
</h3>
<p class="text-xs text-subtitle">
I'm most active on Twitter
</p>
</div>
<div>
<UButtonGroup
orientation="horizontal"
size="sm"
>
<UButton
color="gray"
icon="i-ph-github-logo-bold"
label="Github"
target="_blank"
to="https://github.com/ArthurDanjou"
variant="solid"
/>
<UButton
color="gray"
icon="i-ph-twitter-logo-bold"
label="Twitter"
target="_blank"
to="https://twitter.com/ArthurDanj"
variant="solid"
/>
</UButtonGroup>
</div>
</div>
</div>
</div>
</UCard>
</UModal>
</div>
</template>

View 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>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { useColorStore } from '~/store/color'
import { ColorsTheme } from '~~/types'
const colors = Object.values(ColorsTheme)
const { getColor, setColor } = useColorStore()
const colorMode = useColorMode()
const isDark = ref(colorMode.value === 'dark')
watch(isDark, () => {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
})
</script>
<template>
<UPopover
mode="hover"
:ui="{
background: 'bg-white dark:bg-stone-900',
ring: 'ring-1 ring-gray-200 dark:ring-stone-800',
container: 'z-30'
}"
>
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
size="lg"
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
aria-label="Color picker"
>
<UIcon
class="w-5 h-5 text-primary-500 dark:text-primary-400"
name="i-ph-paint-brush-bold"
/>
</UButton>
</template>
<template #panel>
<div class="p-2">
<div class="grid grid-cols-5 gap-px">
<UTooltip
v-for="color in colors"
:key="color"
:open-delay="500"
:text="color"
class="capitalize"
>
<UButton
color="gray"
square
:ui="{
color: {
white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}
}
}"
:variant="color === getColor ? 'solid' : 'ghost'"
@click.stop.prevent="setColor(color)"
>
<span
:class="`bg-${color}-500/80 border-${color}-500`"
class="flex items-center justify-center w-3 h-3 rounded-full border text-white"
>
<UIcon
v-if="color === getColor"
name="i-ic-round-check"
/>
</span>
</UButton>
</UTooltip>
</div>
<UDivider class="my-2" />
<div>
<UToggle
v-model="isDark"
on-icon="i-heroicons-moon-20-solid"
off-icon="i-heroicons-sun-20-solid"
/>
</div>
</div>
</template>
</UPopover>
</template>

View File

@@ -0,0 +1,106 @@
<script lang="ts" setup>
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 => 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}`
}
const CardUi = {
footer: { padding: 'px-4 py-2' },
body: {base: 'h-full flex items-center'}
}
useIntervalFn(async () => await refresh(), 5000)
</script>
<template>
<UCard
:ui="CardUi"
class="flex flex-col justify-between"
>
<div
v-if="activity && activity.data.activities"
class="flex items-center gap-x-4"
>
<p
class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
>
Activity
</p>
<div v-if="codingActivity">
<div class="flex gap-4 items-center">
<UIcon
class="h-10 w-10"
:name="IDEs.find(ide => ide.name === codingActivity.name)!.icon"
/>
<div>
<div class="flex items-center gap-2">
<h1>{{ codingActivity.name }}</h1>
<UTooltip :text="codingActivity.details === 'Idling' ? 'I\'m sleeping 😴' : 'I\'m online 👋'">
<div
:class="codingActivity.details === 'Idling' ? 'bg-amber-500' : 'bg-green-500'"
class="h-3 w-3 inline-flex rounded-full cursor-pointer"
/>
</UTooltip>
</div>
<h3 v-if="codingActivity.details === 'Idling'">
I'm Idling on my computer
</h3>
<h3 v-else>
{{ codingActivity.details }} - {{ codingActivity.state }}
</h3>
</div>
</div>
</div>
<div
v-else
class="text-subtitle"
>
<div class="flex items-center gap-2">
<h1>I'm currently offline</h1>
<UTooltip text="I'm offline 🫥">
<div class="h-3 w-3 inline-flex rounded-full bg-red-500" />
</UTooltip>
</div>
<h3>Come back later to see what I'm doing</h3>
</div>
</div>
<template #footer>
<div class="flex items-center justify-end w-full">
<ClientOnly>
<p
v-if="codingActivity"
class="text-subtitle text-xs w-1/2"
>
Started {{ useTimeAgo(codingActivity.timestamps.start).value }}, the {{ formatDate(codingActivity.timestamps.start) }}
</p>
</ClientOnly>
<div class="flex items-center space-x-1 w-1/2 justify-end">
<p class="text-subtitle text-xs">
powered by
</p>
<UButton
size="xs"
:padded="false"
variant="link"
to="https://github.com/Phineas/lanyard"
target="_blank"
label="Lanyard"
/>
<UIcon
class="text-subtitle"
name="i-jam-thunder"
/>
</div>
</div>
</template>
</UCard>
</template>
<style>
</style>

View File

@@ -0,0 +1,49 @@
<script setup>
const socials = [
{
name: 'mail',
icon: 'i-material-symbols-alternate-email',
link: 'mailto:arthurdanjou@outlook.fr'
},
{
name: 'twitter',
icon: 'i-ph-twitter-logo-bold',
link: 'https://twitter.com/ArthurDanj'
},
{
name: 'github',
icon: 'i-ph-github-logo-bold',
link: 'https://github.com/ArthurDanjou'
},
{
name: 'linkedin',
icon: 'i-ph-linkedin-logo-bold',
link: 'https://www.linkedin.com/in/arthurdanjou/'
}
]
</script>
<template>
<div class="w-container mt-32 mb-24">
<div class="flex items-center flex-col space-y-4">
<h1 class="text-center lg:text-6xl sm:text-5xl text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 !leading-tight md:w-2/3">
Software engineer, mathematics lover and AI enthusiast
</h1>
<p class="leading-relaxed text-subtitle text-center md:w-2/3 p-2">
I'm Arthur, a software engineer passionate about artificial intelligence and the cloud but also a mathematics student living in France. I am currently studying mathematics at the Faculty of Sciences of Paris-Saclay.
</p>
<div class="flex gap-4">
<UButton
v-for="social in socials"
:key="social.name"
:icon="social.icon"
size="md"
:to="social.link"
variant="ghost"
target="_blank"
:ui="{ rounded: 'rounded-full' }"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,89 @@
<script lang="ts" setup>
import type { Stats } from '~~/types'
const stats = await $fetch<Stats>('/api/stats')
const CardUi = {
footer: { padding: 'px-4 py-2' },
body: {base: 'h-full'}
}
</script>
<template>
<UCard
:ui="CardUi"
class="flex flex-col justify-between"
>
<div class="flex items-center gap-x-4 h-full">
<p
class="uppercase tracking-widest text-sm"
:style="{ writingMode: 'vertical-rl', textOrientation: 'sideways' }"
>
STATS
</p>
<div v-if="stats">
<div class="flex gap-4 items-center">
<div class="text-md">
<div class="flex items-center gap-x-1">
<h3>Total hours:</h3>
<p class="text-subtitle">
{{ usePrecision(stats.coding.data.grand_total.total_seconds_including_other_language / 3600, 0) }} hours
</p>
</div>
<div class="flex items-start gap-x-1 flex-wrap">
<h3>Best Editors:</h3>
<p class="text-subtitle">
{{ stats.editors.data.slice(0, 2).map(editor => `${editor.name} (${editor.percent}%)`).join(', ') }}
</p>
</div>
<div class="flex items-center gap-x-1">
<h3>Best OS:</h3>
<p class="text-subtitle">
{{ stats.os.data[0].name }} with {{ stats.os.data[0].percent }}%
</p>
</div>
<div class="flex items-start gap-x-1 flex-wrap">
<h3>Top languages:</h3>
<p class="text-subtitle">
{{ stats.languages.data.slice(0, 2).map(language => `${language.name} (${language.percent}%)`).join(', ') }}
</p>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="flex items-center justify-between">
<ClientOnly>
<p
v-if="stats"
class="text-subtitle text-xs w-1/2"
>
Started {{ useTimeAgo(new Date(stats.coding.data.range.start)).value }}, the {{ useDateFormat(new Date(stats.coding.data.range.start), 'Do MMMM YYYY').value }}
</p>
</ClientOnly>
<div class="flex items-center justify-end space-x-1">
<p class="text-subtitle text-xs">
powered by
</p>
<UButton
size="xs"
:padded="false"
variant="link"
to="https://wakatime.com/"
target="_blank"
label="Wakatime"
/>
<UIcon
class="text-subtitle"
name="i-jam-thunder"
/>
</div>
</div>
</template>
</UCard>
</template>
<style>
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
defineProps({
startDate: String,
endDate: {
type: String,
required: true
}
})
function formatTodayDate(date: string) {
const split = date.split(' ')
return date === 'Today' ? 'Today' : `${split[0]} ${split[1]}`
}
</script>
<template>
<UBadge
v-if="startDate !== endDate"
size="xs"
variant="soft"
>
{{ formatTodayDate(startDate!.toString()) }} {{ formatTodayDate(endDate) }}
</UBadge>
<UBadge
v-else
size="xs"
variant="soft"
>
{{ formatTodayDate(endDate) }}
</UBadge>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { Education } from '~~/types'
defineProps({
education: Object as PropType<Education>
})
</script>
<template>
<div
v-if="education"
class="group relative flex flex-col items-start"
>
<div class="flex flex-col">
<div>
<DateTag
:end-date="education.endDate"
:start-date="education.startDate"
/>
</div>
<h1 class="my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{{ education.title }}
</h1>
</div>
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
{{ education.location }} {{ education.description }}
</p>
</div>
</template>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import type { WorkExperience } from '~~/types'
defineProps({
experience: Object as PropType<WorkExperience>
})
</script>
<template>
<div
v-if="experience"
class="group relative flex flex-col items-start"
>
<div>
<div class="flex flex-col">
<div>
<DateTag
:end-date="experience.endDate"
:start-date="experience.startDate"
/>
</div>
<div class="flex items-center my-1">
<UButton
v-if="experience.companyLink"
:to="experience.companyLink"
variant="link"
:padded="false"
color="white"
size="xl"
target="_blank"
:label="experience.company"
class="mr-3 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
>
<template #leading>
<UIcon
color="gray"
name="i-akar-icons-link-chain"
/>
</template>
</UButton>
<h1
v-else
class="mr-3 my-1 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100"
>
{{ experience.company }}
</h1>
<div class="text-subtitle text-xs">
{{ experience.location }}
</div>
</div>
</div>
</div>
<p class="text-justify leading-5 text-sm text-zinc-600 dark:text-zinc-400">
{{ experience.title }} {{ experience.description }}
</p>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { Skill } from '~~/types'
defineProps({
skill: Object as PropType<Skill>
})
const { $colorMode } = useNuxtApp()
const isLight = computed(() => $colorMode.value === 'light')
</script>
<template>
<li
v-if="skill"
class="flex items-center gap-2 rounded-md px-2 py-3 duration-300 md:hover:bg-gray-100 md:dark:hover:bg-neutral-800"
>
<div class="flex items-center">
<UIcon
v-if="isLight"
:name="skill.icon.light ? skill.icon.light : skill.icon"
dynamic
size="20"
/>
<UIcon
v-else
:name="skill.icon.dark ? skill.icon.dark : skill.icon"
dynamic
size="20"
/>
</div>
<span class="text-sm text-subtitle">{{ skill.name }}</span>
</li>
</template>