Files
thrilltrack-explorer/migration/PHASE_12_PAGES_MIGRATION.md

18 KiB

PHASE 12: Next.js 15 App Router Pages Migration

Status: Not Started
Estimated Time: 45-55 hours
Priority: CRITICAL
Depends On: All previous phases (1-11)
Blocks: Phase 13 (Next.js Optimization)


🎯 Goal

Convert ALL React SPA pages to Next.js 15 App Router pages while replacing Supabase calls with Django services. This is a DUAL migration:

  1. React → Next.js App Router: Convert pages and routing
  2. Supabase → Django: Replace all data fetching

Critical Requirements:

  • Preserve ALL existing URLs
  • Maintain exact same UI/UX
  • Use Server Components by default
  • Mark Client Components with 'use client'
  • Sacred Pipeline remains intact

📋 Next.js App Router Structure

New Directory Structure

app/
├── layout.tsx                          # Root layout
├── page.tsx                           # Homepage
├── loading.tsx                        # Global loading
├── error.tsx                          # Global error
├── not-found.tsx                      # 404 page
│
├── parks/
│   ├── page.tsx                       # /parks (listing)
│   ├── loading.tsx                    # Loading state
│   ├── [parkSlug]/
│   │   ├── page.tsx                   # /parks/[parkSlug]
│   │   ├── loading.tsx
│   │   └── rides/
│   │       └── page.tsx               # /parks/[parkSlug]/rides
│   └── owners/
│       └── [ownerSlug]/
│           └── page.tsx               # /owners/[ownerSlug]/parks
│
├── rides/
│   ├── page.tsx                       # /rides (listing)
│   ├── [rideSlug]/
│   │   ├── page.tsx                   # /rides/[rideSlug]
│   │   └── reviews/
│   │       └── page.tsx               # /rides/[rideSlug]/reviews
│   └── models/
│       └── [modelSlug]/
│           ├── page.tsx               # /ride-models/[modelSlug]
│           └── rides/
│               └── page.tsx           # /ride-models/[modelSlug]/rides
│
├── manufacturers/
│   ├── page.tsx                       # /manufacturers (listing)
│   └── [manufacturerSlug]/
│       ├── page.tsx                   # /manufacturers/[manufacturerSlug]
│       └── rides/
│           └── page.tsx               # /manufacturers/[manufacturerSlug]/rides
│
├── owners/
│   ├── page.tsx                       # /owners (listing)
│   └── [ownerSlug]/
│       └── page.tsx                   # /owners/[ownerSlug]
│
├── designers/
│   ├── page.tsx                       # /designers (listing)
│   └── [designerSlug]/
│       └── page.tsx                   # /designers/[designerSlug]
│
├── auth/
│   ├── login/
│   │   └── page.tsx
│   ├── register/
│   │   └── page.tsx
│   └── callback/
│       └── page.tsx
│
├── profile/
│   ├── page.tsx                       # /profile
│   ├── settings/
│   │   └── page.tsx
│   └── lists/
│       └── page.tsx
│
├── admin/
│   ├── page.tsx                       # /admin
│   └── moderation/
│       └── page.tsx                   # /admin/moderation
│
├── search/
│   └── page.tsx                       # /search
│
└── contact/
    └── page.tsx                       # /contact

📋 Tasks

Task 12.1: Root Layout & Homepage (6 hours)

Create Root Layout

app/layout.tsx (Server Component):

import { Inter } from 'next/font/google';
import './globals.css';
import { AuthProvider } from '@/components/providers/AuthProvider';
import { Navigation } from '@/components/layout/Navigation';
import { Footer } from '@/components/layout/Footer';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
  title: 'ThrillWiki - Theme Park & Ride Database',
  description: 'Comprehensive database of theme parks and rides',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AuthProvider>
          <Navigation />
          <main className="min-h-screen">{children}</main>
          <Footer />
        </AuthProvider>
      </body>
    </html>
  );
}

Convert Homepage

app/page.tsx (Server Component):

import { env } from '@/lib/env';
import { FeaturedParks } from '@/components/home/FeaturedParks';
import { RecentReviews } from '@/components/home/RecentReviews';
import { Stats } from '@/components/home/Stats';

