Implementing Drizzle to add views and like for post

This commit is contained in:
2024-06-30 14:30:15 +02:00
parent 0faa737863
commit 5af447e4a9
14 changed files with 977 additions and 943 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ logs
.env .env
.env.* .env.*
!.env.example !.env.example
# Drizzle
migrations

View File

@@ -48,10 +48,10 @@ const ide = items.value!.filter(item => item.category === 'ide')
size="xs" size="xs"
/> />
<li class="w-2/3 mx-auto"> <li class="w-2/3 mx-auto">
<NuxtImg <img
alt="My IntelliJ IDE" alt="My IntelliJ IDE"
src="/uses/jetbrains.png" src="/uses/jetbrains.png"
/> >
<p class="text-center text-sm mt-2 italic"> <p class="text-center text-sm mt-2 italic">
My IntelliJ Idea Ultimate IDE My IntelliJ Idea Ultimate IDE
</p> </p>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
const route = useRoute() const route = useRoute()
const { data: post } = await useAsyncData(`writing:${route.params.slug}`, () => queryContent(`/writings/${route.params.slug}`).findOne()) const { data: post } = await useAsyncData(`writing:${route.params.slug}`, () => queryContent(`/writings/${route.params.slug}`).findOne())
const {
data: postDB,
refresh
} = await useAsyncData(`writing:${route.params.slug}:db`, () => $fetch(`/api/posts/${route.params.slug}`, { method: 'POST' }))
function top() { function top() {
window.scrollTo({ window.scrollTo({
@@ -18,10 +22,25 @@ const { copy, copied } = useClipboard({
useHead({ useHead({
title: `${post.value!.title ?? 'Untitled'} | Arthur Danjou` title: `${post.value!.title ?? 'Untitled'} | Arthur Danjou`
}) })
function getDetails() {
const likes = postDB.value?.likes ?? 0
const views = postDB.value?.views ?? 0
const like = likes > 1 ? 'likes' : 'like'
const view = views > 1 ? 'views' : 'view'
return `${likes} ${like} · ${views} ${view}`
}
async function handleLike() {
await $fetch(`/api/posts/like/${route.params.slug}`, { method: 'PUT' })
await refresh()
}
</script> </script>
<template> <template>
<main v-if="post"> <main v-if="post && postDB">
<div class="flex"> <div class="flex">
<NuxtLink <NuxtLink
class="flex items-center gap-2 mb-8 group text-sm hover:text-black dark:hover:text-white duration-300" class="flex items-center gap-2 mb-8 group text-sm hover:text-black dark:hover:text-white duration-300"
@@ -35,21 +54,32 @@ useHead({
Go back Go back
</NuxtLink> </NuxtLink>
</div> </div>
<div class="mb-2 border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm"> <p class="border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm">
{{ useDateFormat(post.publishedAt, 'DD MMMM YYYY').value }} - {{ post.readingTime }}min. {{ getDetails() }}
</p>
<div>
<div class="flex items-end gap-2">
<h1
class="font-bold text-3xl text-black dark:text-white"
>
{{ post.title }}
</h1>
<p class="text-sm text-neutral-500">
{{ useDateFormat(post.publishedAt, 'DD MMMM YYYY').value }} · {{ post.readingTime }}min long
</p>
</div>
<p class="mt-4 text-base">
{{ post.description }}
</p>
</div> </div>
<AppTitle
:description="post.description"
:title="post.title ?? 'Untitled'"
/>
<div <div
v-if="post.cover" v-if="post.cover"
class="w-full rounded-md my-8" class="w-full rounded-md my-8"
> >
<NuxtImg <img
:src="`/writings/${post.cover}`" :src="`/writings/${post.cover}`"
alt="Writing cover" alt="Writing cover"
/> >
</div> </div>
<UDivider <UDivider
class="mt-8" class="mt-8"
@@ -72,6 +102,15 @@ useHead({
forget to leave a like!</strong> forget to leave a like!</strong>
</p> </p>
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<UButton
v-if="postDB.likes"
:label="postDB?.likes > 1 ? `${postDB?.likes} likes` : `${postDB?.likes} like`"
color="red"
icon="i-ph-heart-duotone"
size="lg"
variant="outline"
@click.prevent="handleLike()"
/>
<UButton <UButton
color="white" color="white"
icon="i-ph-arrow-fat-lines-up-duotone" icon="i-ph-arrow-fat-lines-up-duotone"
@@ -86,7 +125,7 @@ useHead({
icon="i-ph-check-square-duotone" icon="i-ph-check-square-duotone"
label="Link copied" label="Link copied"
size="lg" size="lg"
variant="solid" variant="outline"
@click.prevent="copy()" @click.prevent="copy()"
/> />
<UButton <UButton

View File

@@ -8,6 +8,20 @@ useSeoMeta({
const { data: writings } = await useAsyncData('all-writings', () => const { data: writings } = await useAsyncData('all-writings', () =>
queryContent('/writings').sort({ published: -1 }).without('body').find() queryContent('/writings').sort({ published: -1 }).without('body').find()
) )
const { data: writingsDB } = await useAsyncData('all-writings-db', () =>
$fetch(`/api/posts`)
)
function getDetails(slug: string) {
const writing = writingsDB.value!.find((writing: any) => writing.slug === slug)
if (!writing) return ''
const like = writing.likes! > 1 ? 'likes' : 'like'
const view = writing.views! > 1 ? 'views' : 'view'
return `${writing.likes} ${like} · ${writing.views} ${view}`
}
</script> </script>
<template> <template>
@@ -25,15 +39,20 @@ const { data: writings } = await useAsyncData('all-writings', () =>
:to="writing._path" :to="writing._path"
class="group" class="group"
> >
<article> <article class="space-y-1">
<div class="border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm"> <div class="border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm">
{{ useDateFormat(writing.publishedAt, 'DD MMMM YYYY').value }} - {{ writing.readingTime }}min. <p>{{ getDetails(writing.slug) }}</p>
</div>
<div class="flex items-center gap-2">
<h1
class="font-bold text-lg duration-300 text-neutral-600 group-hover:text-black dark:text-neutral-400 dark:group-hover:text-white"
>
{{ writing.title }}
</h1>
<p class="text-sm text-neutral-500 group-hover:text-black dark:group-hover:text-white duration-300">
{{ useDateFormat(writing.publishedAt, 'DD MMMM YYYY').value }} · {{ writing.readingTime }}min long
</p>
</div> </div>
<h1
class="font-bold my-2 text-lg duration-300 text-gray-600 group-hover:text-black dark:text-gray-400 dark:group-hover:text-white"
>
{{ writing.title }}
</h1>
<h3> <h3>
{{ writing.description }} {{ writing.description }}
</h3> </h3>

7
drizzle.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { Config } from 'drizzle-kit'
export default {
dialect: 'sqlite',
schema: './server/database/schema.ts',
out: './server/database/migrations'
} satisfies Config

View File

@@ -9,17 +9,21 @@ export default defineNuxtConfig({
'@nuxt/content', '@nuxt/content',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxtjs/google-fonts', '@nuxtjs/google-fonts',
'@nuxthq/studio', '@nuxthq/studio'
'@nuxt/image'
], ],
hub: { hub: {
cache: true, cache: true,
kv: true kv: true,
database: true,
analytics: true
}, },
app: { app: {
pageTransition: { name: 'page', mode: 'out-in' } pageTransition: { name: 'page', mode: 'out-in' },
head: {
htmlAttrs: { lang: 'en' }
}
}, },
content: { content: {
@@ -35,7 +39,6 @@ export default defineNuxtConfig({
ui: { ui: {
icons: ['heroicons', 'logos', 'ph'] icons: ['heroicons', 'logos', 'ph']
}, },
devtools: { devtools: {

View File

@@ -8,29 +8,33 @@
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "postinstall": "nuxt prepare",
"lint": "eslint ." "lint": "eslint .",
"db:generate": "drizzle-kit generate"
}, },
"dependencies": { "dependencies": {
"@iconify/json": "^2.2.222", "@iconify/json": "^2.2.223",
"@nuxt/content": "^2.13.0", "@nuxt/content": "^2.13.0",
"@nuxt/eslint": "^0.3.13", "@nuxt/eslint": "^0.3.13",
"@nuxt/image": "^1.7.0",
"@nuxt/ui": "^2.17.0", "@nuxt/ui": "^2.17.0",
"@nuxthq/studio": "^2.0.2", "@nuxthq/studio": "^2.0.3",
"@nuxthub/core": "^0.6.17", "@nuxthub/core": "^0.7.0",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/google-fonts": "^3.2.0",
"nuxt": "^3.12.2" "drizzle-orm": "^0.31.2",
"h3-zod": "^0.5.3",
"nuxt": "^3.12.2",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/devtools": "^1.3.6", "@nuxt/devtools": "^1.3.7",
"@nuxt/eslint-config": "^0.3.13", "@nuxt/eslint-config": "^0.3.13",
"@nuxt/ui": "^2.17.0", "@nuxt/ui": "^2.17.0",
"@types/node": "^20.14.9", "@types/node": "^20.14.9",
"@vueuse/core": "^10.11.0", "@vueuse/core": "^10.11.0",
"@vueuse/nuxt": "^10.11.0", "@vueuse/nuxt": "^10.11.0",
"eslint": "^9.5.0", "drizzle-kit": "^0.22.8",
"eslint": "^9.6.0",
"typescript": "^5.5.2", "typescript": "^5.5.2",
"vue-tsc": "^2.0.22", "vue-tsc": "^2.0.24",
"wrangler": "^3.62.0" "wrangler": "^3.62.0"
} }
} }

1711
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
import { useValidatedParams, z } from 'h3-zod'
export default defineEventHandler(async (event) => {
const { slug } = await useValidatedParams(event, {
slug: z.string()
})
return useDB().insert(tables.posts).values({
slug
}).onConflictDoUpdate({
target: tables.posts.slug,
set: {
slug,
views: sql`${tables.posts.views}
+ 1`
}
}).returning().get()
})

View File

@@ -0,0 +1,3 @@
export default defineEventHandler(() => {
return useDB().query.posts.findMany()
})

View File

@@ -0,0 +1,13 @@
import { useValidatedParams, z } from 'h3-zod'
export default defineEventHandler(async (event) => {
const { slug } = await useValidatedParams(event, {
slug: z.string()
})
return useDB().update(tables.posts)
.set({
likes: sql`${tables.posts.likes}
+ 1`
})
.where(eq(tables.posts.slug, slug))
})

View File

@@ -0,0 +1,9 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
export const posts = sqliteTable('posts', {
slug: text('slug').primaryKey(),
likes: integer('likes').default(0),
views: integer('views').default(0),
createdAt: text('created_at').default(sql`(CURRENT_DATE)`)
})

View File

@@ -0,0 +1,16 @@
import { consola } from 'consola'
import { migrate } from 'drizzle-orm/d1/migrator'
export default defineNitroPlugin(async () => {
if (!import.meta.dev) return
onHubReady(async () => {
await migrate(useDB(), { migrationsFolder: 'server/database/migrations' })
.then(() => {
consola.success('Database migrations done')
})
.catch((err) => {
consola.error('Database migrations failed', err)
})
})
})

10
server/utils/db.ts Normal file
View File

@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/d1'
import * as schema from '../database/schema'
export { sql, eq, and, or, asc, desc, sum } from 'drizzle-orm'
export const tables = schema
export function useDB() {
return drizzle(hubDatabase(), { schema })
}