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",