# How to Build Reviews & ratings with V0

- Tool: v0
- Difficulty: Beginner
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

Build a star rating and review system with V0 using Next.js and Supabase that adds user reviews with 1-5 star ratings, helpful votes, photo uploads, and basic moderation to any product or service app. Features aggregate stats via database triggers — all in about 30-60 minutes.

## Before you start

- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A product, service, or item you want users to review

## Step-by-step guide

### 1. Set up the project and reviews schema with triggers

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the schema for reviews, images, helpful votes, and aggregate stats. Add a database trigger to auto-update stats on every review insert or update.

```
// Paste this prompt into V0's AI chat:
// Build a reviews and ratings system. Create a Supabase schema with:
// 1. reviews: id (uuid PK), author_id (uuid FK), item_id (uuid FK), item_type (text check product/service/business), rating (integer check 1-5), title (text), body (text), is_verified_purchase (boolean default false), status (text check pending/approved/rejected/flagged), created_at (timestamptz), updated_at (timestamptz)
// 2. review_images: id (uuid PK), review_id (uuid FK), image_url (text), position (integer)
// 3. helpful_votes: id (uuid PK), review_id (uuid FK), user_id (uuid FK), created_at (timestamptz) with unique constraint on (review_id, user_id)
// 4. review_stats: item_id (uuid PK), average_rating (numeric), total_reviews (integer), rating_distribution (jsonb), updated_at (timestamptz)
// Create a database trigger on reviews INSERT/UPDATE that recalculates review_stats for the affected item_id.
// RLS: anyone can SELECT approved reviews, authenticated users can INSERT, only author/admin can UPDATE.
// Generate SQL migration and TypeScript types.
```

> Pro tip: The database trigger keeps review_stats always current without expensive COUNT queries — this is critical for performance as your review count grows.

**Expected result:** Supabase is connected with reviews, images, votes, and stats tables. A database trigger automatically recalculates aggregate stats whenever a review is added or updated.

### 2. Build the star rating component and review form

Create a reusable star rating input component and the review submission form. The star component uses Lucide Star icons with hover and click states. The form includes rating, title, body, and photo upload.

```
'use client'

import { useState } from 'react'
import { Star } from 'lucide-react'
import { cn } from '@/lib/utils'

export function StarRating({
  value,
  onChange,
  readonly = false,
}: {
  value: number
  onChange?: (rating: number) => void
  readonly?: boolean
}) {
  const [hover, setHover] = useState(0)

  return (
    <div className="flex gap-1">
      {[1, 2, 3, 4, 5].map((star) => (
        <Star
          key={star}
          className={cn(
            'w-6 h-6 transition-colors',
            (hover || value) >= star
              ? 'fill-yellow-400 text-yellow-400'
              : 'text-muted-foreground',
            !readonly && 'cursor-pointer'
          )}
          onMouseEnter={() => !readonly && setHover(star)}
          onMouseLeave={() => !readonly && setHover(0)}
          onClick={() => onChange?.(star)}
        />
      ))}
    </div>
  )
}
```

**Expected result:** A reusable star rating component with hover and click states. Stars fill with yellow on hover and lock on click. Can be used as input (interactive) or display (readonly).

### 3. Create the reviews list with helpful votes

Build the reviews display for an item page. Reviews are sorted by most helpful or most recent, with aggregate stats at the top showing average rating and distribution breakdown.

```
// Paste this prompt into V0's AI chat:
// Build a reviews section for app/items/[id]/reviews/page.tsx.
// Requirements:
// - Top section: aggregate stats from review_stats table
//   - Large average rating number with StarRating (readonly), total review count
//   - Rating distribution bars: 5 rows (5 stars to 1 star), each with a Progress bar and count
// - Sort controls: DropdownMenu with options: Most Helpful, Newest, Highest Rated, Lowest Rated
// - Review list: shadcn/ui Card for each review showing:
//   - Avatar with author name, StarRating (readonly), date
//   - Badge for "Verified Purchase" if is_verified_purchase
//   - Review title (bold), body text
//   - Review photos as small image thumbnails (if any)
//   - "Helpful" Button with count — clicking calls Server Action markHelpful() with optimistic update
// - "Write a Review" Button at top opens the review form Dialog
// - Use Server Components for data fetching, 'use client' for star rating and helpful vote
// - Server Actions: submitReview(), markHelpful()
```

> Pro tip: Use V0's Design Mode (Option+D) to visually adjust the star rating colors, review Card spacing, and image gallery layout without spending credits.

**Expected result:** A reviews section showing aggregate stats with distribution bars, sortable review Cards with star ratings, helpful vote buttons, and verified purchase Badges.

### 4. Add the admin moderation queue

Build an admin page for moderating reviews. New reviews start with status 'pending' and need approval before they appear publicly. Admins can approve, reject, or flag reviews.

