Fix reviews and trust score calculation, improve metadata API docs
- Fix reviews/recent and getRecentReviews to return all reviews (not just those with parsed ratings) - Fix trust score calculation to parse ratings from content text (e.g., [5/5]) - Fix getReviewSummary and getReviewStats to parse ratings from content - Fix OpenAPI schema for contact field (should be array, not object) - Fix popular/trending mints endpoints to show results even without pageviews
This commit is contained in:
@@ -1794,9 +1794,22 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
|
||||
description: { type: 'string', nullable: true },
|
||||
description_long: { type: 'string', nullable: true },
|
||||
contact: {
|
||||
type: 'object',
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
additionalProperties: { type: 'string' }
|
||||
description: 'Contact methods for the mint operator',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
method: {
|
||||
type: 'string',
|
||||
description: 'Contact method (email, twitter, nostr, etc.)'
|
||||
},
|
||||
info: {
|
||||
type: 'string',
|
||||
description: 'Contact identifier (email address, handle, npub, etc.)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
motd: {
|
||||
type: 'string',
|
||||
|
||||
@@ -214,20 +214,34 @@ export function getReviews(mintId, options = {}) {
|
||||
|
||||
/**
|
||||
* Get review stats for a mint
|
||||
* Parses ratings from content text if rating column is null
|
||||
*/
|
||||
export function getReviewStats(mintId) {
|
||||
return queryOne(`
|
||||
SELECT
|
||||
COUNT(*) as total_reviews,
|
||||
AVG(rating) as average_rating,
|
||||
AVG(parsed_rating) as average_rating,
|
||||
COUNT(DISTINCT pubkey) as unique_reviewers,
|
||||
SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||
SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||
SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||
SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star
|
||||
FROM reviews
|
||||
WHERE mint_id = ? AND rating IS NOT NULL
|
||||
SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||
SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||
SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||
SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||
SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star
|
||||
FROM (
|
||||
SELECT
|
||||
pubkey,
|
||||
CASE
|
||||
WHEN rating IS NOT NULL THEN rating
|
||||
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||
ELSE NULL
|
||||
END as parsed_rating
|
||||
FROM reviews
|
||||
WHERE mint_id = ?
|
||||
) parsed
|
||||
`, [mintId]);
|
||||
}
|
||||
|
||||
@@ -381,24 +395,20 @@ export function countReviews(options = {}) {
|
||||
|
||||
/**
|
||||
* Get review summary for a mint (quick overview)
|
||||
* Parses ratings from content text if rating column is null
|
||||
*/
|
||||
export function getReviewSummary(mintId) {
|
||||
const stats = queryOne(`
|
||||
// First get total count of all reviews
|
||||
const totalStats = queryOne(`
|
||||
SELECT
|
||||
COUNT(*) as total_reviews,
|
||||
AVG(rating) as avg_rating,
|
||||
COUNT(DISTINCT pubkey) as unique_reviewers,
|
||||
SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||
SUM(CASE WHEN rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||
SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||
SUM(CASE WHEN rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as one_star,
|
||||
MAX(created_at) as last_review_at
|
||||
FROM reviews
|
||||
WHERE mint_id = ? AND rating IS NOT NULL
|
||||
WHERE mint_id = ?
|
||||
`, [mintId]);
|
||||
|
||||
if (!stats || stats.total_reviews === 0) {
|
||||
if (!totalStats || totalStats.total_reviews === 0) {
|
||||
return {
|
||||
total_reviews: 0,
|
||||
avg_rating: null,
|
||||
@@ -407,18 +417,43 @@ export function getReviewSummary(mintId) {
|
||||
};
|
||||
}
|
||||
|
||||
// Parse ratings from content if rating column is null
|
||||
const ratingStats = queryOne(`
|
||||
SELECT
|
||||
AVG(parsed_rating) as avg_rating,
|
||||
SUM(CASE WHEN parsed_rating = 5 THEN 1 ELSE 0 END) as five_star,
|
||||
SUM(CASE WHEN parsed_rating = 4 THEN 1 ELSE 0 END) as four_star,
|
||||
SUM(CASE WHEN parsed_rating = 3 THEN 1 ELSE 0 END) as three_star,
|
||||
SUM(CASE WHEN parsed_rating = 2 THEN 1 ELSE 0 END) as two_star,
|
||||
SUM(CASE WHEN parsed_rating = 1 THEN 1 ELSE 0 END) as one_star
|
||||
FROM (
|
||||
SELECT CASE
|
||||
WHEN rating IS NOT NULL THEN rating
|
||||
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||
ELSE NULL
|
||||
END as parsed_rating
|
||||
FROM reviews
|
||||
WHERE mint_id = ?
|
||||
) parsed
|
||||
WHERE parsed_rating IS NOT NULL
|
||||
`, [mintId]);
|
||||
|
||||
return {
|
||||
total_reviews: stats.total_reviews,
|
||||
avg_rating: stats.avg_rating ? Math.round(stats.avg_rating * 10) / 10 : null,
|
||||
unique_reviewers: stats.unique_reviewers,
|
||||
total_reviews: totalStats.total_reviews,
|
||||
avg_rating: ratingStats && ratingStats.avg_rating ? Math.round(ratingStats.avg_rating * 10) / 10 : null,
|
||||
unique_reviewers: totalStats.unique_reviewers,
|
||||
rating_distribution: {
|
||||
5: stats.five_star || 0,
|
||||
4: stats.four_star || 0,
|
||||
3: stats.three_star || 0,
|
||||
2: stats.two_star || 0,
|
||||
1: stats.one_star || 0
|
||||
5: (ratingStats && ratingStats.five_star) || 0,
|
||||
4: (ratingStats && ratingStats.four_star) || 0,
|
||||
3: (ratingStats && ratingStats.three_star) || 0,
|
||||
2: (ratingStats && ratingStats.two_star) || 0,
|
||||
1: (ratingStats && ratingStats.one_star) || 0
|
||||
},
|
||||
last_review_at: stats.last_review_at ? unixToISO(stats.last_review_at) : null
|
||||
last_review_at: totalStats.last_review_at ? unixToISO(totalStats.last_review_at) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -142,14 +142,23 @@ function calculateSpeedScore(mintId, component) {
|
||||
* Calculate reviews score (max 20 points)
|
||||
*/
|
||||
function calculateReviewsScore(mintId, component) {
|
||||
// Get review stats
|
||||
// Get review stats - include all reviews, not just those with parsed ratings
|
||||
// Parse rating from content if rating column is null (ratings often in "[5/5]" format in content)
|
||||
const stats = queryOne(`
|
||||
SELECT
|
||||
COUNT(*) as count,
|
||||
AVG(rating) as avg_rating,
|
||||
AVG(CASE
|
||||
WHEN rating IS NOT NULL THEN rating
|
||||
WHEN content LIKE '[5/5]%' OR content LIKE '%(5/5)%' THEN 5
|
||||
WHEN content LIKE '[4/5]%' OR content LIKE '%(4/5)%' THEN 4
|
||||
WHEN content LIKE '[3/5]%' OR content LIKE '%(3/5)%' THEN 3
|
||||
WHEN content LIKE '[2/5]%' OR content LIKE '%(2/5)%' THEN 2
|
||||
WHEN content LIKE '[1/5]%' OR content LIKE '%(1/5)%' THEN 1
|
||||
ELSE NULL
|
||||
END) as avg_rating,
|
||||
COUNT(DISTINCT pubkey) as unique_reviewers
|
||||
FROM reviews
|
||||
WHERE mint_id = ? AND rating IS NOT NULL
|
||||
WHERE mint_id = ?
|
||||
`, [mintId]);
|
||||
|
||||
if (!stats || stats.count === 0) {
|
||||
@@ -163,20 +172,32 @@ function calculateReviewsScore(mintId, component) {
|
||||
|
||||
component.details = {
|
||||
review_count: reviewCount,
|
||||
average_rating: Math.round(avgRating * 10) / 10,
|
||||
average_rating: avgRating ? Math.round(avgRating * 10) / 10 : null,
|
||||
unique_reviewers: uniqueReviewers
|
||||
};
|
||||
|
||||
// Base score from average rating (1-5 scale)
|
||||
const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7);
|
||||
// If we have a calculated average rating, use it
|
||||
if (avgRating) {
|
||||
// Base score from average rating (1-5 scale)
|
||||
const ratingScore = ((avgRating - 1) / 4) * (trustWeights.reviews * 0.7);
|
||||
|
||||
// Bonus for review quantity (max 30% of review score)
|
||||
const quantityBonus = Math.min(
|
||||
trustWeights.reviews * 0.3,
|
||||
(Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.3
|
||||
// Bonus for review quantity (max 30% of review score)
|
||||
const quantityBonus = Math.min(
|
||||
trustWeights.reviews * 0.3,
|
||||
(Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.3
|
||||
);
|
||||
|
||||
return ratingScore + quantityBonus;
|
||||
}
|
||||
|
||||
// Fallback: if we have reviews but no parseable ratings, give partial credit based on count
|
||||
// This rewards having reviews even without explicit ratings
|
||||
const quantityScore = Math.min(
|
||||
trustWeights.reviews * 0.5, // Max 50% of review score without ratings
|
||||
(Math.log10(reviewCount + 1) / 2) * trustWeights.reviews * 0.5
|
||||
);
|
||||
|
||||
return ratingScore + quantityBonus;
|
||||
return quantityScore;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user