v3: overhauled journal view
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

This commit is contained in:
khannurien
2026-06-27 10:57:12 +00:00
parent 4cb537f4d9
commit 01940e14be
6 changed files with 377 additions and 239 deletions

View File

@@ -7,16 +7,17 @@ import { relativeTime } from "../utils/relativeTime.ts";
import { dumpFileUrl, dumpUrl } from "../utils/urls.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { isDumpVisited, isRecent, markDumpVisited } from "../utils/visited.ts";
import { hasQuote, hasThumbnail, type JournalShape } from "../utils/journalLayout.ts";
import { VoteButton } from "./VoteButton.tsx";
import { Markdown } from "./Markdown.tsx";
import { Tooltip } from "./Tooltip.tsx";
import { PlayerContext } from "../contexts/PlayerContext.ts";
export type JournalTier = "large" | "medium" | "small";
export type { JournalShape };
interface JournalCardProps {
dump: Dump;
tier: JournalTier;
shape: JournalShape;
voteCount: number;
voted: boolean;
canVote: boolean;
@@ -26,7 +27,7 @@ interface JournalCardProps {
}
export function JournalCard(
{ dump, tier, voteCount, voted, canVote, castVote, removeVote, isOwner }:
{ dump, shape, voteCount, voted, canVote, castVote, removeVote, isOwner }:
JournalCardProps,
) {
const navigate = useNavigate();
@@ -63,6 +64,15 @@ export function JournalCard(
return rawThumbnail;
})();
// Content mode is independent of grid footprint: a thumbnailed dump reads as
// an image card, a thumbnail-less dump with a note becomes a pull-quote, and
// everything else falls back to a typographic text card.
const mode: "image" | "quote" | "text" = hasThumbnail(dump) && thumbnailUrl
? "image"
: hasQuote(dump)
? "quote"
: "text";
const fallbackIcon = dump.kind === "file"
? (() => {
const m = dump.fileMime ?? "";
@@ -72,6 +82,8 @@ export function JournalCard(
})()
: "🔗";
const embedUrl = dump.richContent?.embedUrl;
const titleLink = (
<Link
to={dumpUrl(dump)}
@@ -111,7 +123,7 @@ export function JournalCard(
);
const vote = (
<div onClick={(e) => e.stopPropagation()}>
<div className="journal-card-vote" onClick={(e) => e.stopPropagation()}>
<VoteButton
dumpId={dump.id}
count={voteCount}
@@ -123,96 +135,85 @@ export function JournalCard(
</div>
);
const embedUrl = dump.richContent?.embedUrl;
const footer = (
<div className="journal-card-footer">
{meta}
{vote}
</div>
);
if (tier === "large") {
return (
<li className="journal-card journal-card--large" onClick={handleNavigate}>
{thumbnailUrl && (
<div
className="journal-card-media"
onClick={embedUrl
? (e) => {
e.stopPropagation();
play({
kind: "embed",
embedUrl,
title: dump.richContent?.title,
type: dump.richContent?.type ?? "unknown",
});
}
: undefined}
>
<img
src={thumbnailUrl}
alt={dump.title}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
{embedUrl && (
<span className="rich-content-play-overlay" aria-hidden="true">
</span>
)}
</div>
)}
<div className="journal-card-body">
{titleLink}
{dump.comment && (
<Markdown className="journal-card-comment">{dump.comment}</Markdown>
)}
<div className="journal-card-footer">
{meta}
{vote}
</div>
</div>
</li>
);
}
const className =
`journal-card journal-card--${shape} journal-card--${mode}`;
if (tier === "medium") {
if (mode === "image") {
// feature shows its note; tall/wide/square keep the image as the subject.
const showComment = shape === "feature" && !!dump.comment;
// For playable media the whole card plays; the title link still navigates.
// Keeping the handler on the <li> means the text overlay can't cover it.
return (
<li
className="journal-card journal-card--medium"
onClick={handleNavigate}
className={className}
onClick={embedUrl
? () =>
play({
kind: "embed",
embedUrl,
title: dump.richContent?.title,
type: dump.richContent?.type ?? "unknown",
})
: handleNavigate}
>
<div className="journal-card-inner">
<div className="journal-card-icon">
{thumbnailUrl
? (
<img
src={thumbnailUrl}
alt=""
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
)
: <span>{fallbackIcon}</span>}
</div>
<div className="journal-card-body">
{titleLink}
{dump.comment && (
<Markdown className="journal-card-comment">
{dump.comment}
</Markdown>
)}
<div className="journal-card-footer">
{meta}
{vote}
</div>
</div>
<div className="journal-card-image">
<img
src={thumbnailUrl ?? undefined}
alt=""
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).style.visibility = "hidden";
}}
/>
{embedUrl && (
<span className="rich-content-play-overlay" aria-hidden="true">
</span>
)}
</div>
<div className="journal-card-overlay">
{titleLink}
{showComment && (
<Markdown className="journal-card-comment" inline>
{dump.comment!}
</Markdown>
)}
{footer}
</div>
</li>
);
}
// small
if (mode === "quote") {
return (
<li className={className} onClick={handleNavigate}>
<span className="journal-card-quotemark" aria-hidden="true"></span>
<Markdown className="journal-card-quote-body" inline>
{dump.comment!}
</Markdown>
<div className="journal-card-attribution">
{titleLink}
{footer}
</div>
</li>
);
}
// text
return (
<li className="journal-card journal-card--small" onClick={handleNavigate}>
<li className={className} onClick={handleNavigate}>
<span className="journal-card-glyph" aria-hidden="true">
{fallbackIcon}
</span>
{titleLink}
{vote}
{footer}
</li>
);
}