h8r4npp3p: Add the new-posts pill, feed list motion and app-wide reduced-motion switch-off
This commit is contained in:
@@ -236,7 +236,8 @@ export function Feed({
|
||||
function showNew() {
|
||||
setPosts((cur) => mergePosts(cur, pending));
|
||||
setPending([]);
|
||||
list.current?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
const calm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
list.current?.scrollTo({ top: 0, behavior: calm ? "auto" : "smooth" });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import { canDelete, type CardPost } from "@/lib/feed-model";
|
||||
import { Composer, type ComposerProps } from "./Composer";
|
||||
import { GrowButton } from "./Buttons";
|
||||
import { PostCard } from "./PostCard";
|
||||
|
||||
export type FeedStatus = "loading" | "ready" | "error";
|
||||
@@ -33,6 +34,24 @@ function Spinner() {
|
||||
return <span className="spinner" role="status" aria-label="Loading" />;
|
||||
}
|
||||
|
||||
/** Shimmer placeholder card shown while the next page loads. */
|
||||
function SkeletonCard() {
|
||||
return (
|
||||
<div className="feed-skel" role="status" aria-label="Loading more posts" data-testid="feed-skeleton">
|
||||
<div className="feed-skel-head">
|
||||
<span className="feed-skel-dot" />
|
||||
<span className="feed-skel-lines">
|
||||
<span className="feed-skel-line feed-skel-w40" />
|
||||
<span className="feed-skel-line feed-skel-w25" />
|
||||
</span>
|
||||
</div>
|
||||
<span className="feed-skel-line feed-skel-w100" />
|
||||
<span className="feed-skel-line feed-skel-w80" />
|
||||
<span className="feed-skel-media" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeedPanel({ children, composer }: { children: ReactNode; composer?: ReactNode }) {
|
||||
return (
|
||||
<section className="tile panel feed" aria-labelledby="h-feed">
|
||||
@@ -69,16 +88,17 @@ export function FeedView(p: FeedViewProps) {
|
||||
<FeedPanel composer={p.composer ? <Composer {...p.composer} /> : null}>
|
||||
<div ref={p.listRef} className="feed-list panel-scroll fade-edges">
|
||||
{p.newCount > 0 ? (
|
||||
<button type="button" className="new-posts" onClick={p.onShowNew}>
|
||||
{p.newCount === 1 ? "1 new post" : `${p.newCount} new posts`}
|
||||
</button>
|
||||
<div className="feed-pill">
|
||||
<GrowButton label={p.newCount === 1 ? "1 new post" : `${p.newCount} new posts`} onClick={p.onShowNew} />
|
||||
</div>
|
||||
) : null}
|
||||
{p.posts.length === 0 ? (
|
||||
<p className="feed-message">No posts yet.</p>
|
||||
) : (
|
||||
p.posts.map((post) => <PostCard
|
||||
p.posts.map((post, i) => <PostCard
|
||||
key={post.key}
|
||||
post={post}
|
||||
index={i}
|
||||
now={p.now}
|
||||
onToggleLike={p.onToggleLike}
|
||||
likeBusy={p.likeBusy?.has(post.key)}
|
||||
@@ -96,11 +116,7 @@ export function FeedView(p: FeedViewProps) {
|
||||
<button type="button" onClick={p.onRetry}>Try again</button>
|
||||
</div>
|
||||
) : null}
|
||||
{p.loadingMore ? (
|
||||
<div className="feed-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : null}
|
||||
{p.loadingMore ? <SkeletonCard /> : null}
|
||||
<div ref={p.sentinelRef} className="sentinel" aria-hidden="true" />
|
||||
</div>
|
||||
</FeedPanel>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
import { HexAvatar } from "@/components/HexAvatar";
|
||||
import { CommentsSection } from "@/components/CommentsSection";
|
||||
import { VideoPlayer } from "@/components/VideoPlayer";
|
||||
@@ -10,6 +10,8 @@ import { formatDuration, relativeTime, type CardMedia, type CardPost } from "@/l
|
||||
export interface PostCardProps {
|
||||
post: CardPost;
|
||||
now?: number;
|
||||
/** Position in the list; the first cards rise in one after another. */
|
||||
index?: number;
|
||||
onToggleLike?: (post: CardPost) => void;
|
||||
likeBusy?: boolean;
|
||||
likeError?: boolean;
|
||||
@@ -89,7 +91,7 @@ function PostVideo({ media, author }: { media: CardMedia; author: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSignedOut, canDelete, onDelete, deleteBusy, deleteError }: PostCardProps) {
|
||||
export function PostCard({ post, now, index = 0, onToggleLike, likeBusy, likeError, onSignedOut, canDelete, onDelete, deleteBusy, deleteError }: PostCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [menu, setMenu] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
@@ -111,7 +113,7 @@ export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSigne
|
||||
}, [menu]);
|
||||
|
||||
return (
|
||||
<article className="post" data-testid="post-card">
|
||||
<article className="post" data-testid="post-card" style={{ "--i": index < 8 ? index : 0 } as CSSProperties}>
|
||||
<header className="post-head">
|
||||
<HexAvatar name={post.authorName} pictureUrl={post.authorPictureUrl} size="lg" />
|
||||
<div className="post-who">
|
||||
|
||||
Reference in New Issue
Block a user