Code Style
Coding conventions and patterns used in Y3NKO.
TypeScript
Strict Mode
Use TypeScript strictly — no any types:
// Bad
function process(data: any) { }
// Good
interface UserData {
id: string
name: string
}
function process(data: UserData) { }Type Definitions
Place shared types in types/index.ts:
// types/index.ts
export interface ListingFilters {
region?: string
city?: string
minPrice?: number
maxPrice?: number
type?: ListingType
}
export type BookingStatus = 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'Prisma Types
Use Prisma’s generated types:
import { Listing, Booking } from '@prisma/client'
import { Prisma } from '@prisma/client'
// With relations
type ListingWithImages = Prisma.ListingGetPayload<{
include: { images: true }
}>React Components
Server Components (Default)
// app/accommodations/page.tsx
import { getListings } from '@/lib/actions/listings'
export default async function AccommodationsPage() {
const listings = await getListings()
return (
<main>
<ListingGrid listings={listings} />
</main>
)
}Client Components
Only use when needed for interactivity:
// components/features/search/search-bar.tsx
"use client"
import { useState } from 'react'
export function SearchBar({ onSearch }: SearchBarProps) {
const [query, setQuery] = useState('')
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
)
}Component Props
Define interfaces for props:
interface ListingCardProps {
listing: Listing & { images: ListingImage[] }
showFavorite?: boolean
className?: string
}
export function ListingCard({ listing, showFavorite = true, className }: ListingCardProps) {
// ...
}File Naming
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | ListingCard.tsx |
| Utilities | camelCase | formatCurrency.ts |
| Pages | page.tsx | app/accommodations/page.tsx |
| Layouts | layout.tsx | app/layout.tsx |
| API Routes | route.ts | app/api/bookings/route.ts |
| Server Actions | camelCase | lib/actions/bookings.ts |
Imports
Use path aliases:
// Good
import { Button } from '@/components/ui/button'
import { prisma } from '@/lib/db'
// Avoid relative imports going up multiple levels
import { Button } from '../../../components/ui/button'Order imports:
- React/Next.js
- Third-party libraries
- Local modules (
@/)
import { Suspense } from 'react'
import Link from 'next/link'
import { format } from 'date-fns'
import { prisma } from '@/lib/db'
import { Button } from '@/components/ui/button'Error Handling
Server Actions
"use server"
export async function createBooking(data: BookingInput) {
const user = await getCurrentUser()
if (!user) {
throw new Error('Please log in to book')
}
try {
// Operation
return { success: true, data: booking }
} catch (error) {
console.error('Booking error:', error)
throw new Error('Failed to create booking')
}
}API Routes
export async function POST(request: Request) {
try {
// Operation
return NextResponse.json({ data: result })
} catch (error) {
console.error('API error:', error)
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
{ status: 500 }
)
}
}Validation
Use Zod for all input validation:
import { z } from 'zod'
export const bookingSchema = z.object({
listingId: z.string().cuid(),
checkIn: z.string().datetime(),
checkOut: z.string().datetime(),
adults: z.number().int().min(1).max(20),
guestEmail: z.string().email(),
guestPhone: z.string().regex(/^\+233[0-9]{9}$/),
}).refine(
data => new Date(data.checkOut) > new Date(data.checkIn),
{ message: "Check-out must be after check-in" }
)CSS / Styling
Tailwind Classes
Order classes logically:
- Layout (display, position)
- Sizing (width, height)
- Spacing (margin, padding)
- Typography
- Colors/Background
- Effects
- Responsive
<div className="flex items-center justify-between w-full p-4 text-lg font-medium text-gray-900 bg-white rounded-lg shadow-md hover:bg-gray-50 md:p-6">Custom CSS Variables
Use CSS variables for brand colors:
// Use CSS variables
<div className="text-[var(--ghana-red)]">
// Or direct hex for clarity
<div className="text-[#CE1126]">Comments
Comment why, not what:
// Bad - describes what
// Loop through users
for (const user of users) { }
// Good - explains why
// Process users in batches to avoid memory issues
for (const batch of chunks(users, 100)) { }Testing
Unit Tests
// lib/__tests__/pricing.test.ts
import { calculatePricing } from '../pricing'
describe('calculatePricing', () => {
it('calculates total correctly', () => {
const result = calculatePricing({
pricePerNight: 100,
nights: 3,
currency: 'GHS'
})
expect(result.subtotal).toBe(300)
expect(result.total).toBeGreaterThan(300) // Includes fees
})
})Accessibility
Always include:
aria-labelfor icon-only buttonsalttext for images- Proper heading hierarchy
- Focus states
<button aria-label="Add to favorites">
<Heart className="w-5 h-5" />
</button>
<Image src={url} alt="Villa exterior with pool" />