```
// Paste this prompt into V0's AI chat:
// Build a review moderation page at app/reviews/page.tsx.
// Requirements:
// - Protected by admin auth
// - Tabs: Pending, Approved, Rejected, Flagged
// - Table of reviews showing: item name, author, rating (stars), title, body preview (50 chars), status Badge, date
// - Each row has action Buttons:
//   - Approve (green): sets status to 'approved', triggers stats recalculation
//   - Reject (red): opens AlertDialog for rejection reason, sets status to 'rejected'
//   - Flag (yellow): sets status to 'flagged' for further review
// - Bulk action: Checkbox selection with bulk approve/reject Buttons
// - Server Actions: moderateReview() that updates status and logs the action
// - Server Components for data fetching
// - Use shadcn/ui Table, Badge, Button, AlertDialog, Checkbox, Tabs
```

**Expected result:** An admin moderation queue with tabs for review statuses, a Table of reviews with approve/reject/flag actions, and bulk moderation capabilities.

## Complete code example

File: `app/actions/reviews.ts`

```typescript
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function submitReview(formData: FormData) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Must be logged in')

  const { data: review } = await supabase
    .from('reviews')
    .insert({
      author_id: user.id,
      item_id: formData.get('item_id') as string,
      item_type: formData.get('item_type') as string,
      rating: parseInt(formData.get('rating') as string),
      title: formData.get('title') as string,
      body: formData.get('body') as string,
      status: 'pending',
    })
    .select()
    .single()

  if (!review) throw new Error('Failed to submit review')

  const itemId = formData.get('item_id') as string
  revalidatePath(`/items/${itemId}/reviews`)
  return review
}

export async function markHelpful(reviewId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Must be logged in')

  const { error } = await supabase
    .from('helpful_votes')
    .insert({ review_id: reviewId, user_id: user.id })

  if (error && error.code !== '23505') {
    throw new Error(error.message)
  }
}

export async function moderateReview(
  reviewId: string,
  status: 'approved' | 'rejected' | 'flagged'
) {
  const supabase = await createClient()

  await supabase
    .from('reviews')
    .update({ status, updated_at: new Date().toISOString() })
    .eq('id', reviewId)

  revalidatePath('/reviews')
}
```

## Common mistakes

- **Computing average ratings with a COUNT query on every page load** — As review counts grow, counting and averaging across thousands of rows on every request becomes slow and expensive. Fix: Use a Supabase database trigger on reviews INSERT/UPDATE that recalculates review_stats for the affected item_id. The display page reads from the pre-computed stats table.
- **Allowing the star rating component to render server-side** — The star rating uses useState for hover state and onClick handlers. Server Components cannot use React hooks. Fix: Mark the star rating component with 'use client'. When displaying reviews, use a readonly version of the same component.
- **Not handling the 23505 duplicate key error for helpful votes** — Users clicking 'Helpful' twice triggers a unique constraint violation. Without handling, this shows an ugly error message. Fix: Catch the 23505 error code in the markHelpful Server Action and silently ignore it — the vote is already recorded.

## Best practices

- Use Supabase database triggers to maintain aggregate review_stats instead of computing averages on every page load
- Use V0's Design Mode (Option+D) to visually adjust star colors, review Card spacing, and image thumbnails for free
- Store review images in a Supabase Storage public bucket for permanent, SEO-friendly URLs
- Handle the Supabase 23505 duplicate key error gracefully for helpful vote deduplication
- Use Server Components for the reviews list to optimize SEO — reviews are valuable search content
- Start reviews with status 'pending' and require admin approval to prevent spam from appearing publicly

## Frequently asked questions

### Can I build this on V0's free tier?

Yes. The free tier provides enough credits to generate the star rating component, review form, reviews list, and moderation page. Supabase free tier handles the database and image storage.

### How do aggregate stats stay up to date?

A Supabase database trigger fires on every review INSERT or UPDATE. It recalculates the average_rating, total_reviews, and rating_distribution for the affected item and updates the review_stats table. The display page reads from this pre-computed table.

### How do I prevent fake reviews?

The build includes three layers: authentication (only logged-in users can review), moderation queue (admin approves before public display), and verified purchase badges. You can add AI-based spam detection as a customization.

### Can I use this for any type of item (products, services, businesses)?

Yes. The item_type field (product/service/business) makes the system generic. Reviews are linked to any item via item_id, so you can attach reviews to products, services, courses, or any entity in your app.

### How do I deploy the reviews system?

Click Share then Publish to Production in V0. Review pages are server-rendered for SEO. Supabase credentials are auto-configured from the Connect panel.

### Can RapidDev help build a custom reviews platform?

Yes. RapidDev has built 600+ apps including review platforms with AI moderation, sentiment analysis, and review incentive systems. Book a free consultation to discuss your requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/reviews-ratings
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/reviews-ratings
