v3: added content slugs, fixed real-time updates in client, added @mentions across the app, added new file selector and drop zone

This commit is contained in:
khannurien
2026-03-22 16:06:26 +00:00
parent 39a0cc397e
commit 34e908d1bc
42 changed files with 2170 additions and 628 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate, useParams } from "react-router";
import { dumpUrl } from "../utils/urls.ts";
import { AddToPlaylistModal } from "../components/AddToPlaylistModal.tsx";
import { API_URL } from "../config/api.ts";
@@ -22,6 +23,7 @@ import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { Markdown } from "../components/Markdown.tsx";
import { CommentThread } from "../components/CommentThread.tsx";
import { Tooltip } from "../components/Tooltip.tsx";
import { friendlyFetchError } from "../utils/apiError.ts";
type DumpState =
@@ -115,6 +117,18 @@ export function Dump() {
.catch(() => {});
}, [selectedDump, token]);
// Scroll to and highlight a comment when navigating to #comment-{id}
useEffect(() => {
if (!location.hash.startsWith("#comment-")) return;
const id = location.hash.slice(1);
const el = document.getElementById(id);
if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "start" });
el.classList.add("comment-node--highlight");
const t = setTimeout(() => el.classList.remove("comment-node--highlight"), 2000);
return () => clearTimeout(t);
}, [comments, location.hash]);
// React to WS comment events
useEffect(() => {
if (!lastCommentEvent || lastCommentEvent.dumpId !== selectedDump) return;
@@ -135,6 +149,14 @@ export function Dump() {
: c
)
);
} else if (
lastCommentEvent.type === "updated" && lastCommentEvent.comment
) {
setComments((prev) =>
prev.map((c) =>
c.id === lastCommentEvent.comment!.id ? lastCommentEvent.comment! : c
)
);
}
}, [lastCommentEvent, selectedDump]);
@@ -213,13 +235,21 @@ export function Dump() {
</Link>
)
: <span className="dump-op-link"></span>}
<time
className="dump-card-date"
dateTime={dump.createdAt.toISOString()}
title={dump.createdAt.toLocaleString()}
>
{relativeTime(dump.createdAt)}
</time>
<Tooltip text={dump.createdAt.toLocaleString()}>
<time
className="dump-card-date"
dateTime={dump.createdAt.toISOString()}
>
{relativeTime(dump.createdAt)}
</time>
</Tooltip>
{dump.updatedAt && (
<Tooltip text={`Edited ${dump.updatedAt.toLocaleString()}`}>
<span className="dump-edited-label">
edited {relativeTime(dump.updatedAt)}
</span>
</Tooltip>
)}
{dump.isPrivate && (
<span className="dump-card-private-badge">private</span>
)}
@@ -251,7 +281,7 @@ export function Dump() {
{/* Actions */}
<div className="dump-actions">
{canEdit && <Link to={`/dumps/${dump.id}/edit`}>Edit</Link>}
{canEdit && <Link to={`${dumpUrl(dump)}/edit`}>Edit</Link>}
<Link to="/"> Back to all dumps</Link>
</div>
@@ -271,6 +301,10 @@ export function Dump() {
c.id === id ? { ...c, deleted: true, body: "" } : c
)
)}
onCommentUpdated={(updated) =>
setComments((prev) =>
prev.map((c) => (c.id === updated.id ? updated : c))
)}
/>
</div>
{playlistModalOpen && (