v3: added an "in collections" section to dump pages, listing the collections a dump belongs to

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-08-08 11:41:36 +00:00
parent b84fcdd523
commit a9b94c0bfb
8 changed files with 183 additions and 52 deletions

View File

@@ -11,14 +11,17 @@ import { API_URL, VALIDATION } from "../config/api.ts";
import type {
Comment,
Dump,
Playlist,
PublicUser,
RawComment,
RawDump,
RawPlaylist,
UpdateDumpRequest,
} from "../model.ts";
import {
deserializeComment,
deserializeDump,
deserializePlaylist,
deserializePublicUser,
parseAPIResponse,
} from "../model.ts";
@@ -33,6 +36,7 @@ import RichContentCard from "../components/RichContentCard.tsx";
import FilePreview from "../components/FilePreview.tsx";
import { VoteButton } from "../components/VoteButton.tsx";
import { DumpCard } from "../components/DumpCard.tsx";
import { PlaylistCard } from "../components/PlaylistCard.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { Markdown } from "../components/Markdown.tsx";
@@ -67,6 +71,7 @@ export function Dump() {
const [comments, setComments] = useState<Comment[]>([]);
const [relatedDumps, setRelatedDumps] = useState<Dump[]>([]);
const [collections, setCollections] = useState<Playlist[]>([]);
const [titleEditing, setTitleEditing] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
@@ -82,6 +87,7 @@ export function Dump() {
removeVote,
lastDumpEvent,
lastCommentEvent,
lastPlaylistEvent,
connectionEpoch,
} = useWS();
@@ -180,6 +186,28 @@ export function Dump() {
return () => controller.abort();
}, [selectedDump, token]);
// Fetch the collections this dump appears in (public ones, plus the
// viewer's own private ones — the server does the filtering)
useEffect(() => {
if (!selectedDump) return;
const controller = new AbortController();
fetch(`${API_URL}/api/dumps/${selectedDump}/playlists`, {
signal: controller.signal,
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
.then((r) => r.json())
.then((body) => {
if (body.success) {
setCollections((body.data as RawPlaylist[]).map(deserializePlaylist));
}
})
.catch(() => {});
return () => controller.abort();
// lastPlaylistEvent: adding/removing this dump, flipping a collection's
// visibility or deleting it all change the list — playlist events are rare
// enough to just refetch on any of them.
}, [selectedDump, token, lastPlaylistEvent]);
// Scroll to and highlight a comment when navigating to #comment-{id}
useEffect(() => {
if (!location.hash.startsWith("#comment-")) return;
@@ -517,6 +545,20 @@ export function Dump() {
</section>
)}
{/* In collections (playlists containing this dump) */}
{collections.length > 0 && (
<section className="related-section collections-section">
<h2 className="related-section-title">
<Trans>In collections</Trans>
</h2>
<ul className="dump-feed">
{collections.map((collection) => (
<PlaylistCard key={collection.id} playlist={collection} />
))}
</ul>
</section>
)}
{/* Comments */}
<CommentThread
dumpId={dump.id}