Skip to main content

The Visita Development Philosophy

The mental model for building scalable Next.js features in a civic intelligence platform.
Core Principle: We build critical infrastructure, not social networks. Every feature must serve awareness and civic participation, not engagement for its own sake.

1️⃣ Start with the Role of the Page (WHY does it exist?)

Before writing code, ask:

Core Questions

  • What job does this page do?
  • Who is this page for? (guest, citizen, business, analyst, admin)
  • What problem does the page solve?
  • Is this a destination page or a support page?

Visita Examples

  • Ward Dashboard → Awareness & Action
  • Directory Page → Discovery & Connection
  • Profile Page → Identity & Reputation
  • Settings Page → Control & Safety
  • Analyst Dashboard → Intelligence & Oversight
👉 If you can’t describe the page in one sentence, the page is doing too much. Example: The Ward Dashboard exists to “provide citizens with real-time intelligence about their immediate geographic area and enable civic participation.”

2️⃣ Page vs Layout vs Component (CRITICAL separation)

In Next.js, you should think in three layers:
  1. Layout (persistent structure)
  2. Page (orchestration)
  3. Components (capabilities)

Ask:

  • What persists across pages? → layout
  • What is unique to this route? → page
  • What is reusable or self-contained? → component

Visita Example: Ward Structure

// Layout: Persistent ward navigation
/app/ward/[code]/layout.tsx
  └── WardSidebar (persists across all ward pages)
  └── WardHeader (breadcrumbs, ward switcher)

// Page: Ward intelligence dashboard
/app/ward/[code]/page.tsx
  └── Fetches ward data
  └── Orchestrates widgets

// Components: Individual capabilities
/components/ward/WeatherWidget.tsx
/components/ward/SignalFeed.tsx
/components/ward/CouncillorProfile.tsx

3️⃣ Define the Page’s Component Inventory

Before coding, list the components this page needs.

Example: Ward Dashboard Page

Component Inventory:
  • WardHeader (title, ward code, last updated)
  • WeatherWidget (current conditions, rain alerts)
  • SignalFeed (crime, alerts, community signals)
  • SignalCard (individual signal display)
  • DirectoryMap (local businesses, services)
  • CouncillorProfile (ward representative info)
  • CommunityActions (active petitions, cleanups)

🔍 Component Questions

For each component, ask:
  • Does this component appear on other pages?
    • SignalCard appears in Ward Feed AND Profile History ✅
  • Could it appear in the future?
    • DirectoryMap could be used in search results ✅
  • Is it domain-specific or generic?
    • WeatherWidget is domain-specific (ward intelligence)
    • Button is generic (UI component)
  • Does it need props?
    • SignalCard needs signal object ✅
  • Does it manage state?
    • SignalFeed manages pagination state ✅
This immediately tells you:
  • Where the component should live (/components/ward/ vs /components/ui/)
  • How reusable it should be (generic API vs specific)
  • How flexible its API must be (props design)

4️⃣ Reusability Test (The “Three Contexts” Rule)

Ask: Can this component logically exist in three different contexts?

✅ Reusable Component: SignalCard

  1. Ward Feed (/ward/[code])
  2. Profile History (/profile/signals)
  3. Search Results (/search?q=crime)
Build generic with flexible props
interface SignalCardProps {
  signal: Signal;
  variant?: 'compact' | 'detailed';
  showLocation?: boolean;
  onAction?: (action: SignalAction) => void;
}

❌ Page-Locked Component: WardHeader

Only appears on ward pages → Keep local to page
// Lives in /app/ward/[code]/components/WardHeader.tsx
Don’t force reusability. Some components should be page-specific. If a component only makes sense in one context, keep it local.

5️⃣ Data Responsibility (Who owns the data?)

One of the most common mistakes in component architecture.

Ask:

  • Does the page fetch data?
  • Or does the component fetch data?
  • Is the data global, shared, or local?

🎯 Rule: Pages Fetch, Components Render

// ✅ GOOD: Page fetches, components receive

