change title from Notebook to DSEC Notebook

This commit is contained in:
liyunze 2026-09-05 12:36:17 +08:00
parent c135b017fb
commit 80833ad8f4
11 changed files with 227 additions and 132 deletions

View file

@ -111,7 +111,11 @@
passwordError = "";
passwordMessage = "";
try {
await mutation("users:changePassword", { token, currentPassword, newPassword });
await mutation("users:changePassword", {
token,
currentPassword,
newPassword,
});
currentPassword = "";
newPassword = "";
confirmPassword = "";
@ -131,7 +135,7 @@
</script>
<svelte:head>
<title>Account — Notebook</title>
<title>Account — DSEC Notebook</title>
</svelte:head>
<div class="page">
@ -202,8 +206,12 @@
</p>
</div>
{#if profileError}<p class="text-primary text-sm">{profileError}</p>{/if}
{#if profileMessage}<p class="text-secondary text-sm">{profileMessage}</p>{/if}
{#if profileError}<p class="text-primary text-sm">
{profileError}
</p>{/if}
{#if profileMessage}<p class="text-secondary text-sm">
{profileMessage}
</p>{/if}
<button type="submit" disabled={profileSaving} class="btn-primary">
{profileSaving ? "Saving..." : "Save profile"}
</button>
@ -250,8 +258,12 @@
required
/>
</div>
{#if passwordError}<p class="text-primary text-sm">{passwordError}</p>{/if}
{#if passwordMessage}<p class="text-secondary text-sm">{passwordMessage}</p>{/if}
{#if passwordError}<p class="text-primary text-sm">
{passwordError}
</p>{/if}
{#if passwordMessage}<p class="text-secondary text-sm">
{passwordMessage}
</p>{/if}
<button type="submit" disabled={passwordSaving} class="btn-primary">
{passwordSaving ? "Changing..." : "Change password"}
</button>

View file

@ -16,7 +16,12 @@
type WeekStat = { weekStart: number; notes: number; questions: number };
type Stats = {
weeks: WeekStat[];
totals: { notes: number; questions: number; users: number; units: number };
totals: {
notes: number;
questions: number;
users: number;
units: number;
};
};
type AdminUser = UserDoc & { noteCount: number; questionCount: number };
@ -54,18 +59,33 @@
let pageError = $state("");
// Unit editor
let unitForm = $state({ id: "", code: "", code2: "", name: "", description: "" });
let unitForm = $state({
id: "",
code: "",
code2: "",
name: "",
description: "",
});
let unitBusy = $state(false);
let unitError = $state("");
let unitSuccess = $state("");
// Account editor
let userForm = $state<{ id: string; email: string; name: string; role: string } | null>(null);
let userForm = $state<{
id: string;
email: string;
name: string;
role: string;
} | null>(null);
let userBusy = $state(false);
let userError = $state("");
// Note editor
let noteForm = $state<{ id: string; title: string; content: string } | null>(null);
let noteForm = $state<{
id: string;
title: string;
content: string;
} | null>(null);
let noteBusy = $state(false);
let noteError = $state("");
@ -207,7 +227,10 @@
unitError = "";
unitSuccess = "";
try {
await mutation("admin:unitsDelete", { token: adminToken(), id: unit._id });
await mutation("admin:unitsDelete", {
token: adminToken(),
id: unit._id,
});
units = (await query("units:getAll")) as UnitDoc[];
} catch (err: any) {
unitError = err.message ?? "Failed to delete unit";
@ -217,7 +240,12 @@
// ---- accounts ----
function startEditUser(user: AdminUser) {
userForm = { id: user._id, email: user.email, name: user.name, role: user.role };
userForm = {
id: user._id,
email: user.email,
name: user.name,
role: user.role,
};
userError = "";
}
@ -239,7 +267,9 @@
name: userForm.name,
role: userForm.role,
});
users = (await query("admin:usersList", { token: adminToken() })) as AdminUser[];
users = (await query("admin:usersList", {
token: adminToken(),
})) as AdminUser[];
resetUserForm();
} catch (err: any) {
userError = err.message ?? "Failed to update account";
@ -258,7 +288,9 @@
name: user.name,
role: user.role === "admin" ? "user" : "admin",
});
users = (await query("admin:usersList", { token: adminToken() })) as AdminUser[];
users = (await query("admin:usersList", {
token: adminToken(),
})) as AdminUser[];
} catch (err: any) {
userError = err.message ?? "Failed to change role";
}
@ -268,8 +300,13 @@
if (!confirm(`Delete ${user.email} and all of their content?`)) return;
userError = "";
try {
await mutation("admin:usersDelete", { token: adminToken(), id: user._id });
users = (await query("admin:usersList", { token: adminToken() })) as AdminUser[];
await mutation("admin:usersDelete", {
token: adminToken(),
id: user._id,
});
users = (await query("admin:usersList", {
token: adminToken(),
})) as AdminUser[];
} catch (err: any) {
userError = err.message ?? "Failed to delete account";
}
@ -299,7 +336,9 @@
title: noteForm.title,
content: noteForm.content,
});
notes = (await query("admin:notesList", { token: adminToken() })) as AdminNote[];
notes = (await query("admin:notesList", {
token: adminToken(),
})) as AdminNote[];
resetNoteForm();
} catch (err: any) {
noteError = err.message ?? "Failed to update note";
@ -312,8 +351,13 @@
if (!confirm(`Delete note "${note.title}"?`)) return;
noteError = "";
try {
await mutation("admin:notesDelete", { token: adminToken(), id: note._id });
notes = (await query("admin:notesList", { token: adminToken() })) as AdminNote[];
await mutation("admin:notesDelete", {
token: adminToken(),
id: note._id,
});
notes = (await query("admin:notesList", {
token: adminToken(),
})) as AdminNote[];
} catch (err: any) {
noteError = err.message ?? "Failed to delete note";
}
@ -321,7 +365,7 @@
</script>
<svelte:head>
<title>Admin — Notebook</title>
<title>Admin — DSEC Notebook</title>
</svelte:head>
<div class="page">
@ -473,25 +517,35 @@
<div class="grid grid-cols-2 gap-4 py-8 lg:grid-cols-4">
<div class="border-rule border p-5">
<p class="kicker">Notes</p>
<p class="text-ink mt-2 font-serif text-4xl">{stats.totals.notes}</p>
<p class="text-ink mt-2 font-serif text-4xl">
{stats.totals.notes}
</p>
</div>
<div class="border-rule border p-5">
<p class="kicker">Questions</p>
<p class="text-ink mt-2 font-serif text-4xl">{stats.totals.questions}</p>
<p class="text-ink mt-2 font-serif text-4xl">
{stats.totals.questions}
</p>
</div>
<div class="border-rule border p-5">
<p class="kicker">Accounts</p>
<p class="text-ink mt-2 font-serif text-4xl">{stats.totals.users}</p>
<p class="text-ink mt-2 font-serif text-4xl">
{stats.totals.users}
</p>
</div>
<div class="border-rule border p-5">
<p class="kicker">Units</p>
<p class="text-ink mt-2 font-serif text-4xl">{stats.totals.units}</p>
<p class="text-ink mt-2 font-serif text-4xl">
{stats.totals.units}
</p>
</div>
</div>
<div class="border-rule border-t pt-8">
<div class="flex items-center justify-between">
<p class="kicker">Posts — last {stats.weeks.length} weeks</p>
<p class="kicker">
Posts — last {stats.weeks.length} weeks
</p>
<div class="flex gap-4 text-[11px] tracking-[0.14em] uppercase">
<span class="text-ink"><span class="text-primary"></span> Notes</span>
<span class="text-ink"
@ -540,7 +594,9 @@
<p class="text-ink font-medium">
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
</p>
<p class="text-muted truncate text-sm">{unit.name}</p>
<p class="text-muted truncate text-sm">
{unit.name}
</p>
</div>
<div class="flex shrink-0 gap-3">
<button
@ -567,7 +623,9 @@
onsubmit={saveUnit}
class="border-rule sticky top-5 h-fit space-y-5 border p-5"
>
<p class="kicker">{unitForm.id ? "Edit unit" : "Add unit"}</p>
<p class="kicker">
{unitForm.id ? "Edit unit" : "Add unit"}
</p>
<div>
<label for="unit-code" class="kicker mb-2 block">Code</label>
<input
@ -643,8 +701,12 @@
<div class="border-rule border-b py-3">
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<p class="text-ink font-medium">{user.name}</p>
<p class="text-muted truncate text-sm">{user.email}</p>
<p class="text-ink font-medium">
{user.name}
</p>
<p class="text-muted truncate text-sm">
{user.email}
</p>
</div>
<span class="chip {user.role === 'admin' ? 'chip-active' : ''}">
{user.role}
@ -765,7 +827,9 @@
<div class="border-rule border-b py-3">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-ink font-medium">{note.title}</p>
<p class="text-ink font-medium">
{note.title}
</p>
<p class="text-muted mt-1 truncate text-sm">
{note.unitCode
? note.unitCode +

View file

@ -138,7 +138,7 @@
</script>
<svelte:head>
<title>Sign in — Notebook</title>
<title>Sign in — DSEC Notebook</title>
</svelte:head>
<div class="page flex min-h-[calc(100vh-220px)] items-center">
@ -302,93 +302,87 @@
</button>
</form>
{/if}
{:else}
{#if !sent}
<form onsubmit={handleSubmit} class="border-rule space-y-5 border-t pt-8">
<div>
<label for="email" class="kicker mb-2 block">Deakin email</label>
<input
id="email"
type="email"
bind:value={email}
placeholder="@deakin.edu.au"
autocomplete="email"
class="field"
required
/>
<p class="text-faint mt-2 text-xs">
We'll email a reset code to your @deakin.edu.au address.
</p>
</div>
{#if error}
<p class="text-primary text-sm">{error}</p>
{/if}
<button type="submit" disabled={loading} class="btn-primary w-full">
{loading ? "Sending code..." : "Send reset code"}
</button>
<button
type="button"
onclick={() => switchMode("signin")}
class="text-muted hover:text-ink text-sm"
>
Back to sign in
</button>
</form>
{:else}
<form onsubmit={handleSubmit} class="border-rule space-y-5 border-t pt-8">
<p class="text-muted text-sm">
We sent a 6-digit code to <span class="text-ink">{email}</span>. It expires
in 10 minutes.
{:else if !sent}
<form onsubmit={handleSubmit} class="border-rule space-y-5 border-t pt-8">
<div>
<label for="email" class="kicker mb-2 block">Deakin email</label>
<input
id="email"
type="email"
bind:value={email}
placeholder="@deakin.edu.au"
autocomplete="email"
class="field"
required
/>
<p class="text-faint mt-2 text-xs">
We'll email a reset code to your @deakin.edu.au address.
</p>
</div>
<div>
<label for="code" class="kicker mb-2 block">Reset code</label>
<input
id="code"
type="text"
inputmode="numeric"
maxlength="6"
autocomplete="one-time-code"
bind:value={code}
placeholder="000000"
class="field"
required
/>
</div>
{#if error}
<p class="text-primary text-sm">{error}</p>
{/if}
<div>
<label for="password" class="kicker mb-2 block">New password</label>
<input
id="password"
type="password"
bind:value={password}
placeholder="At least 8 characters"
autocomplete="new-password"
class="field"
required
/>
</div>
<button type="submit" disabled={loading} class="btn-primary w-full">
{loading ? "Sending code..." : "Send reset code"}
</button>
{#if error}
<p class="text-primary text-sm">{error}</p>
{/if}
<button
type="button"
onclick={() => switchMode("signin")}
class="text-muted hover:text-ink text-sm"
>
Back to sign in
</button>
</form>
{:else}
<form onsubmit={handleSubmit} class="border-rule space-y-5 border-t pt-8">
<p class="text-muted text-sm">
We sent a 6-digit code to <span class="text-ink">{email}</span>. It expires in
10 minutes.
</p>
<button type="submit" disabled={loading} class="btn-primary w-full">
{loading ? "Resetting..." : "Reset password"}
</button>
<div>
<label for="code" class="kicker mb-2 block">Reset code</label>
<input
id="code"
type="text"
inputmode="numeric"
maxlength="6"
autocomplete="one-time-code"
bind:value={code}
placeholder="000000"
class="field"
required
/>
</div>
<button
type="button"
onclick={goBack}
class="text-muted hover:text-ink text-sm"
>
Use a different email
</button>
</form>
{/if}
<div>
<label for="password" class="kicker mb-2 block">New password</label>
<input
id="password"
type="password"
bind:value={password}
placeholder="At least 8 characters"
autocomplete="new-password"
class="field"
required
/>
</div>
{#if error}
<p class="text-primary text-sm">{error}</p>
{/if}
<button type="submit" disabled={loading} class="btn-primary w-full">
{loading ? "Resetting..." : "Reset password"}
</button>
<button type="button" onclick={goBack} class="text-muted hover:text-ink text-sm">
Use a different email
</button>
</form>
{/if}
</div>
</div>

View file

@ -18,7 +18,10 @@
onMount(async () => {
const [n, u] = await Promise.all([query("notes:list", {}), query("units:getAll")]);
const unitMap = new Map((u as UnitDoc[]).map((unit) => [unit._id, unit]));
notes = (n as NoteDoc[]).map((note) => ({ ...note, unit: unitMap.get(note.unitId) }));
notes = (n as NoteDoc[]).map((note) => ({
...note,
unit: unitMap.get(note.unitId),
}));
units = u as UnitDoc[];
loading = false;
});
@ -43,7 +46,7 @@
</script>
<svelte:head>
<title>Notes — Notebook</title>
<title>Notes — DSEC Notebook</title>
</svelte:head>
<div class="page">

View file

@ -128,7 +128,7 @@
</script>
<svelte:head>
<title>Post a note — Notebook</title>
<title>Post a note — DSEC Notebook</title>
</svelte:head>
<svelte:window onbeforeunload={handleBeforeUnload} />

View file

@ -128,7 +128,7 @@
</script>
<svelte:head>
<title>Ask a question — Notebook</title>
<title>Ask a question — DSEC Notebook</title>
</svelte:head>
<svelte:window onbeforeunload={handleBeforeUnload} />

View file

@ -45,7 +45,7 @@
</script>
<svelte:head>
<title>Questions — Notebook</title>
<title>Questions — DSEC Notebook</title>
</svelte:head>
<div class="page">

View file

@ -15,7 +15,9 @@
searchQuery = new URL(window.location.href).searchParams.get("q") ?? "";
if (searchQuery) {
ranQuery = true;
results = (await query("search:all", { query: searchQuery })) as SearchResult[];
results = (await query("search:all", {
query: searchQuery,
})) as SearchResult[];
}
loading = false;
});
@ -25,7 +27,7 @@
</script>
<svelte:head>
<title>{searchQuery ? `Search: ${searchQuery}` : "Search"} — Notebook</title>
<title>{searchQuery ? `Search: ${searchQuery}` : "Search"} DSEC Notebook</title>
</svelte:head>
<div class="page">
@ -110,7 +112,9 @@
{/if}
{:else}
{#if unitResults.length === 0}
<p class="text-muted text-sm">No results found for “{searchQuery}”.</p>
<p class="text-muted text-sm">
No results found for “{searchQuery}”.
</p>
{/if}
{/each}
{/if}

View file

@ -40,15 +40,19 @@
</script>
<svelte:head>
<title>{topic?.name ?? "Topic"} — Notebook</title>
<title>{topic?.name ?? "Topic"} DSEC Notebook</title>
</svelte:head>
<div class="page">
{#if loading}
<p class="kicker py-16">Loading</p>
{:else if topic}
<p class="kicker"><a href="/" class="hover:text-primary">Home</a> · Topic</p>
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">{topic.name}</h1>
<p class="kicker">
<a href="/" class="hover:text-primary">Home</a> · Topic
</p>
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">
{topic.name}
</h1>
{#if topic.description}
<p class="text-muted mt-2 text-[15px]">{topic.description}</p>
{/if}

View file

@ -21,7 +21,7 @@
</script>
<svelte:head>
<title>Units — Notebook</title>
<title>Units — DSEC Notebook</title>
</svelte:head>
<div class="page">
@ -47,7 +47,9 @@
{:else if units.length === 0}
<p class="text-muted py-16 text-sm">No units yet.</p>
{:else if visible.length === 0}
<p class="text-muted py-16 text-sm">No units match “{searchQuery.trim()}”.</p>
<p class="text-muted py-16 text-sm">
No units match “{searchQuery.trim()}”.
</p>
{:else}
<div class="grid grid-cols-1 gap-3 pt-6 sm:grid-cols-2 lg:grid-cols-3">
{#each visible as unit (unit._id)}

View file

@ -43,7 +43,9 @@
tab = "posts";
void (async () => {
const result = (await query("users:getPublicProfile", { id })) as Profile | null;
const result = (await query("users:getPublicProfile", {
id,
})) as Profile | null;
if (cancelled) return;
profile = result;
loading = false;
@ -60,7 +62,7 @@
</script>
<svelte:head>
<title>{profile?.name ?? "Profile"} — Notebook</title>
<title>{profile?.name ?? "Profile"} DSEC Notebook</title>
</svelte:head>
<div class="page">
@ -71,7 +73,9 @@
<Avatar src={profile.avatarUrl} name={profile.name} size="xl" />
<div>
<p class="kicker">Contributor</p>
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">{profile.name}</h1>
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">
{profile.name}
</h1>
<p class="text-muted mt-2 text-sm">
Member since {new Date(profile._creationTime).toLocaleDateString(undefined, {
month: "long",
@ -83,7 +87,9 @@
<section class="border-rule mt-10 grid grid-cols-2 border-y sm:grid-cols-4">
<div class="border-rule border-r px-4 py-5 first:pl-0">
<p class="text-ink font-serif text-3xl">{profile.totalContributions}</p>
<p class="text-ink font-serif text-3xl">
{profile.totalContributions}
</p>
<p class="kicker text-muted mt-1">Contributions</p>
</div>
<div class="border-rule border-r px-4 py-5">
@ -91,11 +97,15 @@
<p class="kicker text-muted mt-1">Notes</p>
</div>
<div class="border-rule border-r px-4 py-5">
<p class="text-ink font-serif text-3xl">{profile.questionCount}</p>
<p class="text-ink font-serif text-3xl">
{profile.questionCount}
</p>
<p class="kicker text-muted mt-1">Questions</p>
</div>
<div class="px-4 py-5">
<p class="text-ink font-serif text-3xl">{profile.commentCount}</p>
<p class="text-ink font-serif text-3xl">
{profile.commentCount}
</p>
<p class="kicker text-muted mt-1">Comments</p>
</div>
</section>
@ -161,7 +171,9 @@
<p class="text-ink mt-2 text-sm leading-relaxed">
{previewMarkdown(comment.content)}
</p>
<p class="text-faint mt-1 text-xs">{timeAgo(comment.createdAt)}</p>
<p class="text-faint mt-1 text-xs">
{timeAgo(comment.createdAt)}
</p>
</article>
{/each}
{/if}