v3: added journal view to the index, code organization pass

This commit is contained in:
khannurien
2026-03-25 21:07:17 +00:00
parent c293f3e706
commit 0cb5a798c7
16 changed files with 1025 additions and 420 deletions

View File

@@ -16,7 +16,7 @@ export function AppHeader(
<header
className={`app-header${centerSlot ? " app-header--has-center" : ""}`}
>
<Link to="/" state={{ tab: "hot" }} className="app-header-brand">
<Link to="/?tab=hot" className="app-header-brand">
🚚 gerbeur
</Link>

View File

@@ -0,0 +1,187 @@
import { useContext } from "react";
import { Link, useNavigate } from "react-router";
import type { Dump } from "../model.ts";
import { API_URL } from "../config/api.ts";
import { relativeTime } from "../utils/relativeTime.ts";
import { dumpUrl } from "../utils/urls.ts";
import { isDumpVisited, isRecent, markDumpVisited } from "../utils/visited.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";
interface JournalCardProps {
dump: Dump;
tier: JournalTier;
voteCount: number;
voted: boolean;
canVote: boolean;
castVote: (id: string) => void;
removeVote: (id: string) => void;
isOwner?: boolean;
}
export function JournalCard(
{ dump, tier, voteCount, voted, canVote, castVote, removeVote, isOwner }:
JournalCardProps,
) {
const navigate = useNavigate();
const { play } = useContext(PlayerContext);
const unread = !isOwner && isRecent(dump.createdAt) && !isDumpVisited(dump.id);
function handleNavigate() {
markDumpVisited(dump.id);
navigate(dumpUrl(dump));
}
const thumbnailUrl = dump.kind === "file" &&
dump.fileMime?.startsWith("image/")
? `${API_URL}/api/files/${dump.id}?v=${dump.fileSize ?? 0}`
: (dump.richContent?.thumbnailUrl ?? null);
const fallbackIcon = dump.kind === "file"
? (() => {
const m = dump.fileMime ?? "";
if (m.startsWith("video/")) return "🎬";
if (m.startsWith("audio/")) return "🎵";
return "📄";
})()
: "🔗";
const titleLink = (
<Link
to={dumpUrl(dump)}
className="journal-card-title"
onClick={(e) => {
e.stopPropagation();
markDumpVisited(dump.id);
}}
>
{unread && <span className="unread-dot" aria-hidden="true" />}
{dump.title}
</Link>
);
const meta = (
<div className="journal-card-meta">
<Tooltip text={dump.createdAt.toLocaleString()}>
<time dateTime={dump.createdAt.toISOString()}>
{relativeTime(dump.createdAt)}
</time>
</Tooltip>
{dump.commentCount > 0 && (
<span>
{dump.commentCount}{" "}
{dump.commentCount === 1 ? "comment" : "comments"}
</span>
)}
{dump.isPrivate && isOwner && (
<span className="dump-card-private-badge">private</span>
)}
</div>
);
const vote = (
<div onClick={(e) => e.stopPropagation()}>
<VoteButton
dumpId={dump.id}
count={voteCount}
voted={voted}
disabled={!canVote}
onCast={castVote}
onRemove={removeVote}
/>
</div>
);
const embedUrl = dump.richContent?.embedUrl;
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({
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>
);
}
if (tier === "medium") {
return (
<li className="journal-card journal-card--medium" onClick={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>
</li>
);
}
// small
return (
<li className="journal-card journal-card--small" onClick={handleNavigate}>
{titleLink}
{vote}
</li>
);
}