// Page: /app/ward/[code]/page.tsx
export default async function WardPage({ params }: { params: { code: string } }) {
  const wardData = await fetchWardData(params.code); // Page fetches
  const signals = await fetchWardSignals(params.code);

  return (
    <div>
      <WeatherWidget data={wardData.weather} />  // Component renders
      <SignalFeed signals={signals} />           // Component renders
    </div>
  );
}

// ❌ BAD: Component fetches its own data
function WeatherWidget({ wardCode }: { wardCode: string }) {
  const [weather, setWeather] = useState(null);

  useEffect(() => {
    fetchWeather(wardCode).then(setWeather);  // Component fetching
  }, [wardCode]);

  return <div>{weather.temperature}</div>;
}

Exceptions (When Components CAN Fetch):

  1. Infinite Scroll (loads more on interaction)
  2. Highly Interactive Widgets (real-time updates)
  3. Lazy-Loaded Sections (below-the-fold content)
// ✅ Acceptable: Lazy-loaded directory map
'use client';
function DirectoryMapWrapper({ wardCode }: { wardCode: string }) {
  const { data: businesses, isLoading } = useBusinessesByWard(wardCode);

  return (
    <Map>
      {businesses?.map(business => (
        <MapMarker key={business.id} business={business} />
      ))}
    </Map>
  );
}

6️⃣ State Ownership (Where does logic live?)

Ask:
  • What state exists on this page?
  • Is the state local or shared?
  • What triggers re-renders?

Guiding Principle

State lives as high as necessary, as low as possible

Visita Examples

// 1. Local State (component-level)
function SignalFilter() {
  const [selectedCategory, setSelectedCategory] = useState('all');  // Local
  // Only this component cares about the filter
}

// 2. Page State (page-level)
'use client';
export default function WardPage({ params }: { params: { code: string } }) {
  const [dateRange, setDateRange] = useState({ start: '7d', end: 'now' });  // Page-level
  // Multiple components use this date range
  const filteredSignals = useSignals(params.code, dateRange);

  return (
    <>
      <DateRangePicker value={dateRange} onChange={setDateRange} />
      <SignalFeed signals={filteredSignals} />
      <SignalChart signals={filteredSignals} />
    </>
  );
}

// 3. Global State (app-level)
function UserProfile() {
  const { user, profile } = useAuth();  // Global auth state
  // Many components across the app need this
}

URL State (Ideal for Filters)

// ✅ BEST: Filters in URL (shareable, bookmarkable)
'use client';
export default function WardPage({ params, searchParams }: {
  params: { code: string };
  searchParams: { category?: string; dateRange?: string };
}) {
  // State is in URL: /ward/12345?category=crime&dateRange=7d
  const signals = useSignals(params.code, searchParams);

  return <SignalFeed signals={signals} />;
}

7️⃣ Server vs Client (Next.js-specific)

Every page should answer:
  • Can this be a Server Component?
  • What must be client-side?
  • Do I need interactivity here?

Decision Tree

Ask: Is there useState, useEffect, click handlers, or browser APIs?
├─ YES → Client Component ('use client')
└─ NO → Server Component (default)

🎯 Default to Server

// ✅ GOOD: Server component by default
export default async function WardPage({ params }: { params: { code: string } }) {
  const ward = await fetchWard(params.code);  // Server-side data fetch
  const signals = await fetchSignals(params.code);

  return (
    <div>
      <WardHeader ward={ward} />  {/* Static, no interactivity */}
      <SignalFeed signals={signals} />  {/* Static list */}
    </div>
  );
}

Opt-in to Client When Needed

// ✅ GOOD: Client component for interactivity
'use client';
export default function SignalFeed({ signals }: { signals: Signal[] }) {
  const [selectedSignal, setSelectedSignal] = useState<Signal | null>(null);  // Interaction

  return (
    <div>
      {signals.map(signal => (
        <button key={signal.id} onClick={() => setSelectedSignal(signal)}>  {/* Click handler */}
          <SignalCard signal={signal} />
        </button>
      ))}
      {selectedSignal && <SignalModal signal={selectedSignal} />}
    </div>
  );
}

