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
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s
This commit is contained in:
14
src/App.css
14
src/App.css
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user