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([{
|
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("/"), {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue