rnxcmkidd: Add the viewport-fit bento shell and a dev feed preview with fixtures
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
// Dev-only: the real signed-in shell with fixture data and no network calls.
|
||||
// Likes, search and the composer work on local state so the panels can be
|
||||
// checked by hand. Used by src/app/dev/feed/page.tsx.
|
||||
import { useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { DiscoverRailsView } from "@/components/DiscoverRails";
|
||||
import { ExpertSearchView } from "@/components/ExpertSearch";
|
||||
import { FeedView } from "@/components/FeedView";
|
||||
import { toCardPosts, type CardPost } from "@/lib/feed-model";
|
||||
import {
|
||||
FIXTURE_CATEGORIES,
|
||||
FIXTURE_DISCOVER,
|
||||
FIXTURE_FEED_ITEMS,
|
||||
FIXTURE_NOW,
|
||||
FIXTURE_PERSON,
|
||||
FIXTURE_SEARCH_RESULTS,
|
||||
} from "./fixtures";
|
||||
|
||||
export function FeedPreview() {
|
||||
const [posts, setPosts] = useState<CardPost[]>(() => toCardPosts(FIXTURE_FEED_ITEMS));
|
||||
const [draft, setDraft] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [category, setCategory] = useState<string | null>(null);
|
||||
|
||||
// With no query or category, show every fixture result so the list scrolls.
|
||||
const results = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return q ? FIXTURE_SEARCH_RESULTS.filter((e) => e.displayName.toLowerCase().includes(q)) : FIXTURE_SEARCH_RESULTS;
|
||||
}, [query]);
|
||||
|
||||
const toggleLike = (post: CardPost) =>
|
||||
setPosts((cur) =>
|
||||
cur.map((p) =>
|
||||
p.key === post.key ? { ...p, isLiked: !p.isLiked, likesCount: p.likesCount + (p.isLiked ? -1 : 1) } : p,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
person={FIXTURE_PERSON}
|
||||
search={
|
||||
<ExpertSearchView
|
||||
query={query}
|
||||
onQuery={setQuery}
|
||||
categories={FIXTURE_CATEGORIES}
|
||||
activeCategory={category}
|
||||
onCategory={setCategory}
|
||||
status="ready"
|
||||
results={results}
|
||||
/>
|
||||
}
|
||||
feed={
|
||||
<FeedView
|
||||
posts={posts}
|
||||
status="ready"
|
||||
loadingMore={false}
|
||||
loadMoreFailed={false}
|
||||
newCount={0}
|
||||
onRetry={() => {}}
|
||||
onShowNew={() => {}}
|
||||
onToggleLike={toggleLike}
|
||||
feedUserId={FIXTURE_PERSON.feedUserId}
|
||||
onDelete={(post) => setPosts((cur) => cur.filter((p) => p.key !== post.key))}
|
||||
composer={{ value: draft, busy: false, error: false, onChange: setDraft, onSubmit: () => setDraft("") }}
|
||||
now={FIXTURE_NOW}
|
||||
/>
|
||||
}
|
||||
discover={<DiscoverRailsView data={FIXTURE_DISCOVER} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Fixture data for the dev-only preview routes (/dev/feed). Nothing here is
|
||||
// real: no Minnect person, post, token or URL. Images are inline SVG data URIs
|
||||
// so the preview never calls the network. Later tasks extend this file.
|
||||
import type { Person } from "@/lib/auth-client";
|
||||
import type { DiscoverCategory, DiscoverData, DiscoverExpert, DiscoverMinnect } from "@/lib/discover";
|
||||
import type { ExpertResult } from "@/lib/experts-search";
|
||||
|
||||
/** Fixed "now" so relative times in the preview are stable. */
|
||||
export const FIXTURE_NOW = Date.parse("2026-10-19T12:00:00Z");
|
||||
|
||||
/** A 16:9 gradient placeholder image, drawn with hsl() so no fetch happens. */
|
||||
export function placeholderImage(hue: number, label = ""): string {
|
||||
const svg =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">` +
|
||||
`<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">` +
|
||||
`<stop offset="0" stop-color="hsl(${hue} 45% 32%)"/><stop offset="1" stop-color="hsl(${(hue + 40) % 360} 40% 14%)"/>` +
|
||||
`</linearGradient></defs><rect width="1280" height="720" fill="url(#g)"/>` +
|
||||
(label
|
||||
? `<text x="64" y="660" font-family="system-ui,sans-serif" font-size="56" font-weight="700" fill="hsl(0 0% 100% / .85)">${label}</text>`
|
||||
: "") +
|
||||
`</svg>`;
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
export const FIXTURE_FEED_USER_ID = "dev-user-1";
|
||||
|
||||
export const FIXTURE_PERSON: Person = {
|
||||
name: "Jordan Rivera",
|
||||
email: "jordan.rivera@example.com",
|
||||
pictureUrl: null,
|
||||
feedUserId: FIXTURE_FEED_USER_ID,
|
||||
};
|
||||
|
||||
const author = (id: string, displayName: string, picture: string | null = null) => ({
|
||||
id,
|
||||
displayName,
|
||||
profilePictureUrl: picture,
|
||||
});
|
||||
|
||||
/** Raw feed items in the shape /api/feed returns; run them through toCardPosts. */
|
||||
export const FIXTURE_FEED_ITEMS: unknown[] = [
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-19T11:42:00Z",
|
||||
likesCount: 4,
|
||||
commentsCount: 1,
|
||||
post: {
|
||||
id: "p1",
|
||||
content: "Shipped the new onboarding flow this morning. The best feedback so far came from a five minute call, not a survey.",
|
||||
author: author(FIXTURE_FEED_USER_ID, "Jordan Rivera"),
|
||||
mediaItems: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-19T10:15:00Z",
|
||||
likesCount: 128,
|
||||
commentsCount: 23,
|
||||
isLiked: true,
|
||||
post: {
|
||||
id: "p2",
|
||||
content: "Three questions I ask before every pricing change:\n• Who pays more?\n• Who leaves?\n• What do we learn either way?",
|
||||
author: author("a2", "Priya Natarajan"),
|
||||
mediaItems: [{ mediaType: "image", imageUrl: placeholderImage(352, "Pricing review"), order: 0 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-19T08:30:00Z",
|
||||
likesCount: 56,
|
||||
commentsCount: 7,
|
||||
post: {
|
||||
id: "p3",
|
||||
content: "A short walkthrough of how we run weekly reviews with a remote team.",
|
||||
author: author("a3", "Marcus Bell"),
|
||||
mediaItems: [{ mediaType: "video", thumbnailUrl: placeholderImage(210, "Weekly reviews"), order: 0 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-18T19:05:00Z",
|
||||
likesCount: 9,
|
||||
commentsCount: 0,
|
||||
post: {
|
||||
id: "p4",
|
||||
content: "Reading list for anyone moving from engineering into product: start with the customer interviews, then the numbers.",
|
||||
author: author("a4", "Elena Kovac"),
|
||||
mediaItems: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-18T14:40:00Z",
|
||||
likesCount: 312,
|
||||
commentsCount: 41,
|
||||
post: {
|
||||
id: "p5",
|
||||
content: "Fundraising tip: send the update even when the month was bad. Especially when the month was bad.",
|
||||
author: author("a5", "Samuel Okafor"),
|
||||
mediaItems: [{ mediaType: "image", imageUrl: placeholderImage(30, "Monthly update"), order: 0 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "post",
|
||||
publishedAt: "2026-10-17T09:00:00Z",
|
||||
likesCount: 17,
|
||||
commentsCount: 3,
|
||||
post: {
|
||||
id: "p6",
|
||||
content: "Your first ten customers will tell you what the product is. Listen before you build the next feature.",
|
||||
author: author("a6", "Hannah Lindqvist"),
|
||||
mediaItems: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const expert = (id: string, displayName: string, byline: string, rating: number, reviewsCount: number): DiscoverExpert => ({
|
||||
id,
|
||||
slug: displayName.replace(/\s+/g, ""),
|
||||
displayName,
|
||||
byline,
|
||||
pictureUrl: null,
|
||||
rating,
|
||||
reviewsCount,
|
||||
available: true,
|
||||
location: { city: null, country: null },
|
||||
});
|
||||
|
||||
export const FIXTURE_FEATURED: DiscoverExpert[] = [
|
||||
expert("e1", "Priya Natarajan", "Pricing and growth advisor", 4.9, 212),
|
||||
expert("e2", "Marcus Bell", "Remote team coach", 4.8, 96),
|
||||
expert("e3", "Elena Kovac", "Product leadership", 4.7, 58),
|
||||
expert("e4", "Samuel Okafor", "Seed-stage fundraising", 5.0, 140),
|
||||
expert("e5", "Hannah Lindqvist", "Customer research", 4.6, 33),
|
||||
];
|
||||
|
||||
const minnect = (id: string, who: DiscoverExpert, question: string, views: number, hue: number): DiscoverMinnect => ({
|
||||
id,
|
||||
question,
|
||||
views,
|
||||
thumbnailUrl: placeholderImage(hue),
|
||||
media: null,
|
||||
expert: { slug: who.slug, displayName: who.displayName, pictureUrl: null, byline: who.byline, rating: who.rating },
|
||||
});
|
||||
|
||||
export const FIXTURE_TRENDING: DiscoverMinnect[] = [
|
||||
minnect("m1", FIXTURE_FEATURED[0], "How do I raise prices without losing my best customers?", 12840, 352),
|
||||
minnect("m2", FIXTURE_FEATURED[3], "What should a pre-seed deck show in the first three slides?", 9310, 30),
|
||||
minnect("m3", FIXTURE_FEATURED[1], "How often should a remote team meet in person?", 6120, 210),
|
||||
minnect("m4", FIXTURE_FEATURED[2], "When is the right time to hire a first product manager?", 4470, 270),
|
||||
minnect("m5", FIXTURE_FEATURED[4], "How many customer interviews are enough before building?", 2015, 150),
|
||||
];
|
||||
|
||||
export const FIXTURE_CATEGORIES: DiscoverCategory[] = [
|
||||
{ id: 1, name: "Business", icon: null, sortOrder: 1 },
|
||||
{ id: 2, name: "Coaching", icon: null, sortOrder: 2 },
|
||||
{ id: 3, name: "Finance", icon: null, sortOrder: 3 },
|
||||
{ id: 4, name: "Product", icon: null, sortOrder: 4 },
|
||||
{ id: 5, name: "Media", icon: null, sortOrder: 5 },
|
||||
];
|
||||
|
||||
export const FIXTURE_DISCOVER: DiscoverData = {
|
||||
featuredExperts: FIXTURE_FEATURED,
|
||||
trendingMinnects: FIXTURE_TRENDING,
|
||||
categories: FIXTURE_CATEGORIES,
|
||||
};
|
||||
|
||||
/** Find experts results: every featured expert plus a few more, filterable by name. */
|
||||
export const FIXTURE_SEARCH_RESULTS: ExpertResult[] = [
|
||||
...FIXTURE_FEATURED,
|
||||
expert("e6", "Daniel Moreau", "Sales operations", 4.5, 21),
|
||||
expert("e7", "Aiko Tanaka", "Brand strategy", 4.8, 77),
|
||||
expert("e8", "Rafael Duarte", "Engineering management", 4.4, 18),
|
||||
].map((e) => ({
|
||||
id: e.id,
|
||||
slug: e.slug,
|
||||
displayName: e.displayName,
|
||||
byline: e.byline,
|
||||
pictureUrl: e.pictureUrl,
|
||||
rating: e.rating,
|
||||
categories: [],
|
||||
}));
|
||||
Reference in New Issue
Block a user