Skip to main content

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

CollectionOperationsPurpose
review-widgetsCREATE, READ, UPDATE, DELETEWidget configurations
reviews.*READSource 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 identifier
  • review_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 settings
  • domain (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 number
  • limit (number): Items per page
  • ratings (array): Star ratings to filter
  • review_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 │
└─────────────────────────┘
┌─────────────────────────┐
│ ← 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_id from 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

  1. Domain Whitelisting: Only configured domain can fetch widget data
  2. Origin Header: Must match widget.domain exactly
  3. Hidden Reviews: Automatically excluded from public display
  4. Rate Limiting: Prevent abuse of public endpoint

Performance Optimization

  1. CDN Caching: Cache widget JS files
  2. Review Caching: Cache review data (5-15 min TTL)
  3. Pagination: Limit reviews per request
  4. 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


Version: 1.0
Last Updated: December 2024
Status: ✅ Production Ready

💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM