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.example
# Drizzle
migrations

View File

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

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
const route = useRoute()
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() {
window.scrollTo({
@@ -18,10 +22,25 @@ const { copy, copied } = useClipboard({
useHead({
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>
<template>
<main v-if="post">
<main v-if="post && postDB">
<div class="flex">
<NuxtLink
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
</NuxtLink>
</div>
<div class="mb-2 border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm">
{{ useDateFormat(post.publishedAt, 'DD MMMM YYYY').value }} - {{ post.readingTime }}min.
<p class="border-l-2 pl-2 border-gray-300 dark:border-gray-700 rounded-sm">
{{ 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>
<AppTitle
:description="post.description"
:title="post.title ?? 'Untitled'"
/>
<div
v-if="post.cover"
class="w-full rounded-md my-8"
>
<NuxtImg
<img
:src="`/writings/${post.cover}`"
alt="Writing cover"
/>
>
</div>
<UDivider
class="mt-8"
@@ -72,6 +102,15 @@ useHead({
forget to leave a like!</strong>
</p>
<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
color="white"
icon="i-ph-arrow-fat-lines-up-duotone"
@@ -86,7 +125,7 @@ useHead({
icon="i-ph-check-square-duotone"
label="Link copied"
size="lg"
variant="solid"
variant="outline"
@click.prevent="copy()"
/>
<UButton

View File

@@ -8,6 +8,20 @@ useSeoMeta({
const { data: writings } = await useAsyncData('all-writings', () =>
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>
<template>
@@ -25,15 +39,20 @@ const { data: writings } = await useAsyncData('all-writings', () =>
:to="writing._path"
class="group"
>
<article>
<article class="space-y-1">
<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>
<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>
{{ writing.description }}
</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',
'@vueuse/nuxt',
'@nuxtjs/google-fonts',
'@nuxthq/studio',
'@nuxt/image'
'@nuxthq/studio'
],
hub: {
cache: true,
kv: true
kv: true,
database: true,
analytics: true
},
app: {
pageTransition: { name: 'page', mode: 'out-in' }
pageTransition: { name: 'page', mode: 'out-in' },
head: {
htmlAttrs: { lang: 'en' }
}
},
content: {
@@ -35,7 +39,6 @@ export default defineNuxtConfig({
ui: {
icons: ['heroicons', 'logos', 'ph']
},
devtools: {

View File

@@ -8,29 +8,33 @@
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint ."
"lint": "eslint .",
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@iconify/json": "^2.2.222",
"@iconify/json": "^2.2.223",
"@nuxt/content": "^2.13.0",
"@nuxt/eslint": "^0.3.13",
"@nuxt/image": "^1.7.0",
"@nuxt/ui": "^2.17.0",
"@nuxthq/studio": "^2.0.2",
"@nuxthub/core": "^0.6.17",
"@nuxthq/studio": "^2.0.3",
"@nuxthub/core": "^0.7.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": {
"@nuxt/devtools": "^1.3.6",
"@nuxt/devtools": "^1.3.7",
"@nuxt/eslint-config": "^0.3.13",
"@nuxt/ui": "^2.17.0",
"@types/node": "^20.14.9",
"@vueuse/core": "^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",
"vue-tsc": "^2.0.22",
"vue-tsc": "^2.0.24",
"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 })
}