Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/seyarhasir/AiVault/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Reviews API allows users to rate and review AI tools. Authenticated users can add reviews, and anyone can view reviews for a specific tool.

Queries

getReviews

Fetch all reviews for a specific tool. Source: /home/daytona/workspace/source/convex/reviews.ts:12 Authentication: Not required
toolId
Id<'tools'>
required
ID of the tool to fetch reviews for
reviews
Review[]
Array of reviews for the specified tool
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";

function ToolReviews({ toolId }: { toolId: Id<"tools"> }) {
  const reviews = useQuery(api.reviews.getReviews, { toolId });
  
  return (
    <div>
      <h2>Reviews</h2>
      {reviews?.map(review => (
        <div key={review._id}>
          <div>Rating: {review.rating}/5</div>
          <p>{review.comment}</p>
          <small>{new Date(review.createdAt).toLocaleDateString()}</small>
        </div>
      ))}
    </div>
  );
}
Notes:
  • Reviews are returned in database order (not sorted)
  • You may want to sort by createdAt or rating client-side
  • No pagination is implemented (all reviews are returned)

Mutations

addReview

Add a new review for a tool. Source: /home/daytona/workspace/source/convex/reviews.ts:22 Authentication: Required
toolId
Id<'tools'>
required
ID of the tool to review
rating
number
required
Rating value (typically 1-5, but no validation is enforced)
comment
string
required
Review text/comment
Returns: void
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";

function AddReviewForm({ toolId }: { toolId: Id<"tools"> }) {
  const addReview = useMutation(api.reviews.addReview);
  const [rating, setRating] = useState(5);
  const [comment, setComment] = useState("");
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      await addReview({
        toolId,
        rating,
        comment
      });
      
      // Reset form
      setRating(5);
      setComment("");
      alert("Review submitted!");
    } catch (error) {
      if (error.message === "Unauthenticated") {
        alert("Please sign in to leave a review");
      }
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Rating:</label>
        <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
          <option value={5}>5 - Excellent</option>
          <option value={4}>4 - Good</option>
          <option value={3}>3 - Average</option>
          <option value={2}>2 - Poor</option>
          <option value={1}>1 - Terrible</option>
        </select>
      </div>
      
      <div>
        <label>Comment:</label>
        <textarea
          value={comment}
          onChange={(e) => setComment(e.target.value)}
          required
          placeholder="Share your experience with this tool..."
        />
      </div>
      
      <button type="submit">Submit Review</button>
    </form>
  );
}
Behavior:
  • Automatically captures the authenticated user’s ID
  • Sets createdAt to current timestamp
  • No validation for duplicate reviews (same user can review multiple times)
  • No validation for rating range
Errors:
  • "Unauthenticated" - User is not signed in

Schema

The reviews table has the following structure:
reviews: defineTable({
  userId: v.string(),        // Clerk user ID
  toolId: v.id("tools"),     // Reference to tools table
  rating: v.number(),        // Rating value
  comment: v.string(),       // Review text
  createdAt: v.number()      // Timestamp in milliseconds
})
  .index("by_toolId", ["toolId"])
  .index("by_userId", ["userId"])
Indices:
  • by_toolId - Fast lookup of all reviews for a tool
  • by_userId - Fast lookup of all reviews by a user

Example: Complete Review Feature

import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
import { useState } from "react";

function ReviewsSection({ toolId }: { toolId: Id<"tools"> }) {
  const reviews = useQuery(api.reviews.getReviews, { toolId });
  const addReview = useMutation(api.reviews.addReview);
  const [rating, setRating] = useState(5);
  const [comment, setComment] = useState("");
  const [showForm, setShowForm] = useState(false);
  
  // Calculate average rating
  const avgRating = reviews && reviews.length > 0
    ? reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length
    : 0;
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (comment.trim().length < 10) {
      alert("Please write a more detailed review (at least 10 characters)");
      return;
    }
    
    try {
      await addReview({ toolId, rating, comment });
      setRating(5);
      setComment("");
      setShowForm(false);
    } catch (error) {
      if (error.message === "Unauthenticated") {
        window.location.href = "/sign-in";
      }
    }
  };
  
  // Sort reviews by date (newest first)
  const sortedReviews = reviews
    ? [...reviews].sort((a, b) => b.createdAt - a.createdAt)
    : [];
  
  return (
    <div className="reviews-section">
      <div className="reviews-header">
        <h2>Reviews</h2>
        {reviews && reviews.length > 0 && (
          <div className="rating-summary">
            <span className="avg-rating">{avgRating.toFixed(1)}</span>
            <span className="star-rating">{'⭐'.repeat(Math.round(avgRating))}</span>
            <span className="review-count">({reviews.length} reviews)</span>
          </div>
        )}
        <button onClick={() => setShowForm(!showForm)}>
          {showForm ? "Cancel" : "Write a Review"}
        </button>
      </div>
      
      {showForm && (
        <form onSubmit={handleSubmit} className="review-form">
          <div>
            <label>Rating:</label>
            <div className="star-input">
              {[1, 2, 3, 4, 5].map(star => (
                <button
                  key={star}
                  type="button"
                  onClick={() => setRating(star)}
                  className={star <= rating ? 'active' : ''}
                >

                </button>
              ))}
            </div>
          </div>
          
          <div>
            <label>Your Review:</label>
            <textarea
              value={comment}
              onChange={(e) => setComment(e.target.value)}
              required
              minLength={10}
              placeholder="Share your experience with this tool..."
              rows={4}
            />
          </div>
          
          <button type="submit">Submit Review</button>
        </form>
      )}
      
      <div className="reviews-list">
        {sortedReviews.length === 0 ? (
          <p>No reviews yet. Be the first to review this tool!</p>
        ) : (
          sortedReviews.map(review => (
            <div key={review._id} className="review-card">
              <div className="review-header">
                <span className="rating">{'⭐'.repeat(review.rating)}</span>
                <span className="date">
                  {new Date(review.createdAt).toLocaleDateString()}
                </span>
              </div>
              <p className="comment">{review.comment}</p>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

Potential Enhancements

The current API is basic. Consider these improvements:
  1. Duplicate Prevention: Prevent users from reviewing the same tool multiple times
  2. Rating Validation: Enforce 1-5 rating range
  3. Edit/Delete: Allow users to edit or delete their reviews
  4. Helpful Votes: Add upvote/downvote for helpful reviews
  5. Pagination: Implement pagination for tools with many reviews
  6. Sorting: Add server-side sorting options (newest, highest rated, etc.)
  7. Moderation: Add admin review approval/rejection
  8. User Info: Return user name/avatar with reviews (from Clerk)
Example duplicate prevention:
export const addReview = mutation({
  args: {
    toolId: v.id("tools"),
    rating: v.number(),
    comment: v.string(),
  },
  handler: async (ctx, args) => {
    const identity = await getIdentity(ctx);
    
    // Check for existing review
    const existing = await ctx.db
      .query("reviews")
      .withIndex("by_userId", (q) => q.eq("userId", identity.subject))
      .filter((q) => q.eq(q.field("toolId"), args.toolId))
      .first();
    
    if (existing) {
      throw new Error("You have already reviewed this tool");
    }
    
    // Validate rating
    if (args.rating < 1 || args.rating > 5) {
      throw new Error("Rating must be between 1 and 5");
    }
    
    await ctx.db.insert("reviews", {
      toolId: args.toolId,
      userId: identity.subject,
      rating: args.rating,
      comment: args.comment,
      createdAt: Date.now(),
    });
  },
});

Build docs developers (and LLMs) love