Documentation is currently in beta. Report issues →
GuidesAdding Features

Adding Features

A guide to the Y3NKO development workflow for adding new features.

Development Workflow

1. Understand the Feature

Before coding:

  • Review requirements
  • Check existing patterns in the codebase
  • Identify affected areas (routes, components, database)
  • Consider mobile experience

2. Create a Branch

git checkout -b feature/your-feature-name

3. Database Changes (if needed)

If the feature requires schema changes:

# Edit prisma/schema.prisma
# Then create migration
npx prisma migrate dev --name add_feature_name

4. Implement the Feature

Follow this order:

  1. Database models and queries
  2. Server actions or API routes
  3. Page components
  4. Client interactions

5. Test Thoroughly

  • Test on mobile viewport
  • Test with different user roles
  • Test error states
  • Check loading states

6. Create Pull Request

git add .
git commit -m "feat: add feature name"
git push origin feature/your-feature-name

Code Patterns

Server Components (Default)

Most pages should be Server Components:

// app/accommodations/page.tsx
import { getListings } from '@/lib/actions/listings'
 
export default async function AccommodationsPage() {
  const listings = await getListings()
 
  return (
    <main>
      <h1>Accommodations</h1>
      <ListingGrid listings={listings} />
    </main>
  )
}

Client Components (When Needed)

Use Client Components only for:

  • User interactions (onClick, onChange)
  • Browser APIs
  • React hooks
// components/features/search/search-bar.tsx
"use client"
 
import { useState } from 'react'
import { useRouter } from 'next/navigation'
 
export function SearchBar() {
  const [query, setQuery] = useState('')
  const router = useRouter()
 
  function handleSearch() {
    router.push(`/accommodations?search=${query}`)
  }
 
  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={handleSearch}>Search</button>
    </div>
  )
}

Server Actions for Mutations

// lib/actions/reviews.ts
"use server"
 
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'
 
export async function createReview(data: CreateReviewInput) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) {
    throw new Error('Please log in to leave a review')
  }
 
  const review = await prisma.review.create({
    data: {
      userId: user.id,
      listingId: data.listingId,
      rating: data.rating,
      comment: data.comment
    }
  })
 
  // Revalidate affected pages
  revalidatePath(`/accommodations/${data.listingSlug}`)
 
  return review
}

File Organization

New Page

app/
└── new-feature/
    ├── page.tsx          # Main page
    ├── loading.tsx       # Loading skeleton
    ├── error.tsx         # Error boundary
    └── [id]/
        └── page.tsx      # Detail page

New Component

components/
└── features/
    └── new-feature/
        ├── index.ts              # Exports
        ├── feature-card.tsx      # Main component
        ├── feature-form.tsx      # Form component
        └── feature-skeleton.tsx  # Loading skeleton

New Server Action

lib/
└── actions/
    └── new-feature.ts    # Server actions for feature

Example: Adding Reviews

1. Schema Update

// prisma/schema.prisma
model Review {
  id        String   @id @default(cuid())
  userId    String
  listingId String
  rating    Int      // 1-5
  comment   String   @db.Text
  createdAt DateTime @default(now())
 
  user    User    @relation(fields: [userId], references: [id])
  listing Listing @relation(fields: [listingId], references: [id])
 
  @@index([listingId])
  @@index([userId])
}
npx prisma migrate dev --name add_reviews

2. Server Actions

// lib/actions/reviews.ts
"use server"
 
import { prisma } from '@/lib/db'
import { getCurrentUser } from '@/lib/auth'
import { z } from 'zod'
 
const reviewSchema = z.object({
  listingId: z.string().cuid(),
  rating: z.number().min(1).max(5),
  comment: z.string().min(10).max(1000)
})
 
export async function createReview(input: z.infer<typeof reviewSchema>) {
  const user = await getCurrentUser()
  if (!user) throw new Error('Unauthorized')
 
  const validated = reviewSchema.parse(input)
 
  return prisma.review.create({
    data: {
      ...validated,
      userId: user.id
    }
  })
}
 
export async function getListingReviews(listingId: string) {
  return prisma.review.findMany({
    where: { listingId },
    include: {
      user: {
        include: { profile: true }
      }
    },
    orderBy: { createdAt: 'desc' }
  })
}

3. Components

// components/features/reviews/review-card.tsx
interface ReviewCardProps {
  review: Review & { user: User & { profile: Profile } }
}
 
export function ReviewCard({ review }: ReviewCardProps) {
  return (
    <div className="border-b py-4">
      <div className="flex items-center gap-3 mb-2">
        <Avatar>
          <AvatarImage src={review.user.profile?.avatarUrl} />
          <AvatarFallback>
            {review.user.profile?.firstName?.[0]}
          </AvatarFallback>
        </Avatar>
        <div>
          <p className="font-medium">
            {review.user.profile?.firstName}
          </p>
          <p className="text-sm text-gray-500">
            {formatDate(review.createdAt)}
          </p>
        </div>
      </div>
      <StarRating rating={review.rating} />
      <p className="mt-2 text-gray-700">{review.comment}</p>
    </div>
  )
}
// components/features/reviews/review-form.tsx
"use client"
 
import { useState } from 'react'
import { createReview } from '@/lib/actions/reviews'
 
export function ReviewForm({ listingId }: { listingId: string }) {
  const [rating, setRating] = useState(5)
  const [comment, setComment] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)
 
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setIsSubmitting(true)
 
    try {
      await createReview({ listingId, rating, comment })
      setComment('')
      toast.success('Review submitted!')
    } catch (error) {
      toast.error('Failed to submit review')
    } finally {
      setIsSubmitting(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <StarInput value={rating} onChange={setRating} />
      <Textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Share your experience..."
      />
      <Button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Review'}
      </Button>
    </form>
  )
}

4. Integration

// app/accommodations/[slug]/page.tsx
import { getListingReviews } from '@/lib/actions/reviews'
import { ReviewCard, ReviewForm } from '@/components/features/reviews'
 
export default async function ListingPage({ params }) {
  const [listing, reviews] = await Promise.all([
    getListing(params.slug),
    getListingReviews(listing.id)
  ])
 
  return (
    <main>
      <ListingDetail listing={listing} />
 
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Reviews</h2>
        <ReviewForm listingId={listing.id} />
        <div className="mt-8 space-y-4">
          {reviews.map(review => (
            <ReviewCard key={review.id} review={review} />
          ))}
        </div>
      </section>
    </main>
  )
}

Checklist

Before submitting a PR:

  • Feature works on mobile
  • Loading states implemented
  • Error states handled
  • TypeScript types correct
  • No console errors
  • Accessibility checked (keyboard, screen reader)
  • Code follows existing patterns
  • Database migrations included (if applicable)