
Buying
5 min read
Abdoh
10/7/2025
La title el grande
descriptions la descripio
# Guides Migration Guide: Vite/React to Next.js This guide explains how to migrate the guides-related functionality from this Vite/React project to a Next.js 13+ project with App Router. ## Next.js Project Structure ``` .next/ # Build output (auto-generated, don't touch) ├── build/ ├── cache/ ├── server/ ├── static/ └── types/ .vscode/ # VS Code workspace settings .yarn/ # Yarn package manager files app/ # Next.js App Router (main application code) components/ # Shared React components hooks/ # Custom React hooks lib/ # Utility functions and types ├── helpers/ # Helper/utility functions └── types/ # TypeScript type definitions node_modules/ # Dependencies (auto-generated) prisma/ # Prisma ORM schema and migrations public/ # Static assets (images, fonts, etc.) services/ # API services and business logic ``` --- ## Migration Steps ### 1. Install Next.js Project ```bash npx create-next-app@latest guides-nextjs --typescript --tailwind --app --src-dir false cd guides-nextjs ``` ### 2. Install Required Dependencies ```bash npm install prisma @prisma/client npm install @radix-ui/react-slot @radix-ui/react-separator npm install class-variance-authority clsx tailwind-merge npm install lucide-react npm install date-fns ``` ### 3. Setup Prisma Schema **File: `prisma/schema.prisma`** ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" // or "mysql", "sqlite" url = env("DATABASE_URL") } model Guide { id String @id @default(cuid()) title String description String content String @db.Text category String author String readTime Int publishedAt DateTime @default(now()) imageUrl String? slug String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category]) @@index([slug]) @@index([publishedAt]) } ``` Initialize Prisma: ```bash npx prisma init npx prisma generate npx prisma db push ``` --- ## File Migration Mapping ### Current Files → Next.js Structure | Current File | Next.js Location | Notes | |-------------|------------------|-------| | `src/pages/Guides.tsx` | `app/guides/page.tsx` | Convert to Server Component | | `src/pages/GuideDetail.tsx` | `app/guide/[slug]/page.tsx` | Dynamic route with params | | `src/components/GuideSection.tsx` | `components/GuideSection.tsx` | Keep as Client Component | | `src/components/ui/*` | `components/ui/*` | Copy all shadcn components | | - | `lib/prisma.ts` | New: Prisma client singleton | | - | `services/guides.ts` | New: Guide data fetching | | - | `lib/types/guide.ts` | New: Guide TypeScript types | --- ## Code Transformations ### 1. Prisma Client Setup **File: `lib/prisma.ts`** ```typescript import { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; ``` ### 2. Type Definitions **File: `lib/types/guide.ts`** ```typescript export interface Guide { id: string; title: string; description: string; content: string; category: string; author: string; readTime: number; publishedAt: Date; imageUrl: string | null; slug: string; } export type GuideCategory = "Buying" | "Safety" | "Finance" | "Investment" | "Maintenance" | "Legal"; ``` ### 3. Guide Services **File: `services/guides.ts`** ```typescript import { prisma } from '@/lib/prisma'; import { Guide } from '@/lib/types/guide'; export async function getAllGuides(): Promise<Guide[]> { return await prisma.guide.findMany({ orderBy: { publishedAt: 'desc' }, }); } export async function getGuideBySlug(slug: string): Promise<Guide | null> { return await prisma.guide.findUnique({ where: { slug }, }); } export async function getGuidesByCategory(category: string): Promise<Guide[]> { return await prisma.guide.findMany({ where: { category }, orderBy: { publishedAt: 'desc' }, }); } export async function searchGuides(query: string): Promise<Guide[]> { return await prisma.guide.findMany({ where: { OR: [ { title: { contains: query, mode: 'insensitive' } }, { description: { contains: query, mode: 'insensitive' } }, ], }, orderBy: { publishedAt: 'desc' }, }); } ``` ### 4. Guides Listing Page (Server Component) **File: `app/guides/page.tsx`** ```typescript import { Metadata } from 'next'; import { getAllGuides } from '@/services/guides'; import GuidesClient from './GuidesClient'; export const metadata: Metadata = { title: 'Property Guides & Resources | Expert Real Estate Advice', description: 'Expert advice, tips, and insights to help you navigate the Somali real estate market', openGraph: { title: 'Property Guides & Resources', description: 'Expert advice, tips, and insights to help you navigate the Somali real estate market', }, }; export default async function GuidesPage() { const guides = await getAllGuides(); return <GuidesClient initialGuides={guides} />; } ``` ### 5. Guides Client Component **File: `app/guides/GuidesClient.tsx`** ```typescript 'use client'; import { useState } from "react"; import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { BookOpen, Clock, Search, ArrowRight } from "lucide-react"; import { Guide } from "@/lib/types/guide"; interface GuidesClientProps { initialGuides: Guide[]; } export default function GuidesClient({ initialGuides }: GuidesClientProps) { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("all"); const categories = ["all", "Buying", "Safety", "Finance", "Investment", "Maintenance", "Legal"]; const filteredGuides = initialGuides.filter(guide => { const matchesSearch = guide.title.toLowerCase().includes(searchQuery.toLowerCase()) || guide.description.toLowerCase().includes(searchQuery.toLowerCase()); const matchesCategory = selectedCategory === "all" || guide.category === selectedCategory; return matchesSearch && matchesCategory; }); return ( <div className="min-h-screen bg-background"> <main> {/* Hero Section */} <section className="bg-hero-gradient py-16"> <div className="container mx-auto px-4"> <div className="max-w-3xl mx-auto text-center"> <h1 className="text-4xl md:text-5xl font-bold text-primary-foreground mb-4"> Property Guides & Resources </h1> <p className="text-xl text-primary-foreground/90 mb-8"> Expert advice, tips, and insights to help you navigate the Somali real estate market </p> {/* Search Bar */} <div className="relative max-w-2xl mx-auto"> <Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" /> <Input type="text" placeholder="Search guides..." className="pl-12 pr-4 py-6 text-lg bg-background" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> </div> </div> </div> </section> {/* Category Filters */} <section className="border-b bg-background sticky top-0 z-10 shadow-sm"> <div className="container mx-auto px-4 py-4"> <div className="flex gap-2 overflow-x-auto scrollbar-hide"> {categories.map((category) => ( <Button key={category} variant={selectedCategory === category ? "default" : "outline"} size="sm" onClick={() => setSelectedCategory(category)} className="whitespace-nowrap" > {category === "all" ? "All Guides" : category} </Button> ))} </div> </div> </section> {/* Guides Grid */} <section className="py-12"> <div className="container mx-auto px-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {filteredGuides.map((guide) => ( <Link key={guide.id} href={`/guide/${guide.slug}`}> <Card className="group hover:shadow-hover transition-all duration-300 cursor-pointer hover:-translate-y-1"> <div className="aspect-video bg-muted overflow-hidden"> <img src={guide.imageUrl || '/placeholder.svg'} alt={guide.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" /> </div> <CardHeader> <div className="flex items-center gap-2 mb-2"> <Badge variant="secondary">{guide.category}</Badge> <div className="flex items-center text-sm text-muted-foreground"> <Clock className="h-4 w-4 mr-1" /> {guide.readTime} min read </div> </div> <CardTitle className="line-clamp-2 group-hover:text-primary transition-colors"> {guide.title} </CardTitle> <CardDescription className="line-clamp-2"> {guide.description} </CardDescription> </CardHeader> <CardContent> <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">By {guide.author}</span> <span className="text-muted-foreground"> {new Date(guide.publishedAt).toLocaleDateString()} </span> </div> <Button variant="ghost" className="w-full mt-4 group-hover:bg-primary group-hover:text-primary-foreground"> Read More <ArrowRight className="ml-2 h-4 w-4" /> </Button> </CardContent> </Card> </Link> ))} </div> {filteredGuides.length === 0 && ( <div className="text-center py-12"> <BookOpen className="h-16 w-16 mx-auto text-muted-foreground mb-4" /> <h3 className="text-xl font-semibold mb-2">No guides found</h3> <p className="text-muted-foreground">Try adjusting your search or filter criteria</p> </div> )} </div> </section> </main> </div> ); } ``` ### 6. Guide Detail Page (Server Component) **File: `app/guide/[slug]/page.tsx`** ```typescript import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { getGuideBySlug } from '@/services/guides'; import GuideDetailClient from './GuideDetailClient'; interface GuideDetailPageProps { params: { slug: string; }; } export async function generateMetadata({ params }: GuideDetailPageProps): Promise<Metadata> { const guide = await getGuideBySlug(params.slug); if (!guide) { return { title: 'Guide Not Found', }; } return { title: `${guide.title} | Property Guides`, description: guide.description, openGraph: { title: guide.title, description: guide.description, images: guide.imageUrl ? [guide.imageUrl] : [], }, }; } export default async function GuideDetailPage({ params }: GuideDetailPageProps) { const guide = await getGuideBySlug(params.slug); if (!guide) { notFound(); } return <GuideDetailClient guide={guide} />; } ``` ### 7. Guide Detail Client Component **File: `app/guide/[slug]/GuideDetailClient.tsx`** ```typescript 'use client'; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { ArrowLeft, Clock, Calendar, User } from "lucide-react"; import { Guide } from "@/lib/types/guide"; interface GuideDetailClientProps { guide: Guide; } export default function GuideDetailClient({ guide }: GuideDetailClientProps) { return ( <div className="min-h-screen bg-background"> <main className="container mx-auto px-4 py-8 max-w-4xl"> <Link href="/guides"> <Button variant="ghost" className="mb-6"> <ArrowLeft className="mr-2 h-4 w-4" /> Back to Guides </Button> </Link> <article> {/* Hero Image */} <div className="aspect-video bg-muted rounded-lg overflow-hidden mb-8"> <img src={guide.imageUrl || '/placeholder.svg'} alt={guide.title} className="w-full h-full object-cover" /> </div> {/* Metadata */} <div className="flex flex-wrap items-center gap-4 mb-6"> <Badge variant="secondary" className="text-sm"> {guide.category} </Badge> <div className="flex items-center text-muted-foreground text-sm"> <Clock className="h-4 w-4 mr-1" /> {guide.readTime} min read </div> <div className="flex items-center text-muted-foreground text-sm"> <User className="h-4 w-4 mr-1" /> {guide.author} </div> <div className="flex items-center text-muted-foreground text-sm"> <Calendar className="h-4 w-4 mr-1" /> {new Date(guide.publishedAt).toLocaleDateString()} </div> </div> {/* Title */} <h1 className="text-4xl md:text-5xl font-bold mb-4"> {guide.title} </h1> {/* Description */} <p className="text-xl text-muted-foreground mb-8"> {guide.description} </p> <Separator className="mb-8" /> {/* Content */} <div className="prose prose-lg dark:prose-invert max-w-none"> {guide.content} </div> </article> </main> </div> ); } ``` ### 8. Guide Section Component (for Homepage) **File: `components/GuideSection.tsx`** ```typescript 'use client'; import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ArrowRight, Clock } from "lucide-react"; import { Guide } from "@/lib/types/guide"; interface GuideSectionProps { guides: Guide[]; } export default function GuideSection({ guides }: GuideSectionProps) { return ( <section className="py-16 bg-muted/30"> <div className="container mx-auto px-4"> <div className="text-center mb-12"> <h2 className="text-3xl md:text-4xl font-bold mb-4"> Property Guides & Resources </h2> <p className="text-lg text-muted-foreground max-w-2xl mx-auto"> Expert advice and insights to help you make informed decisions </p> </div> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> {guides.slice(0, 3).map((guide) => ( <Link key={guide.id} href={`/guide/${guide.slug}`}> <Card className="h-full hover:shadow-lg transition-shadow"> <CardHeader> <Badge variant="secondary" className="w-fit mb-2"> {guide.category} </Badge> <CardTitle className="line-clamp-2">{guide.title}</CardTitle> <CardDescription className="line-clamp-3"> {guide.description} </CardDescription> </CardHeader> <CardContent> <div className="flex items-center text-sm text-muted-foreground"> <Clock className="h-4 w-4 mr-1" /> {guide.readTime} min read </div> </CardContent> </Card> </Link> ))} </div> <div className="text-center"> <Link href="/guides"> <Button size="lg"> View All Guides <ArrowRight className="ml-2 h-5 w-5" /> </Button> </Link> </div> </div> </section> ); } ``` --- ## Key Differences & Next.js Concepts ### 1. **App Router vs React Router** - **Old**: `react-router-dom` with `<Route>` components - **New**: File-based routing in `app/` directory - `app/guides/page.tsx` → `/guides` - `app/guide/[slug]/page.tsx` → `/guide/:slug` ### 2. **Server Components (Default)** - Pages are Server Components by default - Fetch data directly in components (no `useEffect` needed) - Add `'use client'` directive for interactive components ### 3. **Data Fetching** - **Old**: `useState`, `useEffect`, client-side fetching - **New**: `async` Server Components with direct database queries ### 4. **Navigation** - **Old**: `useNavigate()` from `react-router-dom` - **New**: `<Link>` from `next/link` or `useRouter()` from `next/navigation` ### 5. **Metadata & SEO** - **Old**: Manual `<Helmet>` or meta tags - **New**: Export `metadata` object or `generateMetadata` function ### 6. **Static & Dynamic Rendering** - Pages with dynamic data use `params` and can be statically generated - Use `generateStaticParams` for static generation at build time --- ## Environment Variables **File: `.env`** ```env DATABASE_URL="postgresql://user:password@localhost:5432/mydb" ``` --- ## Additional Configuration ### Next.js Config **File: `next.config.js`** ```javascript /** @type {import('next').NextConfig} */ const nextConfig = { images: { domains: ['localhost'], // Add your image domains }, } module.exports = nextConfig ``` ### Tailwind Config Copy your existing `tailwind.config.ts` and `index.css` to maintain design consistency. --- ## Testing the Migration 1. Seed initial guide data: ```typescript // prisma/seed.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { await prisma.guide.createMany({ data: [ { title: "First-Time Home Buyer's Complete Guide in Somalia", description: "Everything you need to know about purchasing your first property", content: "Full guide content here...", category: "Buying", author: "Ahmed Hassan", readTime: 8, slug: "first-time-home-buyers-guide-somalia", }, // Add more guides... ], }); } main() .catch((e) => console.error(e)) .finally(async () => await prisma.$disconnect()); ``` Run: `npx prisma db seed` 2. Start development server: ```bash npm run dev ``` 3. Test routes: - `/guides` - Listing page - `/guide/first-time-home-buyers-guide-somalia` - Detail page --- ## Summary This migration transforms your client-side React app into a full-stack Next.js application with: - ✅ Server-side rendering for better SEO - ✅ Database integration with Prisma - ✅ Type-safe data fetching - ✅ Optimized performance with Server Components - ✅ Modern Next.js 13+ App Router patterns All guides functionality is preserved while gaining backend capabilities and improved performance.