export default async function HomePage() {
  // Fetch data server-side
  const [parks, stats] = await Promise.all([
    fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?featured=true`).then(r => r.json()),
    fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/stats/`).then(r => r.json()),
  ]);

  return (
    <div>
      <Hero />
      <Stats data={stats} />
      <FeaturedParks parks={parks} />
      <RecentReviews />
    </div>
  );
}

Checklist

  • Create app/layout.tsx with root layout
  • Create app/page.tsx for homepage
  • Create app/loading.tsx for loading state
  • Create app/error.tsx for error boundary
  • Create app/not-found.tsx for 404
  • Move CSS to app/globals.css
  • Test homepage loads
  • Verify navigation works
  • Check loading states
  • Test error boundaries

Task 12.2: Park Pages (8 hours)

Park Listing Page

app/parks/page.tsx (Server Component):

import { env } from '@/lib/env';
import { ParksList } from '@/components/parks/ParksList';
import { ParkFilters } from '@/components/parks/ParkFilters';

export const metadata = {
  title: 'Theme Parks - ThrillWiki',
  description: 'Browse theme parks from around the world',
};

export default async function ParksPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | undefined };
}) {
  const params = new URLSearchParams(searchParams as any);
  const parks = await fetch(
    `${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?${params}`,
    { next: { revalidate: 300 } } // Cache for 5 minutes
  ).then(r => r.json());

  return (
    <div>
      <h1>Theme Parks</h1>
      <ParkFilters /> {/* Client Component */}
      <ParksList parks={parks} /> {/* Can be Server Component */}
    </div>
  );
}

Park Detail Page

app/parks/[parkSlug]/page.tsx (Server Component):

import { env } from '@/lib/env';
import { ParkDetail } from '@/components/parks/ParkDetail';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  // Pre-render top 100 parks
  const parks = await fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?page_size=100`)
    .then(r => r.json());
  return parks.results.map((park: any) => ({
    parkSlug: park.slug,
  }));
}

export async function generateMetadata({ params }: { params: { parkSlug: string } }) {
  const park = await fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/${params.parkSlug}/`)
    .then(r => r.json());
  
  return {
    title: `${park.name} - ThrillWiki`,
    description: park.description,
  };
}

export default async function ParkDetailPage({ 
  params 
}: { 
  params: { parkSlug: string } 
}) {
  const park = await fetch(
    `${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/${params.parkSlug}/`,
    { next: { revalidate: 3600 } } // Cache for 1 hour
  ).then(r => r.json())
    .catch(() => notFound());

  return <ParkDetail park={park} />;
}

Checklist

  • Create app/parks/page.tsx
  • Create app/parks/[parkSlug]/page.tsx
  • Create app/parks/[parkSlug]/rides/page.tsx
  • Create app/owners/[ownerSlug]/parks/page.tsx
  • Create loading.tsx for each route
  • Implement generateStaticParams for ISR
  • Implement generateMetadata for SEO
  • Test all park URLs work
  • Verify filtering works
  • Test pagination

Task 12.3: Ride Pages (8 hours)

Ride Listing Page

app/rides/page.tsx (Server Component with caching)

Ride Detail Page

app/rides/[rideSlug]/page.tsx (Server Component)

Ride Model Pages

app/ride-models/[modelSlug]/page.tsx (Server Component)

Checklist

  • Create app/rides/page.tsx
  • Create app/rides/[rideSlug]/page.tsx
  • Create app/rides/[rideSlug]/reviews/page.tsx
  • Create app/ride-models/[modelSlug]/page.tsx
  • Create app/ride-models/[modelSlug]/rides/page.tsx
  • Create loading states
  • Implement metadata
  • Test all ride URLs
  • Verify reviews work

Task 12.4: Company Pages (8 hours)

Convert manufacturer, owner, and designer pages to App Router.

Checklist

  • Create app/manufacturers/page.tsx
  • Create app/manufacturers/[manufacturerSlug]/page.tsx
  • Create app/manufacturers/[manufacturerSlug]/rides/page.tsx
  • Create app/owners/page.tsx
  • Create app/owners/[ownerSlug]/page.tsx
  • Create app/designers/page.tsx
  • Create app/designers/[designerSlug]/page.tsx
  • Test all company URLs
  • Verify filtering works