Hybrid Approach (Islands of Interactivity)

// ✅ BEST: Server component with client islands
export default async function WardPage({ params }: { params: { code: string } }) {
  const ward = await fetchWard(params.code);

  return (
    <div>
      {/* Server-rendered static content */}
      <WardHeader ward={ward} />

      {/* Client-side interactive widget */}
      <SignalFilter />

      {/* Server-rendered list, client-side pagination */}
      <SignalFeedContainer wardCode={params.code} />
    </div>
  );
}

8️⃣ Performance Questions (Before it’s a problem)

Ask early:
  • Is this page heavy?
  • What can be lazy-loaded?
  • What is above-the-fold?

Checklist

1
Step 1: Identify Heavy Components
2
// Heavy components to lazy-load:
- DirectoryMap (Leaflet map + many markers)
- SignalChart (Chart.js or D3 visualization)
- BusinessDirectory (large list with images)
3
Step 2: Use Dynamic Imports
4
// ✅ GOOD: Lazy load heavy components
import dynamic from 'next/dynamic';

const DirectoryMap = dynamic(
  () => import('@/components/ward/DirectoryMap'),
  {
    loading: () => <MapSkeleton />,  // Show skeleton while loading
    ssr: false  // Don't SSR map (browser-only)
  }
);

export default function WardPage() {
  return (
    <div>
      <WeatherWidget />  {/* Above-the-fold, load immediately */}
      <DirectoryMap />   {/* Below-the-fold, lazy load */}
    </div>
  );
}
5
Step 3: Optimize Images
6
import Image from 'next/image';

<Image
  src={business.logo}
  alt={business.name}
  width={80}
  height={80}
  priority={false}  // Don't prioritize below-the-fold images
  placeholder="blur"  // Blur placeholder
  blurDataURL="data:image/jpeg;base64,..."
/>
7
Step 4: Virtualize Long Lists
8
// For very long signal feeds (1000+ items)
import { FixedSizeList as List } from 'react-window';

function VirtualizedSignalFeed({ signals }: { signals: Signal[] }) {
  return (
    <List
      height={600}
      itemCount={signals.length}
      itemSize={120}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <SignalCard signal={signals[index]} />
        </div>
      )}
    </List>
  );
}

9️⃣ Access & Security

Every page should answer:
  • Who can access this page?
  • What happens if user is not allowed?
  • What data must be protected server-side?

Visita Examples

// 1. Public Page (no auth required)
export default function WardPage({ params }: { params: { code: string } }) {
  // Anyone can view ward intelligence
  const ward = await fetchWard(params.code);
  return <WardDashboard ward={ward} />;
}

// 2. Protected Page (auth required)
export default function AccountPage() {
  // Must be authenticated
  return (
    <RequireAuth>
      <AccountDashboard />
    </RequireAuth>
  );
}

// 3. Role-Based Page (specific permissions)
export default function AnalystDashboard() {
  // Must be analyst or admin
  return (
    <RequireContext roles={['analyst', 'admin']}>
      <AnalystTools />
    </RequireContext>
  );
}

Server-Side Protection (Critical!)

Never rely on client-side checks alone. Always verify permissions server-side.
// ✅ GOOD: Server-side auth check
export async function joinWardAction(wardCode: string) {
  'use server';

  const supabase = await createClient();
  const { data: { user }, error: authError } = await supabase.auth.getUser();

  if (authError || !user) {
    return { success: false, error: 'Authentication required' };
  }

  // Check if user is already a member
  const { data: membership } = await supabase
    .from('ward_memberships')
    .select('*')
    .eq('user_id', user.id)
    .eq('ward_code', wardCode)
    .single();

  if (membership) {
    return { success: false, error: 'Already a member' };
  }

  // Add membership
  const { error } = await supabase
    .from('ward_memberships')
    .insert({ user_id: user.id, ward_code: wardCode });

  return { success: !error, error: error?.message };
}

RLS (Row Level Security)

