diff --git a/src/components/Quote.vue b/src/components/Quote.vue index 9d9068a..e925d05 100644 --- a/src/components/Quote.vue +++ b/src/components/Quote.vue @@ -27,6 +27,14 @@ >🙈 {{ formatDate(quote.createdAt) }} + @@ -39,7 +47,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 561f531..6f51083 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/compose.vue b/src/pages/compose.vue index 1a57a72..ddd2470 100644 --- a/src/pages/compose.vue +++ b/src/pages/compose.vue @@ -116,7 +116,8 @@ :class="{ error: saveError }" @click="attemptSaveQuote" > - {{ getLocalizedString("saveQuote") }}  💾 + {{ getLocalizedString(isEditing ? "updateQuote" : "saveQuote") }} +  💾 @@ -126,11 +127,17 @@ 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 ?? null, public: true, @@ -143,6 +150,27 @@ let newQuote: EmptyQuote = reactive({ ], }); +// 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 = [])); @@ -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("/profile"); + }) + .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/pages/profile.vue b/src/pages/profile.vue index cfcb3de..c1cecd4 100644 --- a/src/pages/profile.vue +++ b/src/pages/profile.vue @@ -49,6 +49,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) { 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)); } diff --git a/src/server/api/quotes/[id].post.ts b/src/server/api/quotes/[id].post.ts index abb078b..7ff37c4 100644 --- a/src/server/api/quotes/[id].post.ts +++ b/src/server/api/quotes/[id].post.ts @@ -27,13 +27,16 @@ 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)); } + if (existing.authorId !== event.context.user.id) { + return sendError(event, createError(403)); + } const input = await readBody(event); @@ -56,23 +59,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 62342d4..50dae97 100644 --- a/src/util/localization.ts +++ b/src/util/localization.ts @@ -7,6 +7,7 @@ type Language = { | "no" | "today" | "yesterday" + | "edit" | "email" | "home" | "invitee" @@ -22,6 +23,7 @@ type Language = { | "quote" | "quotes" | "saveQuote" + | "updateQuote" | "search" | "signIn" | "signOut" @@ -51,6 +53,7 @@ const languages: Record = { today: "vandaag", yesterday: "gisteren", + edit: "bewerken", email: "e-mail", home: "start", invitee: "gebruiker uitgenodigd", @@ -66,6 +69,7 @@ const languages: Record = { quote: "quote", quotes: "quotes", saveQuote: "quotuleer", + updateQuote: "werk bij", search: "zoeken", signIn: "log in", signOut: "log uit", @@ -89,6 +93,7 @@ const languages: Record = { today: "today", yesterday: "yesterday", + edit: "edit", email: "email", home: "home", invitee: "invitee", @@ -104,6 +109,7 @@ const languages: Record = { quote: "quote", quotes: "quotes", saveQuote: "save quote", + updateQuote: "update quote", search: "search", signIn: "log in", signOut: "log out",