Task 12.5: User Pages (6 hours)

These pages need authentication and user-specific data.

Profile Page

app/profile/page.tsx (Server Component with auth check):

import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { ProfileView } from '@/components/profile/ProfileView';

export default async function ProfilePage() {
  const session = await getServerSession();
  
  if (!session) {
    redirect('/auth/login');
  }

  const user = await fetch(
    `${env.NEXT_PUBLIC_DJANGO_API_URL}/users/me/`,
    {
      headers: { Authorization: `Bearer ${session.accessToken}` },
      cache: 'no-store' // Don't cache user-specific data
    }
  ).then(r => r.json());

  return <ProfileView user={user} />;
}

Checklist

  • Create app/profile/page.tsx
  • Create app/profile/settings/page.tsx
  • Create app/profile/lists/page.tsx
  • Implement authentication checks
  • Disable caching for user data
  • Test profile loads
  • Test settings work
  • Test list management

Task 12.6: Admin & Moderation Pages (6 hours)

Admin pages require role checks and real-time updates.

Moderation Queue

app/admin/moderation/page.tsx (Server Component):

import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { ModerationQueue } from '@/components/moderation/ModerationQueue';

export default async function ModerationPage() {
  const session = await getServerSession();
  
  if (!session || session.user.role !== 'moderator') {
    redirect('/');
  }

  // Fetch submissions server-side
  const submissions = await fetch(
    `${env.NEXT_PUBLIC_DJANGO_API_URL}/moderation/queue/`,
    {
      headers: { Authorization: `Bearer ${session.accessToken}` },
      cache: 'no-store'
    }
  ).then(r => r.json());

  return <ModerationQueue initialSubmissions={submissions} />;
}

Checklist

  • Create app/admin/page.tsx
  • Create app/admin/moderation/page.tsx
  • Implement role-based access
  • Add server-side permission checks
  • Test moderator access
  • Test admin dashboard
  • Verify Sacred Pipeline

Task 12.7: Authentication Pages (4 hours)

Login Page

app/auth/login/page.tsx (Client Component - needs form state):

'use client';

import { LoginForm } from '@/components/auth/LoginForm';
import { redirect } from 'next/navigation';
import { useSession } from 'next-auth/react';

export default function LoginPage() {
  const { data: session } = useSession();
  
  if (session) {
    redirect('/profile');
  }

  return (
    <div>
      <h1>Login</h1>
      <LoginForm />
    </div>
  );
}

Checklist

  • Create app/auth/login/page.tsx
  • Create app/auth/register/page.tsx
  • Create app/auth/callback/page.tsx
  • Create app/auth/reset-password/page.tsx
  • Test email/password login
  • Test OAuth login (Google, GitHub)
  • Test registration
  • Test password reset

Task 12.8: Search & Contact Pages (3 hours)

Search Page

app/search/page.tsx (Client Component - needs interactive search):

'use client';

import { useSearchParams } from 'next/navigation';
import { SearchResults } from '@/components/search/SearchResults';
import { SearchFilters } from '@/components/search/SearchFilters';

export default function SearchPage() {
  const searchParams = useSearchParams();
  const query = searchParams.get('q');

  return (
    <div>
      <h1>Search Results</h1>
      <SearchFilters />
      <SearchResults query={query} />
    </div>
  );
}

Contact Page

app/contact/page.tsx (Server Component):

import { ContactForm } from '@/components/contact/ContactForm';

export const metadata = {
  title: 'Contact Us - ThrillWiki',
};

export default function ContactPage() {
  return (
    <div>
      <h1>Contact Us</h1>
      <ContactForm />
    </div>
  );
}

Checklist

  • Create app/search/page.tsx
  • Create app/contact/page.tsx
  • Test search functionality
  • Test contact form submission
  • Verify form validation

Task 12.9: Component Migration (6 hours)

Convert React components to work with Next.js.

Server Components (Default)

Components that only display data and don't need interactivity:

  • Lists (ParksL, RidesList)
  • Detail views (ParkDetail, RideDetail)
  • Static content

Client Components ('use client')

Components that need interactivity:

  • Forms (LoginForm, SubmissionForm)
  • Interactive filters
  • Modals and dialogs
  • Components using useState, useEffect, etc.

