Published Jun 23, 2025
Welcome back! In Part 1, we set up the foundation for Reviewly. Now, let’s dive into building the core features that make this platform powerful and user-friendly.
Before we start building features, let’s understand what we have:
src/
├── app/
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts # Authentication
│ │ ├── health/route.ts # Health checks
│ │ ├── reviews/route.ts # Review management
│ │ └── webhooks/route.ts # Webhook handling
│ ├── auth/
│ │ ├── signin/page.tsx # Sign-in/sign-up
│ │ └── error/page.tsx # Auth error handling
│ ├── dashboard/page.tsx # Protected dashboard
│ ├── reviews/page.tsx # Review management
│ ├── widget/page.tsx # Widget configuration
│ └── page.tsx # Landing page
├── lib/
│ └── prisma.ts # Database client
└── components/ # Reusable components
The dashboard is the heart of the application. I designed it with a clean, modern interface:
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/auth/signin");
}
return (
<div className="min-h-screen bg-[#18191A] text-white">
<div className="flex">
{/* Sidebar Navigation */}
<aside className="w-64 bg-[#23272F] min-h-screen p-6">
<div className="flex items-center gap-3 mb-8">
<div className="w-8 h-8 bg-gradient-to-r from-violet-500 to-purple-500 rounded-lg" />
<span className="font-bold">Reviewly</span>
</div>
<nav className="space-y-2">
<a
href="/dashboard"
className="flex items-center gap-3 p-3 rounded-lg bg-[#6C63FF]"
>
<DashboardIcon />
Dashboard
</a>
<a
href="/reviews"
className="flex items-center gap-3 p-3 rounded-lg hover:bg-[#2A2D36]"
>
<ReviewsIcon />
Reviews
</a>
<a
href="/widget"
className="flex items-center gap-3 p-3 rounded-lg hover:bg-[#2A2D36]"
>
<WidgetIcon />
Widgets
</a>
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 p-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">
Welcome back, {session.user?.name}
</h1>
<p className="text-gray-400">
Here's what's happening with your reviews
</p>
</div>
{/* Dashboard Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-[#23272F] p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-2">Total Reviews</h3>
<p className="text-3xl font-bold text-[#6C63FF]">1,247</p>
<p className="text-sm text-gray-400">+12% from last month</p>
</div>
{/* More stat cards... */}
</div>
{/* Recent Activity */}
<div className="bg-[#23272F] p-6 rounded-xl">
<h3 className="text-lg font-semibold mb-4">Recent Reviews</h3>
{/* Review list component */}
</div>
</main>
</div>
</div>
);
}
The review management system is crucial for businesses to moderate and organize their customer feedback:
// API Route: /api/reviews
export async function GET(request: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const status = searchParams.get("status");
const clientId = searchParams.get("clientId");
const where: any = {};
if (status) where.status = status;
if (clientId) where.clientId = clientId;
const reviews = await prisma.review.findMany({
where,
include: {
client: true,
media: true,
},
orderBy: { createdAt: "desc" },
skip: (page - 1) * limit,
take: limit,
});
const total = await prisma.review.count({ where });
return NextResponse.json({
reviews,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
});
}
export async function POST(request: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { rating, content, clientId, mediaUrls } = body;
const review = await prisma.review.create({
data: {
rating,
content,
clientId,
userId: session.user.id,
mediaUrls,
status: "pending", // Default status
},
include: {
client: true,
media: true,
},
});
return NextResponse.json(review);
}
Implementing a robust status system for reviews:
export async function PATCH(request: Request) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const reviewId = searchParams.get("id");
const body = await request.json();
const { status, moderationNote } = body;
const review = await prisma.review.update({
where: { id: reviewId },
data: {
status,
moderationNote,
},
});
// Log the action
await prisma.auditLog.create({
data: {
action: `REVIEW_${status.toUpperCase()}`,
userId: session.user.id,
reviewId,
details: { moderationNote },
},
});
return NextResponse.json(review);
}
The widget system allows businesses to embed review collection and display widgets on their websites:
// Widget Configuration API
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const clientId = searchParams.get("clientId");
const type = searchParams.get("type"); // "collect" or "display"
if (!clientId) {
return NextResponse.json({ error: "Client ID required" }, { status: 400 });
}
const client = await prisma.client.findUnique({
where: { id: clientId },
include: {
reviews: {
where: { status: "approved" },
orderBy: { createdAt: "desc" },
take: 10,
},
},
});
if (!client) {
return NextResponse.json({ error: "Client not found" }, { status: 404 });
}
return NextResponse.json({
client,
widgetConfig: {
theme: client.brandingConfig.theme || "light",
position: client.brandingConfig.position || "bottom-right",
showRating: client.brandingConfig.showRating !== false,
},
});
}
Implementing webhooks for real-time notifications:
// Webhook endpoint for external integrations
export async function POST(request: Request) {
const body = await request.json();
const { event, data, clientId } = body;
// Verify webhook signature
const signature = request.headers.get("x-webhook-signature");
if (!verifyWebhookSignature(signature, body)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
switch (event) {
case "review.created":
await handleReviewCreated(data);
break;
case "review.updated":
await handleReviewUpdated(data);
break;
case "review.approved":
await handleReviewApproved(data);
break;
default:
return NextResponse.json({ error: "Unknown event" }, { status: 400 });
}
return NextResponse.json({ success: true });
}
async function handleReviewCreated(data: any) {
// Send notifications
// Update analytics
// Trigger email campaigns
console.log("Review created:", data);
}
Implementing powerful search capabilities:
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q");
const rating = searchParams.get("rating");
const dateFrom = searchParams.get("dateFrom");
const dateTo = searchParams.get("dateTo");
const where: any = {};
if (query) {
where.OR = [
{ content: { contains: query, mode: "insensitive" } },
{ client: { name: { contains: query, mode: "insensitive" } } },
];
}
if (rating) {
where.rating = parseInt(rating);
}
if (dateFrom || dateTo) {
where.createdAt = {};
if (dateFrom) where.createdAt.gte = new Date(dateFrom);
if (dateTo) where.createdAt.lte = new Date(dateTo);
}
const reviews = await prisma.review.findMany({
where,
include: {
client: true,
media: true,
},
orderBy: { createdAt: "desc" },
});
return NextResponse.json(reviews);
}
Building analytics features for insights:
// Analytics API
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const clientId = searchParams.get("clientId");
const period = searchParams.get("period") || "30d";
const reviews = await prisma.review.findMany({
where: { clientId },
select: {
rating: true,
createdAt: true,
status: true,
},
});
const analytics = {
totalReviews: reviews.length,
averageRating:
reviews.reduce((acc, r) => acc + r.rating, 0) / reviews.length,
ratingDistribution: {
1: reviews.filter((r) => r.rating === 1).length,
2: reviews.filter((r) => r.rating === 2).length,
3: reviews.filter((r) => r.rating === 3).length,
4: reviews.filter((r) => r.rating === 4).length,
5: reviews.filter((r) => r.rating === 5).length,
},
statusDistribution: {
pending: reviews.filter((r) => r.status === "pending").length,
approved: reviews.filter((r) => r.status === "approved").length,
rejected: reviews.filter((r) => r.status === "rejected").length,
},
// Time-based analytics
reviewsOverTime: getReviewsOverTime(reviews, period),
};
return NextResponse.json(analytics);
}
Adding proper indexes for performance:
-- Add indexes for common queries
CREATE INDEX idx_reviews_client_status ON reviews(client_id, status);
CREATE INDEX idx_reviews_created_at ON reviews(created_at);
CREATE INDEX idx_reviews_rating ON reviews(rating);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
Implementing caching for frequently accessed data:
import { unstable_cache } from "next/cache";
const getCachedClientData = unstable_cache(
async (clientId: string) => {
return await prisma.client.findUnique({
where: { id: clientId },
include: { reviews: true },
});
},
["client-data"],
{ revalidate: 300 } // 5 minutes
);
Implementing efficient data loading:
// Infinite scroll for reviews
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const cursor = searchParams.get("cursor");
const limit = parseInt(searchParams.get("limit") || "20");
const reviews = await prisma.review.findMany({
take: limit + 1, // Take one extra to check if there are more
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: "desc" },
include: { client: true },
});
const hasNextPage = reviews.length > limit;
const nextCursor = hasNextPage ? reviews[limit - 1].id : null;
return NextResponse.json({
reviews: reviews.slice(0, limit),
nextCursor,
hasNextPage,
});
}
Securing widget endpoints:
function verifyApiKey(apiKey: string, clientId: string) {
// Verify API key belongs to client
// Check if key is active and not expired
return prisma.apiKey.findFirst({
where: {
key: apiKey,
clientId,
isActive: true,
expiresAt: { gt: new Date() },
},
});
}
Implementing rate limiting for API endpoints:
import { rateLimit } from "@/lib/rate-limit";
const limiter = rateLimit({
interval: 60 * 1000, // 1 minute
uniqueTokenPerInterval: 500,
});
export async function POST(request: Request) {
try {
await limiter.check(request, 10, "CACHE_TOKEN"); // 10 requests per minute
} catch {
return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 });
}
// Continue with request processing
}
We’ve now built:
✅ Dashboard: Complete with navigation and stats
✅ Review Management: CRUD operations with status management
✅ Widget System: Embeddable review collection and display
✅ API Endpoints: RESTful APIs for all features
✅ Search & Filtering: Advanced search capabilities
✅ Analytics: Basic reporting and insights
✅ Security: API key auth and rate limiting
✅ Performance: Caching and pagination
The platform is now feature-complete for MVP. Future enhancements could include:
The platform is ready for production deployment on Vercel with:
Reviewly is now a fully functional review management platform! The combination of modern tech stack, robust architecture, and user-friendly features makes it ready to help businesses transform their customer feedback into growth.
In the next part, we’ll cover deployment, monitoring, and scaling strategies for production use.