Widgets Controller
Source: internal/api/v1/reviews/controllers/widgets.js
Service: widgets service
Module: Reviews
Overview
The Widgets controller manages embeddable review display widgets for customer websites. It enables creating customizable review showcases that can be embedded on any website, with theme customization, rating filters, and domain verification for security.
Key Capabilities
- Create Review Widgets with custom themes and filters
- List All Widgets for an account
- Update Widget Settings (theme, filters, domain)
- Delete Widgets when no longer needed
- Preview Widget Data before publishing
- Public Widget API for customer websites (CORS enabled)
- Domain Verification for security
MongoDB Collections
| Collection | Operations | Purpose |
|---|---|---|
review-widgets | CREATE, READ, UPDATE, DELETE | Widget configurations |
reviews.* | READ | Source review data for display |
Service Methods
1. list()
Retrieve all widgets for an account.
Endpoint: GET /reviews/widgets
Controller Logic:
exports.list = catchAsync(async (req, res) => {
const account_id = req.auth.account_id;
const data = await widgets.get({ account_id });
res.json({
success: true,
data,
});
});
Response:
{
"success": true,
"data": [
{
"_id": "widget_abc123",
"account_id": "acc_123",
"name": "Homepage Reviews",
"review_platform": ["google", "facebook"],
"ratings": [5, 4],
"theme": {
"layout": "grid",
"color_primary": "#1a73e8",
"color_background": "#ffffff",
"show_platform_icons": true,
"max_items": 6
},
"domain": "https://www.acmecorp.com",
"embed_code": "<script src='https://api.dashclicks.com/widgets/widget_abc123.js'></script>",
"created_at": "2024-12-01T10:00:00Z"
}
]
}
2. create()
Create a new review widget.
Endpoint: POST /reviews/widgets
Controller Logic:
exports.create = catchAsync(async (req, res) => {
const {
account_id,
user: { id: user_id },
} = req.auth;
const { name, review_platform, ratings, theme, domain } = req.body;
const widget = await widgets.create({
account_id,
user_id,
name,
review_platform,
ratings,
theme,
domain,
});
res.json({
success: true,
data: widget,
});
});
Request:
{
"name": "Homepage Reviews",
"review_platform": ["google", "facebook"],
"ratings": [5, 4],
"theme": {
"layout": "grid",
"color_primary": "#1a73e8",
"color_background": "#ffffff",
"show_platform_icons": true,
"show_date": true,
"show_rating": true,
"max_items": 6,
"columns": 3
},
"domain": "https://www.acmecorp.com"
}
Request Body:
name(string): Widget identifierreview_platform(array): Platforms to include (google, facebook, yelp, etc.)ratings(array): Star ratings to show (e.g., [5,4] for 4-5 star only)theme(object): Visual customization settingsdomain(string): Website domain for CORS and verification
Theme Options:
{
layout: 'grid' | 'list' | 'carousel' | 'masonry';
color_primary: string; // Hex color for accents
color_background: string; // Hex color for widget background
color_text: string; // Hex color for text
show_platform_icons: boolean; // Display platform logos
show_date: boolean; // Show review date
show_rating: boolean; // Show star ratings
show_author: boolean; // Show reviewer name
max_items: number; // Max reviews to display
columns?: number; // Grid layout columns (1-4)
border_radius?: string; // CSS border radius
font_family?: string; // Custom font
}
Response:
{
"success": true,
"data": {
"_id": "widget_abc123",
"account_id": "acc_123",
"user_id": "user_456",
"name": "Homepage Reviews",
"review_platform": ["google", "facebook"],
"ratings": [5, 4],
"theme": { ... },
"domain": "https://www.acmecorp.com",
"embed_code": "<script src='https://api.dashclicks.com/widgets/widget_abc123.js'></script>",
"created_at": "2024-12-08T10:00:00Z"
}
}
3. update()
Update existing widget configuration.
Endpoint: PUT /reviews/widgets/:id
Controller Logic:
exports.update = catchAsync(async (req, res) => {
const account_id = req.auth.account_id;
const { id } = req.params;
const { name, review_platform, ratings, theme, domain } = req.body;
await widgets.update({
id,
account_id,
name,
review_platform,
ratings,
theme,
domain,
});
res.json({
success: true,
});
});
Request:
{
"name": "Updated Widget Name",
"ratings": [5, 4, 3],
"theme": {
"layout": "carousel",
"color_primary": "#ff6b6b"
}
}
Response:
{
"success": true
}
4. delete()
Delete a widget.
Endpoint: DELETE /reviews/widgets/:id
Controller Logic:
exports.delete = catchAsync(async (req, res) => {
const account_id = req.auth.account_id;
const { id } = req.params;
await widgets.delete({
id,
account_id,
});
res.json({
success: true,
});
});
Response:
{
"success": true
}
5. getPreviewData()
Get preview data for widget configuration (authenticated).
Endpoint: GET /reviews/widgets/preview
Request:
GET /api/v1/v1/reviews/widgets/preview?page=1&limit=6&ratings[]=5&ratings[]=4&review_platform=google
Query Parameters:
page(number): Page numberlimit(number): Items per pageratings(array): Star ratings to filterreview_platform(string): Platform filter
Response:
{
"success": true,
"data": [
{
"_id": "review_abc",
"platform": "google",
"rating": 5,
"author_name": "John Doe",
"text": "Great service!",
"created_at": "2024-12-01T10:00:00Z",
"profile_photo_url": "https://...",
"hidden": false
}
],
"pagination": {
"page": 1,
"limit": 6,
"total": 45
},
"config": {
"ratings": [5, 4],
"review_platform": "google"
}
}
6. getWidgetReviews()
PUBLIC ENDPOINT - Get widget reviews for embedding (no authentication).
Endpoint: GET /reviews/widgets/:id/reviews
Controller Logic:
exports.getWidgetReviews = catchAsync(async (req, res) => {
const widgetId = req.params.id;
const { page, limit } = req.query;
const origin = getOriginFromRequest(req);
const widgetData = await ReviewWidget.findOne({
_id: widgetId,
}).lean();
if (!widgetData) {
throw notFound('Widget not found.');
}
if (widgetData.domain !== origin) {
// When this endpoint is called from a customer's website.
throw notAuthorized('The origin does not match the specified domain.');
}
const { name, theme, domain, account_id, ratings, review_platform } = widgetData;
const { data, pagination } = await widgets.getReviews({
account_id,
ratings,
review_platform,
page: parseInt(page),
limit: parseInt(limit),
});
res.json({
success: true,
data,
pagination,
config: { name, theme, ratings, review_platform, domain },
});
});
Request (from customer website):
GET https://api.dashclicks.com/api/v1/v1/reviews/widgets/widget_abc123/reviews?page=1&limit=6
Origin: https://www.acmecorp.com
Response:
{
"success": true,
"data": [
{
"_id": "review_abc",
"platform": "google",
"rating": 5,
"author_name": "John Doe",
"text": "Great service!",
"created_at": "2024-12-01T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 6,
"total": 45
},
"config": {
"name": "Homepage Reviews",
"theme": {
"layout": "grid",
"color_primary": "#1a73e8"
},
"ratings": [5, 4],
"review_platform": ["google"],
"domain": "https://www.acmecorp.com"
}
}
Security - Domain Verification:
graph TD
A[Customer Website Makes Request] --> B[Extract Origin Header]
B --> C[Fetch Widget Config from DB]
C --> D{Widget Exists?}
D -->|No| E[Return 404 Not Found]
D -->|Yes| F{Origin Matches Domain?}
F -->|No| G[Return 401 Unauthorized]
F -->|Yes| H[Fetch Reviews]
H --> I[Filter by Ratings]
I --> J[Filter by Platform]
J --> K[Exclude Hidden Reviews]
K --> L[Return Reviews + Config]
CORS Headers:
// Required for public widget endpoint
res.setHeader('Access-Control-Allow-Origin', widgetData.domain);
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
Widget Embed Code
JavaScript Snippet
<!-- Embed on customer website -->
<div id="dashclicks-reviews-widget"></div>
<script>
(function () {
const widgetId = 'widget_abc123';
const container = document.getElementById('dashclicks-reviews-widget');
fetch(`https://api.dashclicks.com/api/v1/v1/reviews/widgets/${widgetId}/reviews?page=1&limit=6`)
.then(res => res.json())
.then(data => {
renderReviews(data.data, data.config);
});
function renderReviews(reviews, config) {
// Render based on theme configuration
container.innerHTML = reviews
.map(
review => `
<div class="review-card">
<div class="rating">${'★'.repeat(review.rating)}</div>
<p>${review.text}</p>
<div class="author">${review.author_name}</div>
</div>
`,
)
.join('');
}
})();
</script>
React Component Example
import React, { useEffect, useState } from 'react';
function ReviewsWidget({ widgetId }) {
const [reviews, setReviews] = useState([]);
const [config, setConfig] = useState({});
useEffect(() => {
fetch(`https://api.dashclicks.com/api/v1/v1/reviews/widgets/${widgetId}/reviews?page=1&limit=6`)
.then(res => res.json())
.then(data => {
setReviews(data.data);
setConfig(data.config);
});
}, [widgetId]);
return (
<div
className="reviews-widget"
style={{
backgroundColor: config.theme?.color_background,
}}
>
{reviews.map(review => (
<div key={review._id} className="review-card">
<div className="rating">{'★'.repeat(review.rating)}</div>
<p>{review.text}</p>
<div className="author">{review.author_name}</div>
</div>
))}
</div>
);
}
Edge Cases & Error Handling
1. Widget Not Found:
if (!widgetData) {
throw notFound('Widget not found.');
}
2. Domain Mismatch (Security):
const origin = getOriginFromRequest(req);
if (widgetData.domain !== origin) {
throw notAuthorized('The origin does not match the specified domain.');
}
3. No Reviews Available:
// Returns empty array with valid structure
{
success: true,
data: [],
pagination: { page: 1, limit: 6, total: 0 }
}
4. Invalid Rating Filter:
// Only allows ratings 1-5
ratings = ratings.filter(r => r >= 1 && r <= 5);
5. Hidden Reviews:
// Automatically excluded from widget display
const reviews = await Review.find({
account_id,
hidden: { $ne: true },
rating: { $in: ratings },
});
Widget Layouts
Grid Layout
┌───────┬───────┬───────┐
│Review │Review │Review │
│ #1 │ #2 │ #3 │
├───────┼───────┼───────┤
│Review │Review │Review │
│ #4 │ #5 │ #6 │
└───────┴───────┴───────┘
List Layout
┌─────────────────────────┐
│ Review #1 │
├─────────────────────────┤
│ Review #2 │
├─────────────────────────┤
│ Review #3 │
└─────────────────────────┘
Carousel Layout
┌─────────────────────────┐
│ ← Review #2 → │
└─────────────────────────┘
● ○ ○ ○ ○ ○
Masonry Layout
┌─────┬─────┬─────┐
│ #1 │ #3 │ #5 │
│ ├─────┤ │
├─────┤ #4 ├─────┤
│ #2 │ │ #6 │
└─────┴─────┴─────┘
Authorization
Authenticated Endpoints:
- ✅ list, create, update, delete - Require JWT
- ✅ getPreviewData - Requires JWT
- ✅ Account-scoped:
account_idfrom token
Public Endpoint:
- ⚠️ getWidgetReviews - NO JWT required
- ✅ Domain verification required
- ✅ CORS enabled for configured domain
Integration Points
Dependencies:
- Reviews Controller: Source review data
- Config Controller: Platform connections required
Used By:
- Customer Websites: Embed on any page
- Landing Pages: Display social proof
- Marketing Materials: Show testimonials
Important Notes
Security Considerations
- Domain Whitelisting: Only configured domain can fetch widget data
- Origin Header: Must match widget.domain exactly
- Hidden Reviews: Automatically excluded from public display
- Rate Limiting: Prevent abuse of public endpoint
Performance Optimization
- CDN Caching: Cache widget JS files
- Review Caching: Cache review data (5-15 min TTL)
- Pagination: Limit reviews per request
- Image Optimization: Compress author profile photos
Best Practices
Widget Configuration:
- Ratings Filter: Show only 4-5 star reviews for best impression
- Platform Mix: Include multiple platforms for authenticity
- Max Items: 6-12 reviews optimal for page load
- Auto-Refresh: Update every 24 hours for fresh content
Theme Customization:
- Match brand colors
- Use readable fonts
- Ensure mobile responsiveness
- Test on various screen sizes
Related Documentation
- Reviews Controller (link removed - file does not exist) - Review data source
- Config Controller - Platform setup
- Auto Response Rules - Automated responses
Version: 1.0
Last Updated: December 2024
Status: ✅ Production Ready