mirror of
https://github.com/ArthurDanjou/website.git
synced 2026-01-14 12:14:42 +01:00
Working on oauth
This commit is contained in:
@@ -11,7 +11,7 @@ datasource db {
|
|||||||
|
|
||||||
model Maintenance {
|
model Maintenance {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
reason String
|
reason String @default("")
|
||||||
beginAt DateTime @default(now())
|
beginAt DateTime @default(now())
|
||||||
endAt DateTime @default(now())
|
endAt DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -21,24 +21,24 @@ model Maintenance {
|
|||||||
model Announcement {
|
model Announcement {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
content String
|
content String @default("")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Category {
|
model Category {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
slug String
|
slug String @default("")
|
||||||
name String
|
name String @default("")
|
||||||
talents CategoriesOnTalents[]
|
talents CategoriesOnTalents[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Talent {
|
model Talent {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
logo String
|
logo String @default("")
|
||||||
name String @unique
|
name String @unique @default("")
|
||||||
website String
|
website String @default("")
|
||||||
work String
|
work String @default("")
|
||||||
favorite Boolean @default(false)
|
favorite Boolean @default(false)
|
||||||
categories CategoriesOnTalents[]
|
categories CategoriesOnTalents[]
|
||||||
}
|
}
|
||||||
@@ -56,24 +56,34 @@ model CategoriesOnTalents {
|
|||||||
|
|
||||||
model Post {
|
model Post {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
slug String @unique
|
slug String @unique @default("")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
likes Int @default(0)
|
likes Int @default(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Suggestion {
|
model Suggestion {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
author String @unique
|
email String @unique @default("")
|
||||||
content String
|
content String @default("")
|
||||||
added Boolean @default(false)
|
added Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Form {
|
model Form {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String @default("")
|
||||||
email String
|
email String @default("")
|
||||||
content String
|
content String @default("")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model GuestbookMessage {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
message String @default("")
|
||||||
|
email String @unique @default("")
|
||||||
|
image String @default("")
|
||||||
|
username String @default("")
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ const navs = [
|
|||||||
icon: 'i-ph-shooting-star-bold',
|
icon: 'i-ph-shooting-star-bold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Bookmarks',
|
label: 'Guestbook',
|
||||||
to: '/bookmarks',
|
to: '/guestbook',
|
||||||
icon: 'i-ph-bookmarks-bold',
|
icon: 'i-material-symbols-book-2-outline',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contact',
|
label: 'Contact',
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
export async function usePost(slug: string) {
|
|
||||||
const {
|
|
||||||
data: post,
|
|
||||||
refresh: refreshPost,
|
|
||||||
} = useFetch('/api/article', {
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
slug,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const likes = ref(post.value?.likes)
|
|
||||||
const like = async () => {
|
|
||||||
const { data } = await useFetch('/api/like', { method: 'PUT' })
|
|
||||||
likes.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = ref(post.value!.views)
|
|
||||||
const view = async () => {
|
|
||||||
const { data } = await useFetch('/api/view', { method: 'PUT' })
|
|
||||||
likes.value = data
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
post,
|
|
||||||
like,
|
|
||||||
view,
|
|
||||||
refreshPost,
|
|
||||||
likes: computed(() => likes.value),
|
|
||||||
views: computed(() => views.value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,37 @@ import { providers } from '~~/types'
|
|||||||
useHead({
|
useHead({
|
||||||
title: 'Sign my guestbook • Arthur Danjou',
|
title: 'Sign my guestbook • Arthur Danjou',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { loggedIn, clear , user} = useUserSession()
|
||||||
|
|
||||||
|
const { data: messages, refresh } = useFetch('/api/messages', { method: 'get' })
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const messageContent = ref<string>('')
|
||||||
|
async function sign() {
|
||||||
|
if (messageContent.value.length < 7 || messageContent.value.length > 58)
|
||||||
|
return
|
||||||
|
|
||||||
|
await $fetch('/api/message', {
|
||||||
|
method: 'post',
|
||||||
|
body: {
|
||||||
|
message: messageContent.value,
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
toast.add({
|
||||||
|
title: `Thank's for leaving a message!`,
|
||||||
|
icon: 'i-material-symbols-check-circle-outline-rounded',
|
||||||
|
timeout: 4000,
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
toast.add({
|
||||||
|
title: 'An error occured when signing the book!',
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
messageContent.value = ''
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -20,10 +51,35 @@ useHead({
|
|||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<UIcon name="i-ph-circle-wavy-question-bold" class="text-subtitle text-xl" />
|
<UIcon name="i-ph-circle-wavy-question-bold" class="text-subtitle text-xl" />
|
||||||
<h1 class="text-lg font-bold">
|
<h1 class="text-lg font-bold">
|
||||||
Login to sign my book
|
Want to sign my book ?
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<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-stone-800"
|
||||||
|
placeholder="Leave a message"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
class="absolute right-1 top-1 text-gray-900 dark:text-white rounded-md"
|
||||||
|
label="Send"
|
||||||
|
:disabled="messageContent.trim().length < 7 || messageContent.trim().length > 58"
|
||||||
|
variant="soft"
|
||||||
|
@click.prevent="sign()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
@click.prevent="clear()"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2">
|
||||||
<UButton
|
<UButton
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.slug"
|
:key="provider.slug"
|
||||||
@@ -31,9 +87,37 @@ useHead({
|
|||||||
:color="provider.color"
|
:color="provider.color"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:icon="provider.icon"
|
:icon="provider.icon"
|
||||||
|
:to="provider.link"
|
||||||
|
external
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{ user }}
|
||||||
|
<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="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">
|
||||||
|
<NuxtImg class="w-full h-full rounded-full" :src="message.image" alt="Nature" placeholder />
|
||||||
|
</div>
|
||||||
|
<p class="font-bold">
|
||||||
|
{{ message.username }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Category, Suggestion, Talent } from '@prisma/client'
|
||||||
import { useTalentsStore } from '~/store/talents'
|
import { useTalentsStore } from '~/store/talents'
|
||||||
import { providers } from '~~/types'
|
import { providers } from '~~/types'
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ const { loggedIn, clear } = useUserSession()
|
|||||||
const {
|
const {
|
||||||
data: talents,
|
data: talents,
|
||||||
pending,
|
pending,
|
||||||
} = await useFetch('/api/talents', {
|
} = await useFetch<Array<Talent>>('/api/talents', {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
query: {
|
query: {
|
||||||
favorite: isFavorite,
|
favorite: isFavorite,
|
||||||
@@ -28,9 +29,8 @@ function isCategory(category: string) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
data: getCategories,
|
data: getCategories,
|
||||||
} = await useFetch('/api/categories', { method: 'GET' })
|
} = await useFetch<Array<Category>>('/api/categories', { method: 'GET' })
|
||||||
|
getCategories.value!.forEach((category: any) => categories.value.push({ label: category.name, slug: category.slug }))
|
||||||
getCategories.value?.forEach((category: any) => categories.value.push({ label: category.name, slug: category.slug }))
|
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
function getColor() {
|
function getColor() {
|
||||||
@@ -43,7 +43,7 @@ async function suggest() {
|
|||||||
if (suggestContent.value.trim().length < 4)
|
if (suggestContent.value.trim().length < 4)
|
||||||
return
|
return
|
||||||
|
|
||||||
await $fetch('/api/suggestion', {
|
await $fetch<Suggestion>('/api/suggestion', {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: {
|
body: {
|
||||||
content: suggestContent.value,
|
content: suggestContent.value,
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Post as PrismaPost } from '@prisma/client'
|
||||||
import type { Post } from '~~/types'
|
import type { Post } from '~~/types'
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data: postContent } = await useAsyncData<Post>(`writing:${route.params.slug}`, () => queryContent<Post>(`/writing/${route.params.slug}`).findOne())
|
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) {
|
if (!postContent.value) {
|
||||||
throw showError({
|
throw showError({
|
||||||
@@ -13,10 +33,7 @@ if (!postContent.value) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post, view, like, likes, views } = await usePost(route.params.slug.toString())
|
|
||||||
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
|
const format = (date: string) => useDateFormat(date, 'D MMMM YYYY').value.replaceAll('"', '')
|
||||||
onMounted(() => view())
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: `${postContent.value?.title} • Arthur Danjou's shelf`,
|
title: `${postContent.value?.title} • Arthur Danjou's shelf`,
|
||||||
})
|
})
|
||||||
@@ -38,10 +55,6 @@ const likeCookie = useCookie<boolean>(`post:like:${postContent.value.slug}`, {
|
|||||||
maxAge: 604_800,
|
maxAge: 604_800,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLiked = computed(() => {
|
|
||||||
return likeCookie.value === true
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleLike() {
|
async function handleLike() {
|
||||||
await like()
|
await like()
|
||||||
likeCookie.value = true
|
likeCookie.value = true
|
||||||
@@ -72,7 +85,7 @@ async function handleLike() {
|
|||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div>{{ postContent.readingMins }} min</div>
|
<div>{{ postContent.readingMins }} min</div>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<div>{{ views }} {{ views > 1 ? 'views' : 'view' }}</div>
|
<div>{{ post.views }} {{ post.views > 1 ? 'views' : 'view' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</time>
|
</time>
|
||||||
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
<h1 class="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
@@ -107,15 +120,7 @@ async function handleLike() {
|
|||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4 flex-wrap">
|
<div class="flex gap-4 flex-wrap">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="isLiked"
|
:label="`${likes} ${likes! > 1 ? 'likes' : 'like'}`"
|
||||||
:label="`${likes} ${likes > 1 ? 'likes' : 'like'}`"
|
|
||||||
icon="i-ph-heart-bold"
|
|
||||||
size="lg"
|
|
||||||
variant="solid"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
v-else
|
|
||||||
:label="`${likes} ${likes > 1 ? 'likes' : 'like'}`"
|
|
||||||
icon="i-ph-heart-bold"
|
icon="i-ph-heart-bold"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
},
|
},
|
||||||
update: {},
|
update: {
|
||||||
|
views: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
create: {
|
create: {
|
||||||
slug,
|
slug,
|
||||||
},
|
},
|
||||||
|
|||||||
24
src/server/api/message.post.ts
Normal file
24
src/server/api/message.post.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const MessageValidator = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}).parse
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { message } = await readValidatedBody(event, MessageValidator)
|
||||||
|
const { user } = await requireUserSession(event)
|
||||||
|
return await usePrisma().guestbookMessage.upsert({
|
||||||
|
where: {
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
email: user.email,
|
||||||
|
image: user.picture,
|
||||||
|
username: user.username,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
7
src/server/api/messages.get.ts
Normal file
7
src/server/api/messages.get.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default defineEventHandler(async () => {
|
||||||
|
return await usePrisma().guestbookMessage.findMany({
|
||||||
|
orderBy: {
|
||||||
|
updatedAt: 'desc',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,13 +9,13 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { user } = await requireUserSession(event)
|
const { user } = await requireUserSession(event)
|
||||||
return await usePrisma().suggestion.upsert({
|
return await usePrisma().suggestion.upsert({
|
||||||
where: {
|
where: {
|
||||||
author: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
author: user.email,
|
email: user.email,
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
export default oauth.githubEventHandler({
|
export default oauth.googleEventHandler({
|
||||||
|
config: {
|
||||||
|
redirectUrl: '/talents',
|
||||||
|
},
|
||||||
async onSuccess(event: any, { user }: any) {
|
async onSuccess(event: any, { user }: any) {
|
||||||
await setUserSession(event, {
|
await setUserSession(event, {
|
||||||
user: {
|
user: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.photoUrl,
|
picture: user.picture,
|
||||||
username: String(user.displayName).trim(),
|
username: String(user.name).trim(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return sendRedirect(event, '/')
|
return sendRedirect(event, '/')
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ https://atinux.com/
|
|||||||
https://yael.dev/
|
https://yael.dev/
|
||||||
https://esm.dev/
|
https://esm.dev/
|
||||||
https://antfu.me/
|
https://antfu.me/
|
||||||
|
tom lienard
|
||||||
|
|
||||||
Categories:
|
Categories:
|
||||||
BRAND
|
BRAND
|
||||||
|
|||||||
1
types.ts
1
types.ts
@@ -1,4 +1,3 @@
|
|||||||
import exp from 'node:constants'
|
|
||||||
import type { MarkdownParsedContent, ParsedContent } from '@nuxt/content/dist/runtime/types'
|
import type { MarkdownParsedContent, ParsedContent } from '@nuxt/content/dist/runtime/types'
|
||||||
|
|
||||||
export enum ColorsTheme {
|
export enum ColorsTheme {
|
||||||
|
|||||||
Reference in New Issue
Block a user