Skip to content
Open
13 changes: 12 additions & 1 deletion src/components/Quote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
>🙈</i
>
{{ formatDate(quote.createdAt) }}
<button
class="icon"
v-if="quote.onEditClick"
@click="quote.onEditClick(quote.id)"
:title="getLocalizedString('edit')"
>
✏️
</button>
</span>
</div>
</div>
Expand All @@ -39,7 +47,10 @@ import { formatDate, formatSubquoteText } from "~/util/formatters";
const authState = useAuthState();

const { quote } = defineProps<{
quote: Quote & { subquotes: Omit<Subquote, "quoteId">[] };
quote: Quote & {
subquotes: Omit<Subquote, "quoteId">[];
onEditClick?: (quoteID: number) => void;
};
}>();
</script>

Expand Down
5 changes: 4 additions & 1 deletion src/components/QuoteList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { getLocalizedString } from "~/util/localization";
const authState = useAuthState();

const { quotes } = defineProps<{
quotes: (QuoteType & { subquotes: Omit<SubquoteType, "quoteId">[] })[];
quotes: (QuoteType & {
subquotes: Omit<SubquoteType, "quoteId">[];
onEditClick?: (quoteID: number) => void;
})[];
}>();

/* filter quotes by search query */
Expand Down
86 changes: 68 additions & 18 deletions src/pages/compose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@
:class="{ error: saveError }"
@click="attemptSaveQuote"
>
{{ getLocalizedString("saveQuote") }} &nbsp;<i class="icon">💾</i>
{{ getLocalizedString(isEditing ? "updateQuote" : "saveQuote") }}
&nbsp;<i class="icon">💾</i>
</button>
</main>
</template>
Expand All @@ -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,
Expand All @@ -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<Element[]>([]);
onBeforeUpdate(() => (subquoteLines.value = []));

Expand Down Expand Up @@ -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<QuoteType, "id" | "createdAt" | "updatedAt"> & {
Expand Down
10 changes: 10 additions & 0 deletions src/pages/profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion src/server/api/quotes/[id].delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
15 changes: 9 additions & 6 deletions src/server/api/quotes/[id].post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ export default defineEventHandler(async (event): Promise<QuoteResponse | void> =
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<QuoteUpdateRequest>(event);

Expand All @@ -56,23 +59,23 @@ export default defineEventHandler(async (event): Promise<QuoteResponse | void> =
? 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 },
Expand Down
6 changes: 6 additions & 0 deletions src/util/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Language = {
| "no"
| "today"
| "yesterday"
| "edit"
| "email"
| "home"
| "invitee"
Expand All @@ -22,6 +23,7 @@ type Language = {
| "quote"
| "quotes"
| "saveQuote"
| "updateQuote"
| "search"
| "signIn"
| "signOut"
Expand Down Expand Up @@ -51,6 +53,7 @@ const languages: Record<SupportedLanguage, Language> = {
today: "vandaag",
yesterday: "gisteren",

edit: "bewerken",
email: "e-mail",
home: "start",
invitee: "gebruiker uitgenodigd",
Expand All @@ -66,6 +69,7 @@ const languages: Record<SupportedLanguage, Language> = {
quote: "quote",
quotes: "quotes",
saveQuote: "quotuleer",
updateQuote: "werk bij",
search: "zoeken",
signIn: "log in",
signOut: "log uit",
Expand All @@ -89,6 +93,7 @@ const languages: Record<SupportedLanguage, Language> = {
today: "today",
yesterday: "yesterday",

edit: "edit",
email: "email",
home: "home",
invitee: "invitee",
Expand All @@ -104,6 +109,7 @@ const languages: Record<SupportedLanguage, Language> = {
quote: "quote",
quotes: "quotes",
saveQuote: "save quote",
updateQuote: "update quote",
search: "search",
signIn: "log in",
signOut: "log out",
Expand Down