330 lines
17 KiB
TypeScript
330 lines
17 KiB
TypeScript
// 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 { toCardPosts, type CardPost } from "@/lib/feed-model";
|
|
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 gradient placeholder image (16:9 unless a size is given), drawn with hsl() so no fetch happens. */
|
|
export function placeholderImage(hue: number, label = "", width = 1280, height = 720): string {
|
|
const svg =
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
|
|
`<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="${width}" height="${height}" fill="url(#g)"/>` +
|
|
(label
|
|
? `<text x="64" y="${height - 60}" 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)}`;
|
|
}
|
|
|
|
/** Not a real file: preload is off, so the preview video shows only its poster, play button and length. */
|
|
const FIXTURE_VIDEO_URL = "https://cdn.feed.sharedservices.vt.com/dev-fixture.mp4";
|
|
|
|
/** Fixture videos with real (tiny) files in public/dev: the feed only accepts CDN-host mp4 URLs, so these are swapped to local paths after parsing. */
|
|
const FIXTURE_LANDSCAPE_URL = "https://cdn.feed.sharedservices.vt.com/dev-landscape.mp4";
|
|
const FIXTURE_PORTRAIT_URL = "https://cdn.feed.sharedservices.vt.com/dev-portrait.mp4";
|
|
const LOCAL_FIXTURE_VIDEOS: Record<string, string> = {
|
|
[FIXTURE_LANDSCAPE_URL]: "/dev/landscape.mp4",
|
|
[FIXTURE_PORTRAIT_URL]: "/dev/portrait.mp4",
|
|
};
|
|
|
|
/** toCardPosts for the preview, with the fixture video URLs pointed at the local files. */
|
|
export function fixtureCards(items: unknown[]): CardPost[] {
|
|
return toCardPosts(items).map((p) => ({
|
|
...p,
|
|
media: p.media.map((m) => (m.mp4Url && LOCAL_FIXTURE_VIDEOS[m.mp4Url] ? { ...m, mp4Url: LOCAL_FIXTURE_VIDEOS[m.mp4Url] } : m)),
|
|
}));
|
|
}
|
|
|
|
/** Comments for the preview, in the shape /api/posts/[id]/comments returns. p2 holds a removed one; p4 has none. */
|
|
export const FIXTURE_COMMENTS: Record<string, unknown[]> = {
|
|
p1: [{ id: "c1", content: "Congratulations, the flow reads well.", createdAt: "2026-10-19T11:50:00Z", author: { displayName: "Priya Natarajan" } }],
|
|
p2: [
|
|
{ id: "c2", content: "Number two changed how I price renewals.", createdAt: "2026-10-19T10:40:00Z", author: { displayName: "Marcus Bell" } },
|
|
{ id: "c3", content: "removed text", deletedAt: "2026-10-19T11:00:00Z", createdAt: "2026-10-19T10:55:00Z", author: { displayName: "Elena Kovac" } },
|
|
{ id: "c4", content: "Saving this for the next pricing review.", createdAt: "2026-10-19T11:30:00Z", author: { displayName: "Samuel Okafor" } },
|
|
],
|
|
};
|
|
|
|
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: [],
|
|
},
|
|
},
|
|
{
|
|
// Medium text-only post: too long for 31px, fits at 26px or 22px with no "Read more".
|
|
type: "post",
|
|
publishedAt: "2026-10-19T11:30:00Z",
|
|
likesCount: 12,
|
|
commentsCount: 0,
|
|
post: {
|
|
id: "p10",
|
|
content: "The best advice I got as a new manager: your job is not to have the answers. It is to make sure the right question gets asked in the right room, by the person closest to the customer, early enough that the answer still changes what we build. Everything else, the rituals and the dashboards and the planning decks, exists to protect that one habit. When a meeting ends without a better question than it started with, it was a status update, and those can be an email.",
|
|
author: author("a10", "Grace Whitfield"),
|
|
mediaItems: [],
|
|
},
|
|
},
|
|
{
|
|
// Very long text-only post: does not fit at 19px, so it clamps and ends with "Read more".
|
|
type: "post",
|
|
publishedAt: "2026-10-19T11:10:00Z",
|
|
likesCount: 87,
|
|
commentsCount: 0,
|
|
post: {
|
|
id: "p11",
|
|
content: "I spent ten years building products before I learned the most useful habit of all: write down what you expect to happen before you ship, then check it a month later.\n\nMost teams skip the first half. They launch, watch a dashboard for a week and call whatever they see a win. Without the written guess there is nothing to be wrong about, so nobody learns.\n\nHere is the routine we use now. Every change gets one sentence: who it is for, what they will do differently, and the number that will move if we are right. The sentence goes in the ticket, not in a slide.\n\nThirty days later the person who wrote it reads it out loud in the weekly review and says what actually happened. No blame, no speeches. Just the guess, the result and one line on why they differ.\n\nAfter a quarter you get something no analytics tool gives you: a record of how good your team is at predicting its own customers. Ours was humbling. We were right about a third of the time.\n\nThat number changed how we plan. We now run smaller bets, we talk to five customers before any big build, and we kill features faster when the guess was wrong.\n\nIf you try one thing this month, try the sentence. It takes two minutes and it will teach you more than the next roadmap offsite.",
|
|
author: author("a11", "Daniel Moreau"),
|
|
mediaItems: [],
|
|
},
|
|
},
|
|
{
|
|
// Own, very long text-only post: "Delete error" in the preview shows the failed-delete line on it.
|
|
type: "post",
|
|
publishedAt: "2026-10-19T10:50:00Z",
|
|
likesCount: 3,
|
|
commentsCount: 0,
|
|
post: {
|
|
id: "p-own-long",
|
|
content: "I spent ten years building products before I learned the most useful habit of all: write down what you expect to happen before you ship, then check it a month later.\n\nMost teams skip the first half. They launch, watch a dashboard for a week and call whatever they see a win. Without the written guess there is nothing to be wrong about, so nobody learns.\n\nHere is the routine we use now. Every change gets one sentence: who it is for, what they will do differently, and the number that will move if we are right. The sentence goes in the ticket, not in a slide.\n\nThirty days later the person who wrote it reads it out loud in the weekly review and says what actually happened. No blame, no speeches. Just the guess, the result and one line on why they differ.\n\nAfter a quarter you get something no analytics tool gives you: a record of how good your team is at predicting its own customers. Ours was humbling. We were right about a third of the time.\n\nThat number changed how we plan. We now run smaller bets, we talk to five customers before any big build, and we kill features faster when the guess was wrong.\n\nIf you try one thing this month, try the sentence. It takes two minutes and it will teach you more than the next roadmap offsite.",
|
|
author: author(FIXTURE_FEED_USER_ID, "Jordan Rivera"),
|
|
mediaItems: [],
|
|
},
|
|
},
|
|
{
|
|
// Photo post with a caption longer than three lines: "more" / "less".
|
|
type: "post",
|
|
publishedAt: "2026-10-19T10:50:00Z",
|
|
likesCount: 64,
|
|
commentsCount: 0,
|
|
post: {
|
|
id: "p12",
|
|
content: "Notes from the panel on hiring a first sales lead, since a few people asked. Hire for the next eighteen months, not the next five years: the person who closes your first fifty customers is rarely the one who builds a team of twenty. Ask every candidate to walk you through a deal they lost and what they changed afterwards. Give them real numbers in the interview, including the bad ones, and watch how they react. Pay them on outcomes you can measure from day one, and write down what success looks like at thirty, sixty and ninety days. Last point: the founder keeps selling until the new lead has closed at least three deals alone. Handing over too early was the most common regret in the room.",
|
|
author: author("a12", "Aiko Tanaka"),
|
|
mediaItems: [{ mediaType: "image", imageUrl: placeholderImage(48, "Sales panel", 1080, 1350), order: 0 }],
|
|
},
|
|
},
|
|
{
|
|
// Video post with a long caption.
|
|
type: "post",
|
|
publishedAt: "2026-10-19T10:30:00Z",
|
|
likesCount: 39,
|
|
commentsCount: 0,
|
|
post: {
|
|
id: "p13",
|
|
content: "Full recording of the Thursday session on pricing experiments. Chapters: why we test price at all, how to pick the first segment, the three-week test plan, what to tell existing customers, reading the results without fooling yourself, and the questions from the audience at the end. Slides and the spreadsheet template are linked in the comments. If you only have five minutes, skip to the part on reading results: that is where most teams get it wrong, and where we lost two months last year before we noticed.",
|
|
author: author("a13", "Rafael Duarte"),
|
|
mediaItems: [{ mediaType: "video", thumbnailUrl: placeholderImage(230, "Pricing session"), mp4Url: FIXTURE_LANDSCAPE_URL, videoDuration: 2, order: 0 }],
|
|
},
|
|
},
|
|
{
|
|
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"), mp4Url: FIXTURE_VIDEO_URL, videoDuration: 154, order: 0 }],
|
|
},
|
|
},
|
|
{
|
|
type: "post",
|
|
publishedAt: "2026-10-19T07:50:00Z",
|
|
likesCount: 31,
|
|
commentsCount: 4,
|
|
post: {
|
|
id: "p8",
|
|
content: "Landscape recording of the quarterly walkthrough. It should show whole, centered, over a blurred copy of its own thumbnail.",
|
|
author: author("a8", "Tomas Reyes"),
|
|
mediaItems: [{ mediaType: "video", thumbnailUrl: placeholderImage(180, "Landscape 16:9"), mp4Url: FIXTURE_LANDSCAPE_URL, videoDuration: 2, order: 0 }],
|
|
},
|
|
},
|
|
{
|
|
type: "post",
|
|
publishedAt: "2026-10-19T07:20:00Z",
|
|
likesCount: 44,
|
|
commentsCount: 5,
|
|
post: {
|
|
id: "p9",
|
|
content: "Portrait clip shot on a phone. It should fill the whole slide.",
|
|
author: author("a9", "Ines Moreau"),
|
|
mediaItems: [{ mediaType: "video", thumbnailUrl: placeholderImage(300, "Portrait 9:16", 720, 1280), mp4Url: FIXTURE_PORTRAIT_URL, videoDuration: 2, order: 0 }],
|
|
},
|
|
},
|
|
{
|
|
type: "post",
|
|
publishedAt: "2026-10-18T21:20:00Z",
|
|
likesCount: 22,
|
|
commentsCount: 2,
|
|
post: {
|
|
id: "p7",
|
|
content: "Workshop recap: two slides and the demo recording. Swipe sideways for all three.",
|
|
author: author("a7", "Leila Haddad"),
|
|
mediaItems: [
|
|
{ mediaType: "image", imageUrl: placeholderImage(140, "Slide one"), order: 0 },
|
|
{ mediaType: "image", imageUrl: placeholderImage(260, "Slide two"), order: 1 },
|
|
{ mediaType: "video", thumbnailUrl: placeholderImage(20, "Demo recording"), mp4Url: FIXTURE_VIDEO_URL, videoDuration: 73, order: 2 },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
pictureUrl: string | null = null,
|
|
): DiscoverExpert => ({
|
|
id,
|
|
slug: displayName.replace(/\s+/g, ""),
|
|
displayName,
|
|
byline,
|
|
pictureUrl,
|
|
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),
|
|
// Photo placeholders exercise the image path of the featured carousel; the
|
|
// rest show initials.
|
|
expert("e9", "Leila Haddad", "Marketplace operations", 4.9, 181, placeholderImage(10)),
|
|
expert("e10", "Tomas Ferreira", "Hiring and team design", 4.7, 64, placeholderImage(190)),
|
|
expert("e11", "Grace Whitfield", "Brand and storytelling", 4.8, 102, placeholderImage(300)),
|
|
];
|
|
|
|
const minnect = (id: string, who: DiscoverExpert, question: string, views: number, hue: number): DiscoverMinnect => ({
|
|
id,
|
|
question,
|
|
views,
|
|
thumbnailUrl: placeholderImage(hue),
|
|
media: id === "m2" ? { kind: "video", url: "/fixtures/answer.mp4", duration: 42, waveform: null } : id === "m1" || id === "m4" ? { kind: "audio", url: "/fixtures/answer.mp3", duration: 31, waveform: null } : 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: [],
|
|
}));
|