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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user