diff --git a/components/feed/home-feed.tsx b/components/feed/home-feed.tsx
index d278a432..a033fbb3 100644
--- a/components/feed/home-feed.tsx
+++ b/components/feed/home-feed.tsx
@@ -9,8 +9,9 @@
* • Infinite scroll: 10 posts per page, fetched as a bottom sentinel nears
* the viewport. The server orders not-yet-liked posts from people you
* follow first, then everyone else — seamless, no visible boundary.
- * • When the feed is exhausted, a "new on the platform" strip (≤5 accounts
- * that joined in the last 24h) caps it off — hidden if there are none.
+ * • A "new on the platform" strip (≤5 accounts that joined in the last 24h)
+ * sits at the TOP of the scroll flow, so it scrolls out of view as you go
+ * down — hidden entirely if there are none.
*
* The card's Follow / Follow back affordance is enabled here (enableFollow);
* the server hydrates each post's viewer↔author relationship.
@@ -49,11 +50,6 @@ export function HomeFeed() {
});
if (rows.length < PAGE) {
setHasMore(false);
- // Feed exhausted → load the discovery strip once.
- feed
- .newUsers()
- .then((u) => setNewUsers(u ?? []))
- .catch(() => setNewUsers([]));
}
} catch {
setHasMore(false);
@@ -67,6 +63,15 @@ export function HomeFeed() {
void loadPage();
}, [loadPage]);
+ // Discovery strip (new accounts) loads upfront — it sits at the top of the
+ // scroll flow, not at the end.
+ useEffect(() => {
+ feed
+ .newUsers()
+ .then((u) => setNewUsers(u ?? []))
+ .catch(() => setNewUsers([]));
+ }, []);
+
// The window virtualiser needs `scrollMargin` = how far down the document the
// list begins, to map window scroll → which items are visible. `.offsetTop`
// is relative to the offsetParent (wrong inside the page layout), so measure
@@ -135,6 +140,11 @@ export function HomeFeed() {
return (
+ {/* Top of the scroll flow — scrolls out of view as the feed advances. */}
+
+
+
+
- ) : (
-
- )}
+ ) : null}
);
}
diff --git a/components/feed/new-users-strip.tsx b/components/feed/new-users-strip.tsx
index c0792113..0fdcabf3 100644
--- a/components/feed/new-users-strip.tsx
+++ b/components/feed/new-users-strip.tsx
@@ -1,21 +1,62 @@
"use client";
/**
- * NewUsersStrip — the "new on the platform" discovery row shown at the very
- * end of the home feed. Up to 5 accounts that joined in the last 24h; each
- * avatar links to the user's profile. Renders nothing when the list is empty
- * (the feed simply ends).
+ * NewUsersStrip — the "new on the platform" discovery row at the top of the
+ * home feed. Up to 5 accounts that joined in the last 24h, each rendered as a
+ * card tile. The row scrolls horizontally (native touch/trackpad swipe + a
+ * mouse drag-to-scroll affordance) and bleeds to the section's edges with a
+ * gutter so tiles peek in from the sides. Renders nothing when empty.
*/
+import { useRef } from "react";
import Link from "next/link";
import { Sparkles } from "lucide-react";
import type { UserSummary } from "@/lib/social";
import { UserAvatar } from "@/components/ui/user-avatar";
export function NewUsersStrip({ users }: { users: UserSummary[] }) {
+ const scrollRef = useRef(null);
+ // Mouse drag-to-scroll. Touch/trackpad use native overflow scrolling, so we
+ // only engage for mouse; `moved` suppresses the click that would otherwise
+ // fire after a drag (and navigate to a profile).
+ const drag = useRef({ down: false, startX: 0, startScroll: 0, moved: false });
+
if (users.length === 0) return null;
+
+ function onPointerDown(e: React.PointerEvent) {
+ if (e.pointerType !== "mouse") return;
+ const el = scrollRef.current;
+ if (!el) return;
+ drag.current = {
+ down: true,
+ startX: e.clientX,
+ startScroll: el.scrollLeft,
+ moved: false,
+ };
+ el.setPointerCapture(e.pointerId);
+ }
+ function onPointerMove(e: React.PointerEvent) {
+ if (!drag.current.down) return;
+ const el = scrollRef.current;
+ if (!el) return;
+ const dx = e.clientX - drag.current.startX;
+ if (Math.abs(dx) > 4) drag.current.moved = true;
+ el.scrollLeft = drag.current.startScroll - dx;
+ }
+ function onPointerUp(e: React.PointerEvent) {
+ if (!drag.current.down) return;
+ drag.current.down = false;
+ scrollRef.current?.releasePointerCapture(e.pointerId);
+ }
+ function onClickCapture(e: React.MouseEvent) {
+ if (drag.current.moved) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
return (
-
+
-
+
+ {/* Bleed to the section edges (-mx-5) but keep a px-5 gutter so the
+ first/last tiles have breathing room as they scroll past. */}
+