Enable RLS policies in Supabase for database-level security:
-- Enable RLS on ward_memberships
ALTER TABLE ward_memberships ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only see their own memberships
CREATE POLICY "Users can view own memberships" ON ward_memberships
  FOR SELECT USING (auth.uid() = user_id);

-- Policy: Users can only join wards (not delete others')
CREATE POLICY "Users can insert own memberships" ON ward_memberships
  FOR INSERT WITH CHECK (auth.uid() = user_id);

🔟 UX & Failure States (Often forgotten)

Ask: What does this page look like when:
  • Loading?
  • Empty?
  • Error?
  • Partial data?

Define States for Each Page

// Ward Dashboard States
export default function WardPage({ params }: { params: { code: string } }) {
  return (
    <div>
      {/* Loading State */}
      <Suspense fallback={<WardSkeleton />}>
        <WeatherWidget wardCode={params.code} />
      </Suspense>

      {/* Error State */}
      <ErrorBoundary fallback={<SignalFeedError />}>
        <SignalFeed wardCode={params.code} />
      </ErrorBoundary>

      {/* Empty State */}
      <CommunityActions wardCode={params.code} />
      {/*
        If no actions, component shows:
        <EmptyState
          title="No active community actions"
          description="Be the first to start one!"
          action={<Button>Start Action</Button>}
        />
      */}
    </div>
  );
}

Skeleton Components

// ✅ GOOD: Skeleton for loading states
function WardSkeleton() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-1/3"></div>
      <div className="h-32 bg-gray-200 rounded"></div>
      <div className="space-y-2">
        <div className="h-4 bg-gray-200 rounded"></div>
        <div className="h-4 bg-gray-200 rounded w-5/6"></div>
      </div>
    </div>
  );
}

Empty States

// ✅ GOOD: Helpful empty states
function EmptySignalFeed({ wardCode }: { wardCode: string }) {
  return (
    <div className="text-center py-12">
      <AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
      <h3 className="text-lg font-medium text-gray-900 mb-2">
        No signals yet
      </h3>
      <p className="text-gray-500 mb-4">
        Be the first to report an issue or event in your ward.
      </p>
      <Button onClick={() => openSignalForm()}>
        Report a Signal
      </Button>
    </div>
  );
}

Error Boundaries

// ✅ GOOD: Graceful error handling
'use client';
function SignalFeed({ wardCode }: { wardCode: string }) {
  const { data: signals, error, isLoading } = useSignals(wardCode);

  if (error) {
    throw new Error('Failed to load signals');  // Caught by ErrorBoundary
  }

  if (isLoading) {
    return <SignalFeedSkeleton />;
  }

  return (
    <div>
      {signals.map(signal => (
        <SignalCard key={signal.id} signal={signal} />
      ))}
    </div>
  );
}

1️⃣1️⃣ Folder Placement (Reflect the thinking)

A clean structure reflects good thinking:
// Ward Dashboard Structure
/app
 └── ward
      └── [code]
           ├── page.tsx              // Main ward dashboard
           ├── loading.tsx           // Loading state
           ├── error.tsx             // Error state
           ├── layout.tsx            // Ward layout (sidebar, header)
           └── components/           // Page-specific components
                ├── WardHeader.tsx
                ├── WardStats.tsx
                └── WardActions.tsx

// Shared ward components (used across multiple ward pages)
/components
 └── ward
      ├── WeatherWidget.tsx          // Reusable across pages
      ├── SignalCard.tsx             // Used in feed, profile, search
      ├── SignalFeed.tsx
      ├── DirectoryMap.tsx
      └── CouncillorProfile.tsx

// Generic UI components
/components
 └── ui
      ├── Button.tsx
      ├── Card.tsx
      ├── Modal.tsx
      └── Skeleton.tsx

UI Architecture Hierarchy

The three-layer component architecture ensures clear separation of concerns:
  1. UI Primitives (/components/ui/) — Generic, accessible building blocks
  2. Layout Components (/components/layout/) — Structural, persistent UI
  3. Domain Components (/components/ward/, /components/business/) — Visita-specific logic
