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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,9 @@
tab = "posts"; tab = "posts";
void (async () => { 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; if (cancelled) return;
profile = result; profile = result;
loading = false; loading = false;
@ -60,7 +62,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{profile?.name ?? "Profile"} — Notebook</title> <title>{profile?.name ?? "Profile"} DSEC Notebook</title>
</svelte:head> </svelte:head>
<div class="page"> <div class="page">
@ -71,7 +73,9 @@
<Avatar src={profile.avatarUrl} name={profile.name} size="xl" /> <Avatar src={profile.avatarUrl} name={profile.name} size="xl" />
<div> <div>
<p class="kicker">Contributor</p> <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"> <p class="text-muted mt-2 text-sm">
Member since {new Date(profile._creationTime).toLocaleDateString(undefined, { Member since {new Date(profile._creationTime).toLocaleDateString(undefined, {
month: "long", month: "long",
@ -83,7 +87,9 @@
<section class="border-rule mt-10 grid grid-cols-2 border-y sm:grid-cols-4"> <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"> <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> <p class="kicker text-muted mt-1">Contributions</p>
</div> </div>
<div class="border-rule border-r px-4 py-5"> <div class="border-rule border-r px-4 py-5">
@ -91,11 +97,15 @@
<p class="kicker text-muted mt-1">Notes</p> <p class="kicker text-muted mt-1">Notes</p>
</div> </div>
<div class="border-rule border-r px-4 py-5"> <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> <p class="kicker text-muted mt-1">Questions</p>
</div> </div>
<div class="px-4 py-5"> <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> <p class="kicker text-muted mt-1">Comments</p>
</div> </div>
</section> </section>
@ -161,7 +171,9 @@
<p class="text-ink mt-2 text-sm leading-relaxed"> <p class="text-ink mt-2 text-sm leading-relaxed">
{previewMarkdown(comment.content)} {previewMarkdown(comment.content)}
</p> </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> </article>
{/each} {/each}
{/if} {/if}