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-name3. Database Changes (if needed)
If the feature requires schema changes:
# Edit prisma/schema.prisma
# Then create migration
npx prisma migrate dev --name add_feature_name4. Implement the Feature
Follow this order:
- Database models and queries
- Server actions or API routes
- Page components
- 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-nameCode 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 pageNew Component
components/
└── features/
└── new-feature/
├── index.ts # Exports
├── feature-card.tsx # Main component
├── feature-form.tsx # Form component
└── feature-skeleton.tsx # Loading skeletonNew Server Action
lib/
└── actions/
└── new-feature.ts # Server actions for featureExample: 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_reviews2. 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)