From d278650478ae58a31cef796614b007004965e28e Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 00:12:31 +0200 Subject: [PATCH 1/9] feat: editing quotes --- src/pages/compose.vue | 94 +++++++++++++++++++++++------- src/server/api/quotes/[id].post.ts | 12 ++-- src/util/localization.ts | 3 + 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/pages/compose.vue b/src/pages/compose.vue index 8ba8f08..0e0d278 100644 --- a/src/pages/compose.vue +++ b/src/pages/compose.vue @@ -29,8 +29,8 @@ class="subquote" v-for="(subquote, i) in newQuote.subquotes" :ref=" - (el: HTMLTableRowElement | null) => { - if (el) subquoteLines[i] = el; + (el) => { + if (el) subquoteLines[i] = el as Element; } " :title="subquoteValidateErrors[i] ?? undefined" @@ -116,7 +116,8 @@ :class="{ error: saveError }" @click="attemptSaveQuote" > - {{ getLocalizedString("saveQuote") }}  💾 + {{ getLocalizedString(isEditing ? "updateQuote" : "saveQuote") }} +  💾 @@ -126,13 +127,19 @@ import { type Quote as QuoteType, type Subquote as SubquoteType } from "@prisma/ import { getLocalizedString } from "~/util/localization"; const authState = useAuthState(); const router = useRouter(); +const route = useRoute(); if (!authState.value.loggedIn) { router.push("/"); } +const isEditing = computed(() => !!route.query.edit); +const quoteID = computed(() => + route.query.edit ? parseInt(route.query.edit as string) : null, +); + let newQuote: EmptyQuote = reactive({ - authorId: authState.value.user?.id, + authorId: authState.value.user?.id ?? null, public: true, subquotes: [ { @@ -143,7 +150,28 @@ let newQuote: EmptyQuote = reactive({ ], }); -const subquoteLines = ref([]); +// Load existing quote if in edit mode +onMounted(async () => { + if (isEditing.value && quoteID.value) { + try { + const quote = await $fetch(`/api/quotes/${quoteID.value}`, { + headers: authState.getAuthHeader(), + }); + + newQuote.public = quote.public; + newQuote.subquotes = quote.subquotes.map((sq) => ({ + quotee: sq.quotee, + text: sq.text, + isAction: sq.isAction, + })); + } catch (error) { + console.error("Failed to load quote for editing:", error); + router.push("/"); + } + } +}); + +const subquoteLines = ref([]); onBeforeUpdate(() => (subquoteLines.value = [])); function gotoNext(from: number) { @@ -217,24 +245,46 @@ function attemptSaveQuote() { return; } - $fetch("/api/quotes", { - headers: authState.getAuthHeader(), - method: "POST", - body: { - public: newQuote.public, - subquotes: newQuote.subquotes.map((sq, i) => ({ - ...sq, - subquoteId: i + 1, - })), - }, - }) - .then(() => { - saveError.value = false; - router.push("/"); + const subquotesWithIDs = newQuote.subquotes.map((sq, i) => ({ + ...sq, + subquoteId: i + 1, + })); + + if (isEditing.value && quoteID.value) { + // Update existing quote + $fetch(`/api/quotes/${quoteID.value}`, { + headers: authState.getAuthHeader(), + method: "POST", + body: { + public: newQuote.public, + subquotes: subquotesWithIDs, + }, }) - .catch(() => { - saveError.value = true; - }); + .then(() => { + saveError.value = false; + router.push("/"); + }) + .catch(() => { + saveError.value = true; + }); + } else { + // Create new quote + $fetch("/api/quotes", { + headers: authState.getAuthHeader(), + method: "POST", + body: { + public: newQuote.public, + subquotes: subquotesWithIDs, + }, + }) + .then(() => { + saveError.value = false; + router.push("/"); + }) + .catch(() => { + saveError.value = true; + }); + } } type EmptyQuote = Omit & { diff --git a/src/server/api/quotes/[id].post.ts b/src/server/api/quotes/[id].post.ts index abb078b..516e817 100644 --- a/src/server/api/quotes/[id].post.ts +++ b/src/server/api/quotes/[id].post.ts @@ -27,11 +27,11 @@ export default defineEventHandler(async (event): Promise = return sendError(event, createError(401)); } - const quote = await prisma.quote.findUnique({ + const existing = await prisma.quote.findUnique({ where: { id: quoteID }, include: { subquotes: true }, }); - if (!quote) { + if (!existing) { return sendError(event, createError(404)); } @@ -56,23 +56,23 @@ export default defineEventHandler(async (event): Promise = ? undefined : { deleteMany: - input.subquotes.length >= quote.subquotes.length + input.subquotes.length >= existing.subquotes.length ? undefined : { subquoteId: { gt: input.subquotes.length }, }, createMany: - input.subquotes.length < quote.subquotes.length + input.subquotes.length < existing.subquotes.length ? undefined : { data: input.subquotes - .filter((s) => s.subquoteId > quote.subquotes.length) + .filter((s) => s.subquoteId > existing.subquotes.length) .map((s) => ({ ...s })), }, updateMany: input.subquotes - .filter((s) => s.subquoteId <= quote.subquotes.length) + .filter((s) => s.subquoteId <= existing.subquotes.length) .map((s) => ({ where: { subquoteId: s.subquoteId }, data: { ...s, subquoteId: undefined }, diff --git a/src/util/localization.ts b/src/util/localization.ts index 5617ca1..aec0a0c 100644 --- a/src/util/localization.ts +++ b/src/util/localization.ts @@ -22,6 +22,7 @@ type Language = { | "quote" | "quotes" | "saveQuote" + | "updateQuote" | "search" | "signIn" | "signOut" @@ -66,6 +67,7 @@ const languages: Record = { quote: "quote", quotes: "quotes", saveQuote: "quotuleer", + updateQuote: "werk bij", search: "zoeken", signIn: "log in", signOut: "log uit", @@ -104,6 +106,7 @@ const languages: Record = { quote: "quote", quotes: "quotes", saveQuote: "save quote", + updateQuote: "update quote", search: "search", signIn: "log in", signOut: "log out", From dd8e752893355d2bdcac617102a1bea7b2f4af41 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 01:18:24 +0200 Subject: [PATCH 2/9] add access check for quote update --- src/server/api/quotes/[id].post.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/api/quotes/[id].post.ts b/src/server/api/quotes/[id].post.ts index 516e817..7ff37c4 100644 --- a/src/server/api/quotes/[id].post.ts +++ b/src/server/api/quotes/[id].post.ts @@ -34,6 +34,9 @@ export default defineEventHandler(async (event): Promise = if (!existing) { return sendError(event, createError(404)); } + if (existing.authorId !== event.context.user.id) { + return sendError(event, createError(403)); + } const input = await readBody(event); From 243bb5d5806debcadb5f36f511c9ac4981218c65 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 01:25:05 +0200 Subject: [PATCH 3/9] add edit button --- src/components/Quote.vue | 12 +++++++++++- src/components/QuoteList.vue | 5 ++++- src/pages/profile.vue | 1 + src/util/localization.ts | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/Quote.vue b/src/components/Quote.vue index 9d9068a..9d08726 100644 --- a/src/components/Quote.vue +++ b/src/components/Quote.vue @@ -27,6 +27,13 @@ >🙈 {{ formatDate(quote.createdAt) }} + ✏️ @@ -39,7 +46,10 @@ import { formatDate, formatSubquoteText } from "~/util/formatters"; const authState = useAuthState(); const { quote } = defineProps<{ - quote: Quote & { subquotes: Omit[] }; + quote: Quote & { + subquotes: Omit[]; + onEditClick?: (quoteID: number) => void; + }; }>(); diff --git a/src/components/QuoteList.vue b/src/components/QuoteList.vue index c7360ca..0eabf8f 100644 --- a/src/components/QuoteList.vue +++ b/src/components/QuoteList.vue @@ -20,7 +20,10 @@ import { getLocalizedString } from "~/util/localization"; const authState = useAuthState(); const { quotes } = defineProps<{ - quotes: (QuoteType & { subquotes: Omit[] })[]; + quotes: (QuoteType & { + subquotes: Omit[]; + onEditClick?: (quoteID: number) => void; + })[]; }>(); /* filter quotes by search query */ diff --git a/src/pages/profile.vue b/src/pages/profile.vue index 0cabf16..807c64f 100644 --- a/src/pages/profile.vue +++ b/src/pages/profile.vue @@ -43,6 +43,7 @@ const { data: profile } = await useFetch("/api/user", { ...serializedObj, createdAt: new Date(serializedObj.createdAt), updatedAt: new Date(serializedObj.updatedAt), + onEditClick: (quoteID: number) => router.push(`/compose?edit=${quoteID}`), })) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()), }), diff --git a/src/util/localization.ts b/src/util/localization.ts index aec0a0c..596dace 100644 --- a/src/util/localization.ts +++ b/src/util/localization.ts @@ -7,6 +7,7 @@ type Language = { | "no" | "today" | "yesterday" + | "edit" | "email" | "home" | "invitee" @@ -52,6 +53,7 @@ const languages: Record = { today: "vandaag", yesterday: "gisteren", + edit: "bewerken", email: "e-mail", home: "start", invitee: "gebruiker uitgenodigd", @@ -91,6 +93,7 @@ const languages: Record = { today: "today", yesterday: "yesterday", + edit: "edit", email: "email", home: "home", invitee: "invitee", From 8ba4f59885a4e92d1a2651bbb47eff98a1ba2934 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 01:27:40 +0200 Subject: [PATCH 4/9] add delete access check --- src/server/api/quotes/[id].delete.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/api/quotes/[id].delete.ts b/src/server/api/quotes/[id].delete.ts index c068470..8effe97 100644 --- a/src/server/api/quotes/[id].delete.ts +++ b/src/server/api/quotes/[id].delete.ts @@ -11,7 +11,13 @@ export default defineEventHandler(async (event) => { return sendError(event, createError(404)); } - const deletedQuote = await prisma.quote.delete({ where: { id: quoteID } }); + if (!event.context.user) { + return sendError(event, createError(401)); + } + + const deletedQuote = await prisma.quote.delete({ + where: { id: quoteID, authorId: event.context.user.id }, + }); if (!deletedQuote) { return sendError(event, createError(404)); } From 6eb4b06f31abacdfbfb7e180c6312702b827a18d Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 01:34:18 +0200 Subject: [PATCH 5/9] fix localization(?) --- src/util/localization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/localization.ts b/src/util/localization.ts index 596dace..de73c46 100644 --- a/src/util/localization.ts +++ b/src/util/localization.ts @@ -152,7 +152,7 @@ function getLocale(): SupportedLanguage { } function _getSupportedPreferredLocales(): SupportedLanguage[] { - if (process.server) { + if (import.meta.server) { const reqLanguagesHeader = useRequestHeaders(["accept-language"])[ "accept-language" ]; @@ -161,7 +161,7 @@ function _getSupportedPreferredLocales(): SupportedLanguage[] { (l) => l.includes("-") && l.slice(0, 2) in languages, ) as SupportedLanguage[]; } - } else if (process.client) { + } else if (import.meta.client) { return navigator.languages.filter( (l) => l.includes("-") && l.slice(0, 2) in languages, ) as SupportedLanguage[]; From 53f6375b90f3f9677307f98b3e776f8b1386471c Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 01:57:00 +0200 Subject: [PATCH 6/9] make edit button clickable --- src/components/Quote.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Quote.vue b/src/components/Quote.vue index 9d08726..aa18eab 100644 --- a/src/components/Quote.vue +++ b/src/components/Quote.vue @@ -28,7 +28,8 @@ > {{ formatDate(quote.createdAt) }} Date: Mon, 14 Apr 2025 02:06:45 +0200 Subject: [PATCH 7/9] make edit button client-only --- src/pages/profile.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/profile.vue b/src/pages/profile.vue index 807c64f..7a1edeb 100644 --- a/src/pages/profile.vue +++ b/src/pages/profile.vue @@ -43,7 +43,6 @@ const { data: profile } = await useFetch("/api/user", { ...serializedObj, createdAt: new Date(serializedObj.createdAt), updatedAt: new Date(serializedObj.updatedAt), - onEditClick: (quoteID: number) => router.push(`/compose?edit=${quoteID}`), })) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()), }), @@ -54,6 +53,16 @@ const { data: profile } = await useFetch("/api/user", { return result; }); +// Add onEditClick function on client side +onMounted(() => { + if (profile.value) { + profile.value.authoredQuotes = profile.value.authoredQuotes.map((quote) => ({ + ...quote, + onEditClick: (quoteID: number) => router.push(`/compose?edit=${quoteID}`), + })); + } +}); + function invite() { if (!profile.value) return; if (profile.value._count.invitations >= 5) { From 4fa7229bde13f2bffd9f61a0fabb8889be02ce35 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 02:10:02 +0200 Subject: [PATCH 8/9] fix edit button? --- src/components/Quote.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Quote.vue b/src/components/Quote.vue index aa18eab..e925d05 100644 --- a/src/components/Quote.vue +++ b/src/components/Quote.vue @@ -27,14 +27,14 @@ >🙈 {{ formatDate(quote.createdAt) }} - ✏️ + ✏️ + From 47ea9e2d5c02efbc1c4049ce5fd7b868b6de419a Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 14 Apr 2025 02:16:17 +0200 Subject: [PATCH 9/9] change redirect after edit to /profile --- src/pages/compose.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/compose.vue b/src/pages/compose.vue index 0e0d278..ddd2470 100644 --- a/src/pages/compose.vue +++ b/src/pages/compose.vue @@ -262,7 +262,7 @@ function attemptSaveQuote() { }) .then(() => { saveError.value = false; - router.push("/"); + router.push("/profile"); }) .catch(() => { saveError.value = true;