v3: fixed journal view not scrolling, fixed some user profile layout bugs, increase thumbnail size on the index
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s

This commit is contained in:
khannurien
2026-06-28 05:27:49 +00:00
parent bcbb8f5583
commit b567e390d9
10 changed files with 220 additions and 215 deletions

View File

@@ -2855,8 +2855,8 @@ body.has-fab .page-content {
.dump-card-preview,
.playlist-card-preview {
flex-shrink: 0;
width: 48px;
height: 48px;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
@@ -2875,7 +2875,7 @@ body.has-fab .page-content {
/* ── Shared card preview icon ── */
.dump-card-preview-icon,
.playlist-card-icon {
font-size: 1.4rem;
font-size: 1.8rem;
opacity: 0.7;
line-height: 1;
}
@@ -2889,14 +2889,14 @@ body.has-fab .page-content {
}
.dump-card-preview .rich-content-compact {
width: 48px;
height: 48px;
width: 80px;
height: 80px;
justify-content: center;
}
.dump-card-preview .rich-content-compact-thumbnail {
width: 48px;
height: 48px;
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 0;
border: none;

View File

@@ -3,13 +3,12 @@ import { AppHeader } from "./AppHeader.tsx";
interface PageShellProps {
children: ReactNode;
feed?: ReactNode;
centered?: boolean;
centerSlot?: ReactNode;
}
export function PageShell(
{ children, feed, centered = false, centerSlot }: PageShellProps,
{ children, centered = false, centerSlot }: PageShellProps,
) {
return (
<div className="page-shell">
@@ -19,7 +18,6 @@ export function PageShell(
>
{children}
</main>
{feed}
</div>
);
}

View File

@@ -1,7 +1,16 @@
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useState } from "react";
/**
* Observe a sentinel element and call `onLoadMore` when it scrolls into view.
*
* The sentinel is tracked via a state-backed callback ref rather than a plain
* `useRef` so the observer re-attaches whenever the underlying DOM node
* changes — e.g. when the index page swaps the active feed tab and a fresh
* sentinel `<div>` mounts in place of the old one. A static ref would keep
* observing the now-detached node and silently stop loading more.
*/
export function useInfiniteScroll(onLoadMore: () => void, enabled: boolean) {
const sentinelRef = useRef<HTMLDivElement>(null);
const [sentinel, setSentinel] = useState<HTMLDivElement | null>(null);
const handleIntersect = useCallback(
(entries: IntersectionObserverEntry[]) => {
@@ -13,14 +22,13 @@ export function useInfiniteScroll(onLoadMore: () => void, enabled: boolean) {
);
useEffect(() => {
const el = sentinelRef.current;
if (!el) return;
if (!sentinel) return;
const observer = new IntersectionObserver(handleIntersect, {
rootMargin: "200px",
});
observer.observe(el);
observer.observe(sentinel);
return () => observer.disconnect();
}, [handleIntersect]);
}, [handleIntersect, sentinel]);
return sentinelRef;
return setSentinel;
}

View File

@@ -1,5 +1,5 @@
import {
type RefObject,
type Ref,
useCallback,
useEffect,
useLayoutEffect,
@@ -41,7 +41,7 @@ interface UseUserDumpFeedResult {
state: State;
setState: React.Dispatch<React.SetStateAction<State>>;
setItems: (fn: (prev: Dump[]) => Dump[]) => void;
sentinelRef: RefObject<HTMLDivElement | null>;
sentinelRef: Ref<HTMLDivElement>;
}
/**

View File

@@ -70,37 +70,7 @@ export function UserDumps() {
const { profileUser, items: dumps, hasMore, loadingMore } = state;
return (
<PageShell
feed={dumps.length > 0 && (
<>
<ul className="dump-feed">
{dumps.map((dump) => (
<DumpCard
key={dump.id}
dump={dump}
voteCount={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
canVote={!!me}
castVote={castVote}
removeVote={removeVote}
isOwner={isOwnProfile}
/>
))}
</ul>
{hasMore && <div ref={sentinelRef} />}
{loadingMore && (
<p className="feed-loading-more">
<Trans>Loading more</Trans>
</p>
)}
{!hasMore && (
<p className="feed-end">
<Trans>You've reached the end.</Trans>
</p>
)}
</>
)}
>
<PageShell>
<ProfileSubpageHeader
username={username!}
profileUser={profileUser}
@@ -120,11 +90,41 @@ export function UserDumps() {
{createModalOpen && (
<DumpCreateModal onClose={() => setCreateModalOpen(false)} />
)}
{dumps.length === 0 && (
<p className="empty-state">
<Trans>Nothing here yet.</Trans>
</p>
)}
{dumps.length === 0
? (
<p className="empty-state">
<Trans>Nothing here yet.</Trans>
</p>
)
: (
<>
<ul className="dump-feed">
{dumps.map((dump) => (
<DumpCard
key={dump.id}
dump={dump}
voteCount={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
canVote={!!me}
castVote={castVote}
removeVote={removeVote}
isOwner={isOwnProfile}
/>
))}
</ul>
{hasMore && <div ref={sentinelRef} />}
{loadingMore && (
<p className="feed-loading-more">
<Trans>Loading more</Trans>
</p>
)}
{!hasMore && (
<p className="feed-end">
<Trans>You've reached the end.</Trans>
</p>
)}
</>
)}
</PageShell>
);
}

View File

@@ -144,56 +144,56 @@ export function UserUpvoted() {
);
return (
<PageShell
feed={visibleDumps.length > 0 && (
<>
<ul className="dump-feed">
{visibleDumps.map((dump) => {
const phase = fading[dump.id];
const extraCls = phase === "cooldown"
? "dump-card--fading"
: phase === "dismissing"
? "dump-card--dismissing"
: undefined;
return (
<DumpCard
key={dump.id}
dump={dump}
voteCount={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
canVote={!!me}
castVote={castVote}
removeVote={removeVote}
className={extraCls}
isOwner={!!me && me.id === dump.userId}
/>
);
})}
</ul>
{hasMore && <div ref={sentinelRef} />}
{loadingMore && (
<p className="feed-loading-more">
<Trans>Loading more</Trans>
</p>
)}
{!hasMore && (
<p className="feed-end">
<Trans>You've reached the end.</Trans>
</p>
)}
</>
)}
>
<PageShell>
<ProfileSubpageHeader
username={username!}
profileUser={profileUser}
title={t`Upvoted`}
/>
{visibleDumps.length === 0 && (
<p className="empty-state">
<Trans>Nothing here yet.</Trans>
</p>
)}
{visibleDumps.length === 0
? (
<p className="empty-state">
<Trans>Nothing here yet.</Trans>
</p>
)
: (
<>
<ul className="dump-feed">
{visibleDumps.map((dump) => {
const phase = fading[dump.id];
const extraCls = phase === "cooldown"
? "dump-card--fading"
: phase === "dismissing"
? "dump-card--dismissing"
: undefined;
return (
<DumpCard
key={dump.id}
dump={dump}
voteCount={voteCounts[dump.id] ?? dump.voteCount}
voted={myVotes.has(dump.id)}
canVote={!!me}
castVote={castVote}
removeVote={removeVote}
className={extraCls}
isOwner={!!me && me.id === dump.userId}
/>
);
})}
</ul>
{hasMore && <div ref={sentinelRef} />}
{loadingMore && (
<p className="feed-loading-more">
<Trans>Loading more</Trans>
</p>
)}
{!hasMore && (
<p className="feed-end">
<Trans>You've reached the end.</Trans>
</p>
)}
</>
)}
</PageShell>
);
}

View File

@@ -1,4 +1,4 @@
import type { RefObject } from "react";
import type { Ref } from "react";
import type { Dump, User } from "../../model.ts";
export interface MainFeedProps {
@@ -7,7 +7,7 @@ export interface MainFeedProps {
error: string | null;
hasMore: boolean;
loadingMore: boolean;
sentinelRef: RefObject<HTMLDivElement | null>;
sentinelRef: Ref<HTMLDivElement>;
voteCounts: Record<string, number>;
myVotes: Set<string>;
user: User | null;