mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
edited admin units dashboard
This commit is contained in:
parent
2ff5b92bbc
commit
830a04893a
14 changed files with 3743 additions and 36 deletions
98
dev-dist/sw.js
Normal file
98
dev-dist/sw.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return (
|
||||
registry[uri] ||
|
||||
new Promise((resolve) => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri =
|
||||
nextDefineUri ||
|
||||
("document" in self ? document.currentScript.src : "") ||
|
||||
location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = (depUri) => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require,
|
||||
};
|
||||
registry[uri] = Promise.all(
|
||||
depsNames.map((depName) => specialDeps[depName] || require(depName)),
|
||||
).then((deps) => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(["./workbox-7e5eb42b"], function (workbox) {
|
||||
"use strict";
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute(
|
||||
[
|
||||
{
|
||||
url: "/",
|
||||
revision: "0.agglep2qr98",
|
||||
},
|
||||
],
|
||||
{},
|
||||
);
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(
|
||||
new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
||||
allowlist: [/^\/$/],
|
||||
}),
|
||||
);
|
||||
});
|
||||
3545
dev-dist/workbox-7e5eb42b.js
Normal file
3545
dev-dist/workbox-7e5eb42b.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -255,24 +255,27 @@ function topicsGetAll(db: Db) {
|
|||
// ---- units ----
|
||||
|
||||
function unitsGetByCode(db: Db, args: { code: string }) {
|
||||
const code = args.code.toUpperCase();
|
||||
return (
|
||||
db
|
||||
.prepare("SELECT id AS _id, code, name, description FROM units WHERE code = ?")
|
||||
.get(args.code.toUpperCase()) ?? null
|
||||
.prepare(
|
||||
"SELECT id AS _id, code, code2, name, description FROM units WHERE code = ? OR code2 = ?",
|
||||
)
|
||||
.get(code, code) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function unitsGetAll(db: Db) {
|
||||
return db
|
||||
.prepare("SELECT id AS _id, code, name, description FROM units ORDER BY code ASC")
|
||||
.prepare("SELECT id AS _id, code, code2, name, description FROM units ORDER BY code ASC")
|
||||
.all();
|
||||
}
|
||||
|
||||
function unitsCreateCustom(db: Db, args: { code: string; name: string }) {
|
||||
const code = args.code.toUpperCase();
|
||||
const existing = db.prepare("SELECT id AS _id FROM units WHERE code = ?").get(code) as
|
||||
| { _id: string }
|
||||
| undefined;
|
||||
const existing = db
|
||||
.prepare("SELECT id AS _id FROM units WHERE code = ? OR code2 = ?")
|
||||
.get(code, code) as { _id: string } | undefined;
|
||||
if (existing) return existing._id;
|
||||
|
||||
const id = newId();
|
||||
|
|
@ -550,7 +553,7 @@ function getNoteWithDetails(db: Db, args: { id: string }) {
|
|||
.prepare("SELECT id AS _id, name, slug, description FROM topics WHERE id = ?")
|
||||
.get(note.topicId);
|
||||
const unit = db
|
||||
.prepare("SELECT id AS _id, code, name, description FROM units WHERE id = ?")
|
||||
.prepare("SELECT id AS _id, code, code2, name, description FROM units WHERE id = ?")
|
||||
.get(note.unitId);
|
||||
const comments = commentsListByNote(db, { noteId: note._id });
|
||||
|
||||
|
|
@ -567,7 +570,7 @@ function getQuestionWithDetails(db: Db, args: { id: string }) {
|
|||
.prepare("SELECT id AS _id, name, slug, description FROM topics WHERE id = ?")
|
||||
.get(question.topicId);
|
||||
const unit = db
|
||||
.prepare("SELECT id AS _id, code, name, description FROM units WHERE id = ?")
|
||||
.prepare("SELECT id AS _id, code, code2, name, description FROM units WHERE id = ?")
|
||||
.get(question.unitId);
|
||||
const answers = commentsListByQuestion(db, { questionId: question._id });
|
||||
|
||||
|
|
@ -635,35 +638,51 @@ function adminCompleteSetup(db: Db, args: { email: string; code: string }) {
|
|||
|
||||
function adminUnitsSave(
|
||||
db: Db,
|
||||
args: { token: string; id?: string; code: string; name: string; description?: string },
|
||||
args: {
|
||||
token: string;
|
||||
id?: string;
|
||||
code: string;
|
||||
code2?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
},
|
||||
) {
|
||||
requireAdmin(db, args.token);
|
||||
const code = (args.code ?? "").trim().toUpperCase();
|
||||
const name = (args.name ?? "").trim();
|
||||
if (!code || !name) throw new Error("Code and name are required");
|
||||
|
||||
const code2 = (args.code2 ?? "").trim().toUpperCase() || null;
|
||||
if (code2 && code2 === code)
|
||||
throw new Error("The second code must differ from the primary code");
|
||||
|
||||
const duplicate = db
|
||||
.prepare("SELECT id AS _id FROM units WHERE code = ? AND id != ?")
|
||||
.get(code, args.id ?? "") as { _id: string } | undefined;
|
||||
.prepare("SELECT id AS _id FROM units WHERE (code = ? OR code2 = ?) AND id != ?")
|
||||
.get(code, code, args.id ?? "") as { _id: string } | undefined;
|
||||
if (duplicate) throw new Error("A unit with this code already exists");
|
||||
|
||||
if (code2) {
|
||||
const duplicate2 = db
|
||||
.prepare("SELECT id AS _id FROM units WHERE (code = ? OR code2 = ?) AND id != ?")
|
||||
.get(code2, code2, args.id ?? "") as { _id: string } | undefined;
|
||||
if (duplicate2) throw new Error("A unit with this second code already exists");
|
||||
}
|
||||
|
||||
const description = args.description?.trim() || null;
|
||||
if (args.id) {
|
||||
const existing = db.prepare("SELECT id AS _id FROM units WHERE id = ?").get(args.id);
|
||||
if (!existing) throw new Error("Unit not found");
|
||||
db.prepare("UPDATE units SET code = ?, name = ?, description = ? WHERE id = ?").run(
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
args.id,
|
||||
);
|
||||
db.prepare(
|
||||
"UPDATE units SET code = ?, code2 = ?, name = ?, description = ? WHERE id = ?",
|
||||
).run(code, code2, name, description, args.id);
|
||||
return args.id;
|
||||
}
|
||||
|
||||
const id = newId();
|
||||
db.prepare("INSERT INTO units (id, code, name, description) VALUES (?, ?, ?, ?)").run(
|
||||
db.prepare("INSERT INTO units (id, code, code2, name, description) VALUES (?, ?, ?, ?, ?)").run(
|
||||
id,
|
||||
code,
|
||||
code2,
|
||||
name,
|
||||
description,
|
||||
);
|
||||
|
|
@ -797,7 +816,8 @@ function adminNotesList(db: Db, args: { token: string }) {
|
|||
n.updatedAt,
|
||||
n.voteCount,
|
||||
n.commentCount,
|
||||
u.code AS unitCode
|
||||
u.code AS unitCode,
|
||||
u.code2 AS unitCode2
|
||||
FROM notes n
|
||||
LEFT JOIN units u ON u.id = n.unitId
|
||||
ORDER BY n.createdAt DESC`,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function createSchema(database: DatabaseSync) {
|
|||
CREATE TABLE IF NOT EXISTS units (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
code2 TEXT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
|
@ -214,6 +215,11 @@ function migrate(database: DatabaseSync) {
|
|||
if (!columns.some((c) => c.name === "role")) {
|
||||
database.exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'");
|
||||
}
|
||||
|
||||
const unitColumns = database.prepare("PRAGMA table_info(units)").all() as { name: string }[];
|
||||
if (!unitColumns.some((c) => c.name === "code2")) {
|
||||
database.exec("ALTER TABLE units ADD COLUMN code2 TEXT");
|
||||
}
|
||||
}
|
||||
|
||||
function seed(database: DatabaseSync) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type TopicDoc = Doc<"topics"> & {
|
|||
|
||||
export type UnitDoc = Doc<"units"> & {
|
||||
code: string;
|
||||
code2?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
class="group border-rule flex items-baseline justify-between gap-4 border-b px-5 py-3"
|
||||
>
|
||||
<span class="text-ink group-hover:text-primary font-serif text-lg"
|
||||
>{unit.code}</span
|
||||
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}</span
|
||||
>
|
||||
<span class="text-muted truncate text-sm">{unit.name}</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
authorName: string;
|
||||
createdAt: number;
|
||||
unitCode?: string;
|
||||
unitCode2?: string;
|
||||
voteCount: number;
|
||||
commentCount: number;
|
||||
};
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
let pageError = $state("");
|
||||
|
||||
// Unit editor
|
||||
let unitForm = $state({ id: "", code: "", name: "", description: "" });
|
||||
let unitForm = $state({ id: "", code: "", code2: "", name: "", description: "" });
|
||||
let unitBusy = $state(false);
|
||||
let unitError = $state("");
|
||||
let unitSuccess = $state("");
|
||||
|
|
@ -163,6 +164,7 @@
|
|||
unitForm = {
|
||||
id: unit._id,
|
||||
code: unit.code,
|
||||
code2: unit.code2 ?? "",
|
||||
name: unit.name,
|
||||
description: unit.description ?? "",
|
||||
};
|
||||
|
|
@ -171,7 +173,7 @@
|
|||
}
|
||||
|
||||
function resetUnitForm() {
|
||||
unitForm = { id: "", code: "", name: "", description: "" };
|
||||
unitForm = { id: "", code: "", code2: "", name: "", description: "" };
|
||||
unitError = "";
|
||||
unitSuccess = "";
|
||||
}
|
||||
|
|
@ -186,6 +188,7 @@
|
|||
token: adminToken(),
|
||||
id: unitForm.id || undefined,
|
||||
code: unitForm.code,
|
||||
code2: unitForm.code2,
|
||||
name: unitForm.name,
|
||||
description: unitForm.description,
|
||||
});
|
||||
|
|
@ -534,7 +537,9 @@
|
|||
class="border-rule flex items-center justify-between gap-4 border-b py-3"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-ink font-medium">{unit.code}</p>
|
||||
<p class="text-ink font-medium">
|
||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||
</p>
|
||||
<p class="text-muted truncate text-sm">{unit.name}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-3">
|
||||
|
|
@ -558,7 +563,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<form onsubmit={saveUnit} class="border-rule h-fit space-y-5 border p-5">
|
||||
<form
|
||||
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>
|
||||
<div>
|
||||
<label for="unit-code" class="kicker mb-2 block">Code</label>
|
||||
|
|
@ -571,6 +579,18 @@
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit-code2" class="kicker mb-2 block"
|
||||
>Second code (optional)</label
|
||||
>
|
||||
<input
|
||||
id="unit-code2"
|
||||
type="text"
|
||||
bind:value={unitForm.code2}
|
||||
placeholder="e.g., SIT103"
|
||||
class="field"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="unit-name" class="kicker mb-2 block">Name</label>
|
||||
<input
|
||||
|
|
@ -747,7 +767,10 @@
|
|||
<div class="min-w-0">
|
||||
<p class="text-ink font-medium">{note.title}</p>
|
||||
<p class="text-muted mt-1 truncate text-sm">
|
||||
{note.unitCode ?? "—"} · {note.authorName} · {timeAgo(
|
||||
{note.unitCode
|
||||
? note.unitCode +
|
||||
(note.unitCode2 ? ` / ${note.unitCode2}` : "")
|
||||
: "—"} · {note.authorName} · {timeAgo(
|
||||
note.createdAt,
|
||||
)}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
class="chip {selectedUnitId === unit._id ? 'chip-active' : ''}"
|
||||
onclick={() => (selectedUnitId = unit._id)}
|
||||
>
|
||||
{unit.code}
|
||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -85,7 +85,9 @@
|
|||
<FeedRow
|
||||
href="/notes/{note._id}"
|
||||
title={note.title}
|
||||
unitCode={note.unit?.code}
|
||||
unitCode={note.unit
|
||||
? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "")
|
||||
: undefined}
|
||||
meta="{note.authorName} · {timeAgo(
|
||||
note.createdAt,
|
||||
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,9 @@
|
|||
<VoteStack count={note.voteCount} targetType="note" targetId={note._id} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="kicker">
|
||||
{#if unit}{unit.code}{/if}{#if unit && topic}
|
||||
{#if unit}{unit.code}{unit.code2
|
||||
? ` / ${unit.code2}`
|
||||
: ""}{/if}{#if unit && topic}
|
||||
·
|
||||
{/if}{#if topic}{topic.name}{/if}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,9 @@
|
|||
<select id="unit" bind:value={selectedUnitId} class="field">
|
||||
<option value="">Select a unit...</option>
|
||||
{#each units as unit}
|
||||
<option value={unit._id}>{unit.code} — {unit.name}</option>
|
||||
<option value={unit._id}
|
||||
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""} — {unit.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,9 @@
|
|||
<select id="unit" bind:value={selectedUnitId} class="field">
|
||||
<option value="">Select a unit...</option>
|
||||
{#each units as unit}
|
||||
<option value={unit._id}>{unit.code} — {unit.name}</option>
|
||||
<option value={unit._id}
|
||||
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""} — {unit.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
class="chip {selectedUnitId === unit._id ? 'chip-active' : ''}"
|
||||
onclick={() => (selectedUnitId = unit._id)}
|
||||
>
|
||||
{unit.code}
|
||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -90,7 +90,9 @@
|
|||
<FeedRow
|
||||
href="/questions/{question._id}"
|
||||
title={question.title}
|
||||
unitCode={question.unit?.code}
|
||||
unitCode={question.unit
|
||||
? question.unit.code + (question.unit.code2 ? ` / ${question.unit.code2}` : "")
|
||||
: undefined}
|
||||
meta="{question.authorName} · {timeAgo(
|
||||
question.createdAt,
|
||||
)} · {question.answerCount} answer{question.answerCount === 1 ? '' : 's'}"
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@
|
|||
<VoteStack count={question.voteCount} targetType="question" targetId={question._id} />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="kicker">
|
||||
{#if unit}{unit.code}{/if}{#if unit && topic}
|
||||
{#if unit}{unit.code}{unit.code2
|
||||
? ` / ${unit.code2}`
|
||||
: ""}{/if}{#if unit && topic}
|
||||
·
|
||||
{/if}{#if topic}{topic.name}{/if}
|
||||
{#if question.solved}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
<p class="kicker py-16">Loading</p>
|
||||
{:else if unit}
|
||||
<p class="kicker"><a href="/" class="hover:text-primary">Home</a> · Unit</p>
|
||||
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">{unit.code}</h1>
|
||||
<h1 class="text-ink mt-2 font-serif text-4xl font-medium">
|
||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||
</h1>
|
||||
<p class="text-muted mt-1 text-[15px]">{unit.name}</p>
|
||||
{#if unit.description}
|
||||
<p class="text-muted mt-2 text-sm">{unit.description}</p>
|
||||
|
|
@ -48,7 +50,7 @@
|
|||
<FeedRow
|
||||
href="/notes/{note._id}"
|
||||
title={note.title}
|
||||
unitCode={unit.code}
|
||||
unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")}
|
||||
meta="{note.authorName} · {timeAgo(
|
||||
note.createdAt,
|
||||
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}"
|
||||
|
|
@ -65,7 +67,7 @@
|
|||
<FeedRow
|
||||
href="/questions/{question._id}"
|
||||
title={question.title}
|
||||
unitCode={unit.code}
|
||||
unitCode={unit.code + (unit.code2 ? ` / ${unit.code2}` : "")}
|
||||
meta="{question.authorName} · {timeAgo(
|
||||
question.createdAt,
|
||||
)} · {question.answerCount} answer{question.answerCount === 1 ? '' : 's'}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue