v3: overhauled journal view
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user