From 2cb3ef9694106f113941f8079d8c26fa534576aa Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 14 Oct 2026 12:08:15 -0400 Subject: [PATCH] fix(website): label fallback club routes and chat as examples (W-routes, W-chat) When a club has no curated routes or a chat channel has no messages, the data layer returned fixture content with no indication it was sample data, so riders could mistake MotorSignal's "Coastal Marshes" route or canned chat messages for their club's real data. Flag fixture-sourced content and label it in the UI: - Add isExample to ClubRoute and ChatMessage; set it on every fixture fallback path (empty Firestore collection/channel and the dev demo preview). - Founder Routes: amber "Examples" count, an explanatory banner, and a per-card "Example" badge when the routes shown are samples. - Founder Chat: an "Example thread" pill (instead of "Live Connected") and a banner when the only messages are examples, so the header never claims a live conversation that does not exist. Tests: example-labelled routes render the banner/badge/count; example-labelled chat renders the banner and "Example thread" pill and does not claim "Live Connected". Existing route/chat tests (real data, no isExample) remain green. Co-Authored-By: Claude Opus 4.8 --- .../founder-portal/founder-chat.tsx | 28 ++++++++++--- .../founder-portal/founder-routes.tsx | 35 +++++++++++++--- website/lib/club-community.ts | 16 ++++--- website/tests/founder-routes-chat.test.tsx | 42 +++++++++++++++++++ 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/website/components/founder-portal/founder-chat.tsx b/website/components/founder-portal/founder-chat.tsx index 1e54c20..1707e08 100644 --- a/website/components/founder-portal/founder-chat.tsx +++ b/website/components/founder-portal/founder-chat.tsx @@ -60,6 +60,7 @@ export function FounderChat({ context }: FounderChatProps) { const activeChannel = CHANNELS.find((c) => c.id === activeChannelId) || CHANNELS[0]; const isRestrictedChannel = activeChannel.requiresOfficer && !isOfficer; + const hasExampleMessages = messages.length > 0 && messages.every((m) => m.isExample); useEffect(() => { setError(null); @@ -67,7 +68,7 @@ export function FounderChat({ context }: FounderChatProps) { if (isDemoFixture) { const channelMsgs = FIXTURE_CHAT_MESSAGES[activeChannelId] || []; - setMessages(channelMsgs.map((m) => ({ ...m, clubId: club.id, channelId: activeChannelId }))); + setMessages(channelMsgs.map((m) => ({ ...m, clubId: club.id, channelId: activeChannelId, isExample: true }))); } else { unsubscribe = subscribeClubChat( club.id, @@ -143,10 +144,17 @@ export function FounderChat({ context }: FounderChatProps) {

Squad Live Chat

- - - Live Connected - + {hasExampleMessages ? ( + + visibility + Example thread + + ) : ( + + + Live Connected + + )}

Real-time squad channels for ride organization, road condition warnings, and administrative officer discussions. @@ -179,6 +187,16 @@ export function FounderChat({ context }: FounderChatProps) { + {hasExampleMessages && ( +

+ info + + These are example messages — this + channel has no posts yet. Send a message to start the real conversation. + +
+ )} + {error && (
diff --git a/website/components/founder-portal/founder-routes.tsx b/website/components/founder-portal/founder-routes.tsx index 9c5c2b5..d4c0a13 100644 --- a/website/components/founder-portal/founder-routes.tsx +++ b/website/components/founder-portal/founder-routes.tsx @@ -178,6 +178,8 @@ export function FounderRoutes({ context }: FounderRoutesProps) { const canCurate = permissions.canPlanRides || permissions.canManageSettings; + const hasExampleRoutes = routes.length > 0 && routes.every((r) => r.isExample); + return (
{/* Top Header Card */} @@ -187,8 +189,14 @@ export function FounderRoutes({ context }: FounderRoutesProps) {

Curated Club Routes

- - {routes.length} Available + + {hasExampleRoutes ? `${routes.length} Examples` : `${routes.length} Available`}

@@ -207,6 +215,16 @@ export function FounderRoutes({ context }: FounderRoutesProps) { )}

+ {hasExampleRoutes && ( +
+ info + + These are example routes — your club + hasn’t curated any yet. Curate a route to replace them with your own. + +
+ )} + {error && (
@@ -314,9 +332,16 @@ export function FounderRoutes({ context }: FounderRoutesProps) {
{/* Card Header & Badge */}
-

- {route.title} -

+
+

+ {route.title} +

+ {route.isExample && ( + + Example + + )} +
{ if (options?.allowDemoPreview) { - return FIXTURE_CLUB_ROUTES.map((r) => ({ ...r, clubId })); + return FIXTURE_CLUB_ROUTES.map((r) => ({ ...r, clubId, isExample: true })); } callerUid(); const routesRef = collection(firestore, 'clubs', clubId, 'routes'); const q = query(routesRef, orderBy('createdAt', 'desc'), limit(50)); const snap = await getDocs(q); if (snap.empty) { - // Return sample starter routes if no custom routes have been uploaded - return FIXTURE_CLUB_ROUTES.map((r) => ({ ...r, clubId })); + // No custom routes uploaded yet: surface sample starter routes, explicitly flagged as + // examples so the UI can label them and they are never mistaken for real club routes. + return FIXTURE_CLUB_ROUTES.map((r) => ({ ...r, clubId, isExample: true })); } return snap.docs.map((d) => { const data = d.data(); @@ -358,6 +361,8 @@ export interface ChatMessage { authorRole: ClubRole; content: string; createdAt: string; + /** True when this is MotorSignal sample/example content, not a real posted message. */ + isExample?: boolean; } export const FIXTURE_CHAT_MESSAGES: Record = { @@ -441,9 +446,10 @@ export function subscribeClubChat( q, (snap) => { if (snap.empty) { - // Fallback to fixture messages if channel has no messages yet + // No messages in this channel yet: surface sample messages, explicitly flagged as + // examples so the UI can label them and they are never mistaken for real posts. const initial = FIXTURE_CHAT_MESSAGES[channelId] || []; - onMessages(initial.map((m) => ({ ...m, clubId, channelId }))); + onMessages(initial.map((m) => ({ ...m, clubId, channelId, isExample: true }))); return; } const msgs: ChatMessage[] = snap.docs.map((d) => { diff --git a/website/tests/founder-routes-chat.test.tsx b/website/tests/founder-routes-chat.test.tsx index 625c276..fbb481b 100644 --- a/website/tests/founder-routes-chat.test.tsx +++ b/website/tests/founder-routes-chat.test.tsx @@ -250,6 +250,18 @@ describe('Founder Routes Management', () => { expect(mocks.deleteRoute).toHaveBeenCalledWith('ClubID', 'r1'); }); }); + + // WEB-W-routes regression: fixture fallback routes must be labelled as examples so a + // rider can never mistake sample content for the club's real curated routes. + it('labels example routes when the club has none of its own', async () => { + mocks.fetchRoutes.mockResolvedValue(sampleRoutes.map((r) => ({ ...r, isExample: true }))); + render(); + await screen.findByText('Blood Mountain Gap Loop'); + + expect(screen.getByText(/example routes/i)).toBeDefined(); + expect(screen.getByText(/Examples$/)).toBeDefined(); + expect(screen.getAllByText('Example').length).toBeGreaterThanOrEqual(1); + }); }); describe('Founder Chat Management', () => { @@ -296,6 +308,36 @@ describe('Founder Chat Management', () => { }); }); + // WEB-W-chat regression: fixture fallback messages must be labelled as examples and the + // header must not claim a live connection when only sample content is shown. + it('labels example messages when the channel has no real posts', async () => { + mocks.subscribeChat.mockImplementation((_clubId, _channelId, onMessages) => { + onMessages([ + { + id: 'example-msg-1', + clubId: 'ClubID', + channelId: 'general', + authorUid: 'owner', + authorDisplayName: 'Morgan Vance', + authorHandle: '@morgan_v', + authorDisplayRank: 'President', + authorRole: 'owner', + content: 'Welcome to squad chat!', + createdAt: '2026-06-01T12:00:00Z', + isExample: true, + }, + ]); + return vi.fn(); + }); + + render(); + await screen.findByText('Welcome to squad chat!'); + + expect(screen.getByText('Example thread')).toBeDefined(); + expect(screen.getByText(/example messages/i)).toBeDefined(); + expect(screen.queryByText('Live Connected')).toBeNull(); + }); + it('restricts Officers Lounge channel for non-officers', async () => { const memberContext: FounderClubContext = { ...mockContext,