Checklist

  • Identify all components using Supabase
  • Determine Server vs Client Component
  • Add 'use client' where needed
  • Replace Supabase with service calls
  • Test all components render
  • Verify interactions work
  • Check no hydration errors

Task 12.10: Routing & Navigation (4 hours)

Update all navigation to use Next.js routing.

Replace React Router

// OLD (React Router)
import { Link, useNavigate } from 'react-router-dom';

// NEW (Next.js)
import Link from 'next/link';
import { useRouter } from 'next/navigation';
// OLD
<Link to="/parks">Parks</Link>

// NEW
<Link href="/parks">Parks</Link>

Checklist

  • Replace all React Router imports
  • Update all components
  • Update all navigate() calls to router.push()
  • Update all useParams to use params prop
  • Update all useSearchParams
  • Test navigation works
  • Test browser back button
  • Test deep linking

🎯 Success Criteria

Zero React Router Usage

  • No react-router-dom imports remain
  • All navigation uses Next.js Link/router
  • No old React pages in src/pages/ (move to app/)

Zero Supabase Usage

  • grep -r "supabase\." app/ components/ lib/ --include="*.ts" --include="*.tsx" returns 0
  • grep -r "from '@/lib/supabaseClient'" returns 0
  • grep -r "from '@supabase/supabase-js'" returns 0

All URLs Preserved

  • /parks works
  • /parks/[parkSlug] works
  • /parks/[parkSlug]/rides works
  • /owners/[ownerSlug]/parks works
  • /rides works
  • /rides/[rideSlug] works
  • /ride-models/[modelSlug] works
  • /manufacturers/[manufacturerSlug] works
  • /manufacturers/[manufacturerSlug]/rides works
  • All other URLs work

All Pages Load

  • Homepage loads (SSR)
  • All park pages load
  • All ride pages load
  • All company pages load
  • Profile page loads (auth check)
  • Settings page loads (auth check)
  • Admin dashboard loads (role check)
  • Moderation queue loads (role check)
  • Search page loads
  • Contact page loads
  • Auth pages load

All Features Work

  • Can browse entities
  • Can view entity details
  • Can filter/sort
  • Can submit content (creates submission)
  • Can moderate content
  • Can write reviews
  • Can add ride credits
  • Can manage top lists
  • Can upload photos
  • Can search
  • Authentication works
  • Server-side rendering works
  • Client-side navigation works

Next.js Specific

  • Server Components render on server
  • Client Components work in browser
  • No hydration errors
  • Loading states display
  • Error boundaries catch errors
  • Metadata API generates correct tags
  • ISR/SSR configured correctly
  • Build succeeds without errors

Sacred Pipeline Intact

  • All entity changes go through submissions
  • Moderation queue receives submissions
  • Approval creates entities/updates
  • Rejection saves reason
  • No pipeline bypasses

📝 Implementation Strategy

1. Start with Entity Pages (Most Critical)

Focus on parks, rides, companies first since they're core functionality.

2. Then User Pages

Profile and settings are user-facing and important.

3. Then Admin Pages

Moderation queue and admin dashboard.

4. Then Misc Pages

Homepage, search, contact, etc.

5. Component Sweep

Find any remaining components with Supabase calls.

Run grep commands to find ANY remaining Supabase usage.


🚨 Critical Rules

DO NOT

  • Skip any page
  • Leave any supabase. calls
  • Assume a page works without testing it
  • Move to Phase 13 until grep returns 0 results

MUST DO

  • Test EVERY page you touch
  • Verify EVERY interaction works
  • Check console for errors
  • Verify Sacred Pipeline intact
  • Document any issues found

Refer back to previous phases for service usage examples:

  • Phase 1: Foundation (BaseService pattern)
  • Phase 4: Entity Services (parks, rides, companies)
  • Phase 5: Reviews & Social
  • Phase 6: Moderation & Admin
  • Phase 7: Media & Photos
  • Phase 8: Search
  • Phase 9: Timeline & History
  • Phase 10: Users & Profiles
  • Phase 11: Contact & Reports

⏭️ Next Phase

Once this phase is complete, proceed to Phase 13: Next.js Optimization



Document Version: 2.0
Last Updated: November 9, 2025
Changes: Converted to Next.js 15 App Router migration