Golden Rule: UI components must not know business logic. Domain components may use UI components. Never the reverse. For detailed UI architecture guidance, see the UI Strategy & Design System guide.

Component Placement Rules

  • Page-specific: /app/ward/[code]/components/
  • Ward domain: /components/ward/
  • Generic UI: /components/ui/
  • Business domain: /components/business/
  • Auth domain: /components/auth/

1️⃣2️⃣ UI Architecture Principles

Component Hierarchy

Every component should be placed in the appropriate layer:
  • Page-specific: /app/ward/[code]/components/ — Only used by this page
  • Domain: /components/ward/ — Shared across ward pages
  • Layout: /components/layout/ — Structural, persistent UI
  • UI Primitives: /components/ui/ — Generic, reusable atoms

UI Strategy Alignment

Visita uses a Headless + Utility CSS approach:
  • Radix UI for behavior and accessibility
  • Tailwind CSS for styling control
  • shadcn/ui for component templates (we own the code)
This strategy gives us:
  • ✅ Full control over design and behavior
  • ✅ Accessible by default
  • ✅ No vendor lock-in
  • ✅ Civic-appropriate aesthetics

Design Philosophy

Visita’s UI must feel:
  • Civic — Serious, trustworthy, institutional
  • Calm — Not flashy or attention-grabbing
  • Structured — Clear hierarchy and organization
  • Neutral — Data-respecting, not emotionally manipulative
  • Accessible — Works for all citizens
What We Avoid:
  • ❌ Over-rounded “startup bubbles”
  • ❌ Flashy gradients and animations
  • ❌ Engagement-optimized patterns
  • ❌ Trendy designs that age poorly
For comprehensive UI guidance, see:

1️⃣3️⃣ Final Developer Checklist (Use this every time)

Before writing code, answer:

Page-Level

  • What is this page’s purpose? (one-sentence description)
  • Who is it for? (guest, citizen, business, analyst, admin)
  • What data does it need? (list all data dependencies)
  • Is it server or client? (default to server, opt-in to client)
  • What are the loading/empty/error states? (define all UX states)
  • Is it public or protected? (auth requirements)

Component-Level

  • Is this reusable? (three contexts test)
  • What props does it need? (design flexible API)
  • Does it own state? (local vs page vs global)
  • Can it be dumb? (prefer presentational over smart)
  • Does it fetch data? (or does page provide data)
  • What are its states? (loading, empty, error)

Architecture

  • What belongs in layout? (persistent structure)
  • What belongs in page? (orchestration, data fetching)
  • What belongs in component? (capabilities, rendering)
  • Where should it live? (folder placement)
  • How is it secured? (RLS, server checks, client guards)

Performance

  • Is anything heavy? (maps, charts, large lists)
  • What can be lazy-loaded? (below-the-fold content)
  • Are images optimized? (Next.js Image, placeholders)
  • Is this SEO-critical? (server-render important content)

Security

  • Who can access? (public, auth, role-based)
  • Server-side checks? (never trust client)
  • RLS policies? (database-level security)
  • Input validation? (sanitize all inputs)

The Meta-Principle (This is the real lesson)

Pages orchestrate. Components specialize. Layouts persist. Data flows down. State lives low. Security never sleeps.
In Visita, we build critical infrastructure for civic intelligence. Every architectural decision should reflect:
  1. Clarity over cleverness (simple, readable code)
  2. Security over speed (verify, then trust)
  3. Awareness over engagement (inform, don’t manipulate)
  4. Resilience over features (handle failure gracefully)
  5. Civic value over vanity metrics (solve real problems)

Summary

This development philosophy ensures:
  • Scalable architecture (clear separation of concerns)
  • Performance by default (server components, lazy loading)
  • Security first (RLS, server checks, input validation)
  • Great UX (loading states, empty states, error handling)
  • Maintainable code (reusable components, clear structure)
  • Civic alignment (awareness, action, infrastructure mindset)

Status: Active
Applies To: All frontend development
Reviewed: January 2026