mirror of
https://github.com/ArthurDanjou/artsite.git
synced 2026-01-14 15:54:13 +01:00
Implementing Drizzle to add views and like for post
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ logs
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Drizzle
|
||||
migrations
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
7
drizzle.config.ts
Normal 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
|
||||
@@ -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: {
|
||||
|
||||
22
package.json
22
package.json
@@ -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
1711
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
17
server/api/posts/[slug].post.ts
Normal file
17
server/api/posts/[slug].post.ts
Normal 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()
|
||||
})
|
||||
3
server/api/posts/index.get.ts
Normal file
3
server/api/posts/index.get.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default defineEventHandler(() => {
|
||||
return useDB().query.posts.findMany()
|
||||
})
|
||||
13
server/api/posts/like/[slug].put.ts
Normal file
13
server/api/posts/like/[slug].put.ts
Normal 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))
|
||||
})
|
||||
9
server/database/schema.ts
Normal file
9
server/database/schema.ts
Normal 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)`)
|
||||
})
|
||||
16
server/plugins/migrations.ts
Normal file
16
server/plugins/migrations.ts
Normal 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
10
server/utils/db.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user