mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
fixed styles
This commit is contained in:
parent
efb364d7cc
commit
3130a1affa
12 changed files with 393 additions and 116 deletions
|
|
@ -78,7 +78,7 @@ define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
|
|||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "/",
|
||||
"revision": "0.7vtfun0cbi"
|
||||
"revision": "0.cgn6ae45jcc"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
||||
|
|
|
|||
|
|
@ -109,10 +109,29 @@
|
|||
<label for="markdown-editor" class="kicker mb-2 block">{label}</label>
|
||||
{/if}
|
||||
|
||||
{#if mode === "write"}
|
||||
<div
|
||||
class="border-rule bg-surface mb-2 flex flex-wrap items-center gap-1 rounded-sm border p-1"
|
||||
<div class="border-l border-t border-r border-rule w-fit">
|
||||
<button
|
||||
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("**")}>
|
||||
<strong>B</strong>
|
||||
</button>
|
||||
|
|
@ -159,30 +178,6 @@
|
|||
❝
|
||||
</button>
|
||||
</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
|
||||
id="markdown-editor"
|
||||
bind:this={textarea}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
type="button"
|
||||
class="nav-link"
|
||||
onclick={() => {
|
||||
if (!confirm("Sign out?")) return;
|
||||
logout();
|
||||
goto("/");
|
||||
}}
|
||||
|
|
@ -156,6 +157,7 @@
|
|||
type="button"
|
||||
class="nav-link block"
|
||||
onclick={() => {
|
||||
if (!confirm("Sign out?")) return;
|
||||
logout();
|
||||
goto("/");
|
||||
mobileMenuOpen = false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { mutation } from "$lib/api";
|
||||
import { query, mutation } from "$lib/api";
|
||||
import { getToken } from "$lib/stores/auth";
|
||||
import { goto } from "$app/navigation";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let {
|
||||
count = 0,
|
||||
|
|
@ -18,6 +19,16 @@
|
|||
let busy = $state(false);
|
||||
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) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -295,10 +295,30 @@ function notesCreate(
|
|||
const user = requireAuth(db, args.token);
|
||||
const id = newId();
|
||||
const now = Date.now();
|
||||
db.exec("BEGIN");
|
||||
try {
|
||||
db.prepare(
|
||||
`INSERT INTO notes (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, commentCount)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0)`,
|
||||
).run(id, args.title, args.content, args.topicId, args.unitId, user._id, user.name, now, now);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +368,27 @@ function notesRemove(db: Db, args: { token: string; id: string }) {
|
|||
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 ----
|
||||
|
||||
const QUESTION_COLUMNS =
|
||||
|
|
@ -360,10 +401,30 @@ function questionsCreate(
|
|||
const user = requireAuth(db, args.token);
|
||||
const id = newId();
|
||||
const now = Date.now();
|
||||
db.exec("BEGIN");
|
||||
try {
|
||||
db.prepare(
|
||||
`INSERT INTO questions (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, answerCount, solved)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0)`,
|
||||
).run(id, args.title, args.content, args.topicId, args.unitId, user._id, user.name, now, now);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -441,6 +502,30 @@ function questionsRemove(db: Db, args: { token: string; id: string }) {
|
|||
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 ----
|
||||
|
||||
const COMMENT_COLUMNS =
|
||||
|
|
@ -566,6 +651,17 @@ function votesCast(
|
|||
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 ----
|
||||
|
||||
function getNoteWithDetails(db: Db, args: { id: string }) {
|
||||
|
|
@ -949,18 +1045,21 @@ const handlers: Record<string, Handler> = {
|
|||
"search:all": searchAll,
|
||||
"notes:getById": notesGetById,
|
||||
"notes:remove": notesRemove,
|
||||
"notes:update": notesUpdate,
|
||||
"questions:create": questionsCreate,
|
||||
"questions:list": questionsList,
|
||||
"questions:search": questionsSearch,
|
||||
"questions:getById": questionsGetById,
|
||||
"questions:markSolved": questionsMarkSolved,
|
||||
"questions:remove": questionsRemove,
|
||||
"questions:update": questionsUpdate,
|
||||
"comments:createOnNote": commentsCreateOnNote,
|
||||
"comments:createOnQuestion": commentsCreateOnQuestion,
|
||||
"comments:listByNote": commentsListByNote,
|
||||
"comments:listByQuestion": commentsListByQuestion,
|
||||
"comments:remove": commentsRemove,
|
||||
"votes:cast": votesCast,
|
||||
"votes:getMyVote": votesGetMyVote,
|
||||
"details:getNoteWithDetails": getNoteWithDetails,
|
||||
"details:getQuestionWithDetails": getQuestionWithDetails,
|
||||
"admin:getState": adminGetState,
|
||||
|
|
|
|||
|
|
@ -694,7 +694,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-rule h-fit border p-5">
|
||||
<div class="border-rule sticky top-5 h-fit border p-5">
|
||||
{#if userForm}
|
||||
<form onsubmit={saveUser} class="space-y-5">
|
||||
<p class="kicker">Edit account</p>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ button {
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -80,14 +80,14 @@
|
|||
<div class="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
class="kicker {sort === 'newest' ? 'text-ink' : ''}"
|
||||
class="kicker {sort === 'newest' ? 'text-primary' : ''}"
|
||||
onclick={() => (sort = "newest")}
|
||||
>
|
||||
Newest
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="kicker {sort === 'top' ? 'text-ink' : ''}"
|
||||
class="kicker {sort === 'top' ? 'text-primary' : ''}"
|
||||
onclick={() => (sort = "top")}
|
||||
>
|
||||
Top
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { get } from "svelte/store";
|
||||
import VoteStack from "$lib/components/VoteStack.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||
import { timeAgo } from "$lib/time";
|
||||
import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
||||
|
||||
|
|
@ -21,6 +22,12 @@
|
|||
let deleteLoading = $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 () => {
|
||||
const id = page.params.id;
|
||||
const result = await query("details:getNoteWithDetails", { id });
|
||||
|
|
@ -76,6 +83,55 @@
|
|||
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>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -96,12 +152,35 @@
|
|||
·
|
||||
{/if}{#if topic}{topic.name}{/if}
|
||||
</p>
|
||||
<h1 class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl">
|
||||
{#if editMode}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editTitle}
|
||||
placeholder="Note title"
|
||||
class="field mt-2"
|
||||
/>
|
||||
{:else}
|
||||
<h1
|
||||
class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"
|
||||
>
|
||||
{note.title}
|
||||
</h1>
|
||||
{#if note.updatedAt > note.createdAt}
|
||||
<p class="kicker mt-2">Edited</p>
|
||||
{/if}
|
||||
{/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"
|
||||
|
|
@ -113,12 +192,33 @@
|
|||
</button>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if editMode}
|
||||
<div class="mt-8">
|
||||
<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">
|
||||
<p class="kicker mb-6">Comments ({comments.length})</p>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
let useCustomUnit = $state(false);
|
||||
let error = $state("");
|
||||
let loading = $state(false);
|
||||
let success = $state("");
|
||||
|
||||
onMount(async () => {
|
||||
await initAuth();
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
error = "";
|
||||
success = "";
|
||||
|
||||
if (!title.trim() || !content.trim()) {
|
||||
error = "Title and content are required";
|
||||
|
|
@ -67,25 +65,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
await mutation("notes:create", {
|
||||
const id = (await mutation("notes:create", {
|
||||
token,
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
topicId: selectedTopicId,
|
||||
unitId,
|
||||
});
|
||||
})) as string;
|
||||
|
||||
success = "Note published.";
|
||||
title = "";
|
||||
content = "";
|
||||
selectedTopicId = "";
|
||||
selectedUnitId = "";
|
||||
customUnit = "";
|
||||
useCustomUnit = false;
|
||||
|
||||
setTimeout(() => {
|
||||
success = "";
|
||||
}, 3000);
|
||||
goto(`/notes/${id}`);
|
||||
} catch (err: any) {
|
||||
error = err.message ?? "Failed to publish note";
|
||||
} finally {
|
||||
|
|
@ -104,10 +92,6 @@
|
|||
Share study notes with the Deakin community. Focus on a specific topic, not an entire unit.
|
||||
</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">
|
||||
<div>
|
||||
<label for="title" class="kicker mb-2 block">Title</label>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
let useCustomUnit = $state(false);
|
||||
let error = $state("");
|
||||
let loading = $state(false);
|
||||
let success = $state("");
|
||||
|
||||
onMount(async () => {
|
||||
await initAuth();
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
error = "";
|
||||
success = "";
|
||||
|
||||
if (!title.trim() || !content.trim()) {
|
||||
error = "Title and content are required";
|
||||
|
|
@ -67,25 +65,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
await mutation("questions:create", {
|
||||
const id = (await mutation("questions:create", {
|
||||
token,
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
topicId: selectedTopicId,
|
||||
unitId,
|
||||
});
|
||||
})) as string;
|
||||
|
||||
success = "Question posted.";
|
||||
title = "";
|
||||
content = "";
|
||||
selectedTopicId = "";
|
||||
selectedUnitId = "";
|
||||
customUnit = "";
|
||||
useCustomUnit = false;
|
||||
|
||||
setTimeout(() => {
|
||||
success = "";
|
||||
}, 3000);
|
||||
goto(`/questions/${id}`);
|
||||
} catch (err: any) {
|
||||
error = err.message ?? "Failed to post question";
|
||||
} finally {
|
||||
|
|
@ -104,10 +92,6 @@
|
|||
Stuck on something? Ask the Deakin community for help.
|
||||
</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">
|
||||
<div>
|
||||
<label for="title" class="kicker mb-2 block">Question title</label>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { get } from "svelte/store";
|
||||
import VoteStack from "$lib/components/VoteStack.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||
import { timeAgo } from "$lib/time";
|
||||
import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
||||
|
||||
|
|
@ -20,6 +21,12 @@
|
|||
let answerError = $state("");
|
||||
let answerLoading = $state(false);
|
||||
|
||||
let editMode = $state(false);
|
||||
let editTitle = $state("");
|
||||
let editContent = $state("");
|
||||
let editError = $state("");
|
||||
let editLoading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
const id = page.params.id;
|
||||
const result = await query("details:getQuestionWithDetails", { id });
|
||||
|
|
@ -73,6 +80,55 @@
|
|||
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>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -95,12 +151,36 @@
|
|||
{#if question.solved}
|
||||
· Solved{/if}
|
||||
</p>
|
||||
<h1 class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl">
|
||||
{#if editMode}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editTitle}
|
||||
placeholder="Question title"
|
||||
class="field mt-2"
|
||||
/>
|
||||
{:else}
|
||||
<h1
|
||||
class="text-ink mt-2 font-serif text-3xl leading-tight font-medium sm:text-4xl"
|
||||
>
|
||||
{question.title}
|
||||
</h1>
|
||||
{#if question.updatedAt > question.createdAt}
|
||||
<p class="kicker mt-2">Edited</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !editMode}
|
||||
<p class="kicker mt-3">
|
||||
{question.authorName} · {timeAgo(question.createdAt)}
|
||||
{#if isAuthor && !question.solved}
|
||||
{#if isAuthor}
|
||||
·
|
||||
<button
|
||||
type="button"
|
||||
onclick={startEdit}
|
||||
class="text-secondary hover:text-secondary-dark"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{#if !question.solved}
|
||||
·
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -110,13 +190,35 @@
|
|||
Mark as solved
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if editMode}
|
||||
<div class="mt-8">
|
||||
<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">
|
||||
<p class="kicker mb-6">Answers ({answers.length})</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue