fixed styles

This commit is contained in:
liyunze 2026-09-01 21:32:51 +08:00
parent efb364d7cc
commit 3130a1affa
12 changed files with 393 additions and 116 deletions

View file

@ -78,7 +78,7 @@ define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
*/ */
workbox.precacheAndRoute([{ workbox.precacheAndRoute([{
"url": "/", "url": "/",
"revision": "0.7vtfun0cbi" "revision": "0.cgn6ae45jcc"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {

View file

@ -109,10 +109,29 @@
<label for="markdown-editor" class="kicker mb-2 block">{label}</label> <label for="markdown-editor" class="kicker mb-2 block">{label}</label>
{/if} {/if}
{#if mode === "write"} <div class="border-l border-t border-r border-rule w-fit">
<div <button
class="border-rule bg-surface mb-2 flex flex-wrap items-center gap-1 rounded-sm border p-1" type="button"
class="{mode === 'write'
? 'text-primary'
: 'hover:text-primary'} text-sm px-2 py-2 border-r border-rule"
onclick={() => (mode = "write")}
> >
Write
</button>
<button
type="button"
class="{mode === 'preview'
? 'text-primary'
: 'hover:text-primary'} text-sm px-2 py-2"
onclick={() => (mode = "preview")}
>
Preview
</button>
</div>
{#if mode === "write"}
<div class="border-rule bg-surface flex flex-wrap items-center gap-1 rounded-sm border p-1">
<button type="button" class="editor-tool" title="Bold" onclick={() => wrap("**")}> <button type="button" class="editor-tool" title="Bold" onclick={() => wrap("**")}>
<strong>B</strong> <strong>B</strong>
</button> </button>
@ -159,30 +178,6 @@
</button> </button>
</div> </div>
{/if}
<div class="border-rule mb-2 flex items-center gap-2 border-b">
<button
type="button"
class="kicker {mode === 'write'
? 'text-ink'
: 'hover:text-primary'} border-b-2 border-transparent pb-2"
onclick={() => (mode = "write")}
>
Write
</button>
<button
type="button"
class="kicker {mode === 'preview'
? 'text-ink'
: 'hover:text-primary'} border-b-2 border-transparent pb-2"
onclick={() => (mode = "preview")}
>
Preview
</button>
</div>
{#if mode === "write"}
<textarea <textarea
id="markdown-editor" id="markdown-editor"
bind:this={textarea} bind:this={textarea}

View file

@ -55,6 +55,7 @@
type="button" type="button"
class="nav-link" class="nav-link"
onclick={() => { onclick={() => {
if (!confirm("Sign out?")) return;
logout(); logout();
goto("/"); goto("/");
}} }}
@ -156,6 +157,7 @@
type="button" type="button"
class="nav-link block" class="nav-link block"
onclick={() => { onclick={() => {
if (!confirm("Sign out?")) return;
logout(); logout();
goto("/"); goto("/");
mobileMenuOpen = false; mobileMenuOpen = false;

View file

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { mutation } from "$lib/api"; import { query, mutation } from "$lib/api";
import { getToken } from "$lib/stores/auth"; import { getToken } from "$lib/stores/auth";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { onMount } from "svelte";
let { let {
count = 0, count = 0,
@ -18,6 +19,16 @@
let busy = $state(false); let busy = $state(false);
const voteCount = $derived(localCount ?? count); const voteCount = $derived(localCount ?? count);
onMount(async () => {
const token = getToken();
if (!token) return;
try {
userVote = (await query("votes:getMyVote", { token, targetType, targetId })) ?? 0;
} catch {
/* ignore */
}
});
async function vote(value: 1 | -1, e: MouseEvent) { async function vote(value: 1 | -1, e: MouseEvent) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -295,10 +295,30 @@ function notesCreate(
const user = requireAuth(db, args.token); const user = requireAuth(db, args.token);
const id = newId(); const id = newId();
const now = Date.now(); const now = Date.now();
db.prepare( db.exec("BEGIN");
`INSERT INTO notes (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, commentCount) try {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0)`, db.prepare(
).run(id, args.title, args.content, args.topicId, args.unitId, user._id, user.name, now, now); `INSERT INTO notes (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, commentCount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)`,
).run(
id,
args.title,
args.content,
args.topicId,
args.unitId,
user._id,
user.name,
now,
now,
);
db.prepare(
"INSERT INTO votes (id, userId, targetType, targetId, value) VALUES (?, ?, 'note', ?, 1)",
).run(newId(), user._id, id);
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
throw err;
}
return id; return id;
} }
@ -348,6 +368,27 @@ function notesRemove(db: Db, args: { token: string; id: string }) {
db.prepare("DELETE FROM notes WHERE id = ?").run(args.id); db.prepare("DELETE FROM notes WHERE id = ?").run(args.id);
} }
function notesUpdate(db: Db, args: { token: string; id: string; title: string; content: string }) {
const user = requireAuth(db, args.token);
const note = db.prepare("SELECT id, authorId FROM notes WHERE id = ?").get(args.id) as
| { id: string; authorId: string }
| undefined;
if (!note || note.authorId !== user._id) throw new Error("Not authorized");
const title = (args.title ?? "").trim();
const content = (args.content ?? "").trim();
if (!title || !content) throw new Error("Title and content are required");
const updatedAt = Date.now();
db.prepare("UPDATE notes SET title = ?, content = ?, updatedAt = ? WHERE id = ?").run(
title,
content,
updatedAt,
args.id,
);
return { updatedAt };
}
// ---- questions ---- // ---- questions ----
const QUESTION_COLUMNS = const QUESTION_COLUMNS =
@ -360,10 +401,30 @@ function questionsCreate(
const user = requireAuth(db, args.token); const user = requireAuth(db, args.token);
const id = newId(); const id = newId();
const now = Date.now(); const now = Date.now();
db.prepare( db.exec("BEGIN");
`INSERT INTO questions (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, answerCount, solved) try {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)`, db.prepare(
).run(id, args.title, args.content, args.topicId, args.unitId, user._id, user.name, now, now); `INSERT INTO questions (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, answerCount, solved)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 0)`,
).run(
id,
args.title,
args.content,
args.topicId,
args.unitId,
user._id,
user.name,
now,
now,
);
db.prepare(
"INSERT INTO votes (id, userId, targetType, targetId, value) VALUES (?, ?, 'question', ?, 1)",
).run(newId(), user._id, id);
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");
throw err;
}
return id; return id;
} }
@ -441,6 +502,30 @@ function questionsRemove(db: Db, args: { token: string; id: string }) {
db.prepare("DELETE FROM questions WHERE id = ?").run(args.id); db.prepare("DELETE FROM questions WHERE id = ?").run(args.id);
} }
function questionsUpdate(
db: Db,
args: { token: string; id: string; title: string; content: string },
) {
const user = requireAuth(db, args.token);
const question = db.prepare("SELECT id, authorId FROM questions WHERE id = ?").get(args.id) as
| { id: string; authorId: string }
| undefined;
if (!question || question.authorId !== user._id) throw new Error("Not authorized");
const title = (args.title ?? "").trim();
const content = (args.content ?? "").trim();
if (!title || !content) throw new Error("Title and content are required");
const updatedAt = Date.now();
db.prepare("UPDATE questions SET title = ?, content = ?, updatedAt = ? WHERE id = ?").run(
title,
content,
updatedAt,
args.id,
);
return { updatedAt };
}
// ---- comments ---- // ---- comments ----
const COMMENT_COLUMNS = const COMMENT_COLUMNS =
@ -566,6 +651,17 @@ function votesCast(
return { voteCount, userVote }; return { voteCount, userVote };
} }
function votesGetMyVote(
db: Db,
args: { token: string; targetType: "note" | "question"; targetId: string },
) {
const user = requireAuth(db, args.token);
const row = db
.prepare("SELECT value FROM votes WHERE userId = ? AND targetType = ? AND targetId = ?")
.get(user._id, args.targetType, args.targetId) as { value: number } | undefined;
return row?.value ?? 0;
}
// ---- details ---- // ---- details ----
function getNoteWithDetails(db: Db, args: { id: string }) { function getNoteWithDetails(db: Db, args: { id: string }) {
@ -949,18 +1045,21 @@ const handlers: Record<string, Handler> = {
"search:all": searchAll, "search:all": searchAll,
"notes:getById": notesGetById, "notes:getById": notesGetById,
"notes:remove": notesRemove, "notes:remove": notesRemove,
"notes:update": notesUpdate,
"questions:create": questionsCreate, "questions:create": questionsCreate,
"questions:list": questionsList, "questions:list": questionsList,
"questions:search": questionsSearch, "questions:search": questionsSearch,
"questions:getById": questionsGetById, "questions:getById": questionsGetById,
"questions:markSolved": questionsMarkSolved, "questions:markSolved": questionsMarkSolved,
"questions:remove": questionsRemove, "questions:remove": questionsRemove,
"questions:update": questionsUpdate,
"comments:createOnNote": commentsCreateOnNote, "comments:createOnNote": commentsCreateOnNote,
"comments:createOnQuestion": commentsCreateOnQuestion, "comments:createOnQuestion": commentsCreateOnQuestion,
"comments:listByNote": commentsListByNote, "comments:listByNote": commentsListByNote,
"comments:listByQuestion": commentsListByQuestion, "comments:listByQuestion": commentsListByQuestion,
"comments:remove": commentsRemove, "comments:remove": commentsRemove,
"votes:cast": votesCast, "votes:cast": votesCast,
"votes:getMyVote": votesGetMyVote,
"details:getNoteWithDetails": getNoteWithDetails, "details:getNoteWithDetails": getNoteWithDetails,
"details:getQuestionWithDetails": getQuestionWithDetails, "details:getQuestionWithDetails": getQuestionWithDetails,
"admin:getState": adminGetState, "admin:getState": adminGetState,

View file

@ -694,7 +694,7 @@
{/if} {/if}
</div> </div>
<div class="border-rule h-fit border p-5"> <div class="border-rule sticky top-5 h-fit border p-5">
{#if userForm} {#if userForm}
<form onsubmit={saveUser} class="space-y-5"> <form onsubmit={saveUser} class="space-y-5">
<p class="kicker">Edit account</p> <p class="kicker">Edit account</p>

View file

@ -85,7 +85,7 @@ button {
} }
.kicker { .kicker {
@apply text-faint font-sans text-[11px] font-medium tracking-[0.18em] uppercase; @apply font-sans text-[11px] font-medium tracking-[0.18em] uppercase;
} }
.btn-primary { .btn-primary {

View file

@ -80,14 +80,14 @@
<div class="flex gap-4"> <div class="flex gap-4">
<button <button
type="button" type="button"
class="kicker {sort === 'newest' ? 'text-ink' : ''}" class="kicker {sort === 'newest' ? 'text-primary' : ''}"
onclick={() => (sort = "newest")} onclick={() => (sort = "newest")}
> >
Newest Newest
</button> </button>
<button <button
type="button" type="button"
class="kicker {sort === 'top' ? 'text-ink' : ''}" class="kicker {sort === 'top' ? 'text-primary' : ''}"
onclick={() => (sort = "top")} onclick={() => (sort = "top")}
> >
Top Top

View file

@ -6,6 +6,7 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import VoteStack from "$lib/components/VoteStack.svelte"; import VoteStack from "$lib/components/VoteStack.svelte";
import Markdown from "$lib/components/Markdown.svelte"; import Markdown from "$lib/components/Markdown.svelte";
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
import { timeAgo } from "$lib/time"; import { timeAgo } from "$lib/time";
import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
@ -21,6 +22,12 @@
let deleteLoading = $state(false); let deleteLoading = $state(false);
let isAuthor = $state(false); let isAuthor = $state(false);
let editMode = $state(false);
let editTitle = $state("");
let editContent = $state("");
let editError = $state("");
let editLoading = $state(false);
onMount(async () => { onMount(async () => {
const id = page.params.id; const id = page.params.id;
const result = await query("details:getNoteWithDetails", { id }); const result = await query("details:getNoteWithDetails", { id });
@ -76,6 +83,55 @@
deleteLoading = false; deleteLoading = false;
} }
} }
function startEdit() {
if (!note) return;
editTitle = note.title;
editContent = note.content;
editError = "";
editMode = true;
}
function cancelEdit() {
editMode = false;
editError = "";
}
async function saveEdit() {
if (!note) return;
if (!editTitle.trim() || !editContent.trim()) {
editError = "Title and content are required";
return;
}
const token = getToken();
if (!token) {
editError = "You must be signed in";
return;
}
editLoading = true;
editError = "";
try {
await mutation("notes:update", {
token,
id: note._id,
title: editTitle.trim(),
content: editContent.trim(),
});
const updated = await query("details:getNoteWithDetails", { id: note._id });
if (updated) {
note = updated as NoteDoc;
topic = (updated as any).topic ?? null;
unit = (updated as any).unit ?? null;
comments = (updated as any).comments ?? [];
}
editMode = false;
} catch (err: any) {
editError = err.message ?? "Failed to save";
} finally {
editLoading = false;
}
}
</script> </script>
<svelte:head> <svelte:head>
@ -96,29 +152,73 @@
· ·
{/if}{#if topic}{topic.name}{/if} {/if}{#if topic}{topic.name}{/if}
</p> </p>
<h1 class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"> {#if editMode}
{note.title} <input
</h1> type="text"
<p class="kicker mt-3"> bind:value={editTitle}
{note.authorName} · {timeAgo(note.createdAt)} placeholder="Note title"
{#if isAuthor} class="field mt-2"
· />
<button {:else}
type="button" <h1
onclick={deleteNote} class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"
disabled={deleteLoading} >
class="text-primary hover:text-primary-dark" {note.title}
> </h1>
{deleteLoading ? "Deleting..." : "Delete"} {#if note.updatedAt > note.createdAt}
</button> <p class="kicker mt-2">Edited</p>
{/if} {/if}
</p> {/if}
{#if !editMode}
<p class="kicker mt-3">
{note.authorName} · {timeAgo(note.createdAt)}
{#if isAuthor}
·
<button
type="button"
onclick={startEdit}
class="text-secondary hover:text-secondary-dark"
>
Edit
</button>
·
<button
type="button"
onclick={deleteNote}
disabled={deleteLoading}
class="text-primary hover:text-primary-dark"
>
{deleteLoading ? "Deleting..." : "Delete"}
</button>
{/if}
</p>
{/if}
</div> </div>
</div> </div>
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed"> {#if editMode}
<Markdown content={note.content} /> <div class="mt-8">
</div> <MarkdownEditor bind:content={editContent} label="Content" rows={12} />
<div class="mt-4 flex items-center gap-3">
<button
type="button"
onclick={saveEdit}
disabled={editLoading}
class="btn-primary"
>
{editLoading ? "Saving..." : "Save changes"}
</button>
<button type="button" onclick={cancelEdit} class="kicker hover:text-ink">
Cancel
</button>
<span class="text-primary text-xs">{editError}</span>
</div>
</div>
{:else}
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed">
<Markdown content={note.content} />
</div>
{/if}
<section class="border-rule mt-12 border-t pt-8"> <section class="border-rule mt-12 border-t pt-8">
<p class="kicker mb-6">Comments ({comments.length})</p> <p class="kicker mb-6">Comments ({comments.length})</p>

View file

@ -17,7 +17,6 @@
let useCustomUnit = $state(false); let useCustomUnit = $state(false);
let error = $state(""); let error = $state("");
let loading = $state(false); let loading = $state(false);
let success = $state("");
onMount(async () => { onMount(async () => {
await initAuth(); await initAuth();
@ -33,7 +32,6 @@
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
error = ""; error = "";
success = "";
if (!title.trim() || !content.trim()) { if (!title.trim() || !content.trim()) {
error = "Title and content are required"; error = "Title and content are required";
@ -67,25 +65,15 @@
} }
} }
await mutation("notes:create", { const id = (await mutation("notes:create", {
token, token,
title: title.trim(), title: title.trim(),
content: content.trim(), content: content.trim(),
topicId: selectedTopicId, topicId: selectedTopicId,
unitId, unitId,
}); })) as string;
success = "Note published."; goto(`/notes/${id}`);
title = "";
content = "";
selectedTopicId = "";
selectedUnitId = "";
customUnit = "";
useCustomUnit = false;
setTimeout(() => {
success = "";
}, 3000);
} catch (err: any) { } catch (err: any) {
error = err.message ?? "Failed to publish note"; error = err.message ?? "Failed to publish note";
} finally { } finally {
@ -104,10 +92,6 @@
Share study notes with the Deakin community. Focus on a specific topic, not an entire unit. Share study notes with the Deakin community. Focus on a specific topic, not an entire unit.
</p> </p>
{#if success}
<p class="text-secondary mb-6 text-sm">{success}</p>
{/if}
<form onsubmit={handleSubmit} class="border-rule space-y-6 border-t pt-8"> <form onsubmit={handleSubmit} class="border-rule space-y-6 border-t pt-8">
<div> <div>
<label for="title" class="kicker mb-2 block">Title</label> <label for="title" class="kicker mb-2 block">Title</label>

View file

@ -17,7 +17,6 @@
let useCustomUnit = $state(false); let useCustomUnit = $state(false);
let error = $state(""); let error = $state("");
let loading = $state(false); let loading = $state(false);
let success = $state("");
onMount(async () => { onMount(async () => {
await initAuth(); await initAuth();
@ -33,7 +32,6 @@
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
error = ""; error = "";
success = "";
if (!title.trim() || !content.trim()) { if (!title.trim() || !content.trim()) {
error = "Title and content are required"; error = "Title and content are required";
@ -67,25 +65,15 @@
} }
} }
await mutation("questions:create", { const id = (await mutation("questions:create", {
token, token,
title: title.trim(), title: title.trim(),
content: content.trim(), content: content.trim(),
topicId: selectedTopicId, topicId: selectedTopicId,
unitId, unitId,
}); })) as string;
success = "Question posted."; goto(`/questions/${id}`);
title = "";
content = "";
selectedTopicId = "";
selectedUnitId = "";
customUnit = "";
useCustomUnit = false;
setTimeout(() => {
success = "";
}, 3000);
} catch (err: any) { } catch (err: any) {
error = err.message ?? "Failed to post question"; error = err.message ?? "Failed to post question";
} finally { } finally {
@ -104,10 +92,6 @@
Stuck on something? Ask the Deakin community for help. Stuck on something? Ask the Deakin community for help.
</p> </p>
{#if success}
<p class="text-secondary mb-6 text-sm">{success}</p>
{/if}
<form onsubmit={handleSubmit} class="border-rule space-y-6 border-t pt-8"> <form onsubmit={handleSubmit} class="border-rule space-y-6 border-t pt-8">
<div> <div>
<label for="title" class="kicker mb-2 block">Question title</label> <label for="title" class="kicker mb-2 block">Question title</label>

View file

@ -6,6 +6,7 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import VoteStack from "$lib/components/VoteStack.svelte"; import VoteStack from "$lib/components/VoteStack.svelte";
import Markdown from "$lib/components/Markdown.svelte"; import Markdown from "$lib/components/Markdown.svelte";
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
import { timeAgo } from "$lib/time"; import { timeAgo } from "$lib/time";
import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types"; import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
@ -20,6 +21,12 @@
let answerError = $state(""); let answerError = $state("");
let answerLoading = $state(false); let answerLoading = $state(false);
let editMode = $state(false);
let editTitle = $state("");
let editContent = $state("");
let editError = $state("");
let editLoading = $state(false);
onMount(async () => { onMount(async () => {
const id = page.params.id; const id = page.params.id;
const result = await query("details:getQuestionWithDetails", { id }); const result = await query("details:getQuestionWithDetails", { id });
@ -73,6 +80,55 @@
alert(err.message ?? "Failed"); alert(err.message ?? "Failed");
} }
} }
function startEdit() {
if (!question) return;
editTitle = question.title;
editContent = question.content;
editError = "";
editMode = true;
}
function cancelEdit() {
editMode = false;
editError = "";
}
async function saveEdit() {
if (!question) return;
if (!editTitle.trim() || !editContent.trim()) {
editError = "Title and content are required";
return;
}
const token = getToken();
if (!token) {
editError = "You must be signed in";
return;
}
editLoading = true;
editError = "";
try {
await mutation("questions:update", {
token,
id: question._id,
title: editTitle.trim(),
content: editContent.trim(),
});
const updated = await query("details:getQuestionWithDetails", { id: question._id });
if (updated) {
question = { ...(updated as any), answers: undefined } as QuestionDoc;
topic = (updated as any).topic ?? null;
unit = (updated as any).unit ?? null;
answers = (updated as any).answers ?? [];
}
editMode = false;
} catch (err: any) {
editError = err.message ?? "Failed to save";
} finally {
editLoading = false;
}
}
</script> </script>
<svelte:head> <svelte:head>
@ -95,28 +151,74 @@
{#if question.solved} {#if question.solved}
· Solved{/if} · Solved{/if}
</p> </p>
<h1 class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"> {#if editMode}
{question.title} <input
</h1> type="text"
<p class="kicker mt-3"> bind:value={editTitle}
{question.authorName} · {timeAgo(question.createdAt)} placeholder="Question title"
{#if isAuthor && !question.solved} class="field mt-2"
· />
<button {:else}
type="button" <h1
onclick={markSolved} class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"
class="text-secondary hover:text-secondary-dark" >
> {question.title}
Mark as solved </h1>
</button> {#if question.updatedAt > question.createdAt}
<p class="kicker mt-2">Edited</p>
{/if} {/if}
</p> {/if}
{#if !editMode}
<p class="kicker mt-3">
{question.authorName} · {timeAgo(question.createdAt)}
{#if isAuthor}
·
<button
type="button"
onclick={startEdit}
class="text-secondary hover:text-secondary-dark"
>
Edit
</button>
{#if !question.solved}
·
<button
type="button"
onclick={markSolved}
class="text-secondary hover:text-secondary-dark"
>
Mark as solved
</button>
{/if}
{/if}
</p>
{/if}
</div> </div>
</div> </div>
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed"> {#if editMode}
<Markdown content={question.content} /> <div class="mt-8">
</div> <MarkdownEditor bind:content={editContent} label="Details" rows={12} />
<div class="mt-4 flex items-center gap-3">
<button
type="button"
onclick={saveEdit}
disabled={editLoading}
class="btn-primary"
>
{editLoading ? "Saving..." : "Save changes"}
</button>
<button type="button" onclick={cancelEdit} class="kicker hover:text-ink">
Cancel
</button>
<span class="text-primary text-xs">{editError}</span>
</div>
</div>
{:else}
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed">
<Markdown content={question.content} />
</div>
{/if}
<section class="border-rule mt-12 border-t pt-8"> <section class="border-rule mt-12 border-t pt-8">
<p class="kicker mb-6">Answers ({answers.length})</p> <p class="kicker mb-6">Answers ({answers.length})</p>