sms-extension-1777874553/client/src/components/BusinessReviewModal.jsx

355 lines
14 KiB
JavaScript

import { useEffect, useMemo } from 'react';
import CdnGallery from './CdnGallery';
import {
getBusinessDomain,
getBusinessImage,
getBusinessName,
getBusinessTagline,
} from '../utils/businessProfile';
function normalizeText(value) {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeUniqueStrings(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
return value
.map((entry) => normalizeText(entry))
.filter((entry) => {
if (!entry || seen.has(entry)) return false;
seen.add(entry);
return true;
});
}
function normalizeColorEntries(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
return value
.map((entry, index) => {
if (typeof entry === 'string') {
const hex = normalizeText(entry);
return hex ? { name: '', hex, key: `${hex}:${index}` } : null;
}
if (!entry || typeof entry !== 'object') return null;
const hex = normalizeText(entry.hex || entry.value || entry.color);
if (!hex) return null;
const name = normalizeText(entry.name || entry.label || entry.role);
return { name, hex, key: `${name}:${hex}` };
})
.filter((entry) => {
if (!entry || seen.has(entry.key)) return false;
seen.add(entry.key);
return true;
});
}
function extractCdnUrls(business) {
const artifactUrls = business?.scrapeArtifacts?.cdnUrls;
if (Array.isArray(artifactUrls) && artifactUrls.length > 0) {
return normalizeUniqueStrings(artifactUrls);
}
return normalizeUniqueStrings(business?.relevantImagePaths);
}
function normalizeScrapeLinks(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
return value
.map((entry) => {
if (typeof entry === 'string') {
const href = normalizeText(entry);
return href ? { href, label: href } : null;
}
if (!entry || typeof entry !== 'object') return null;
const href = normalizeText(entry.href || entry.url || entry.link);
if (!href) return null;
const label = normalizeText(entry.text || entry.title || entry.label || href);
return { href, label };
})
.filter((entry) => {
if (!entry || seen.has(entry.href)) return false;
seen.add(entry.href);
return true;
});
}
function formatPrettyJson(value) {
if (value == null) return '';
if (typeof value === 'string') {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch {
return value;
}
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function extractColors(business) {
const labeledColors = normalizeColorEntries(business?.scrapeArtifacts?.json?.branding?.labeledColors);
if (labeledColors.length > 0) return labeledColors;
const directColors = normalizeColorEntries(business?.colors);
if (directColors.length > 0) return directColors;
const brandingColors = business?.scrapeArtifacts?.json?.branding?.colors;
return normalizeColorEntries(brandingColors);
}
function extractAboutText(business) {
const directSummary = normalizeText(business?.aboutSummary);
if (directSummary) return directSummary;
const scrapeJson = business?.scrapeArtifacts?.json;
const aboutExcerpt = normalizeText(scrapeJson?.aboutPage?.excerpt);
if (aboutExcerpt) return aboutExcerpt;
const representativeAbout = Array.isArray(scrapeJson?.representativeTextBlocks)
? scrapeJson.representativeTextBlocks.find((block) => normalizeText(block?.pageType) === 'about')
: null;
const representativeAboutText = normalizeText(representativeAbout?.text);
if (representativeAboutText) return representativeAboutText;
const homepageExcerpt = normalizeText(scrapeJson?.homepage?.excerpt);
if (homepageExcerpt) return homepageExcerpt;
return normalizeText(scrapeJson?.summaryText);
}
export default function BusinessReviewModal({ business, onClose }) {
const name = getBusinessName(business);
const domain = getBusinessDomain(business);
const tagline = getBusinessTagline(business);
const image = getBusinessImage(business);
const tone = normalizeText(business?.tone);
const taglines = normalizeUniqueStrings(business?.taglines);
const colors = extractColors(business);
const aboutText = extractAboutText(business);
const cdnUrls = extractCdnUrls(business);
const links = normalizeScrapeLinks(business?.scrapeArtifacts?.links);
const prettyJson = useMemo(() => formatPrettyJson(business?.scrapeArtifacts?.json), [business]);
useEffect(() => {
const previousBodyOverflow = document.body.style.overflow;
const previousBodyOverscroll = document.body.style.overscrollBehavior;
const previousHtmlOverflow = document.documentElement.style.overflow;
const previousHtmlOverscroll = document.documentElement.style.overscrollBehavior;
document.body.style.overflow = 'hidden';
document.body.style.overscrollBehavior = 'none';
document.documentElement.style.overflow = 'hidden';
document.documentElement.style.overscrollBehavior = 'none';
return () => {
document.body.style.overflow = previousBodyOverflow;
document.body.style.overscrollBehavior = previousBodyOverscroll;
document.documentElement.style.overflow = previousHtmlOverflow;
document.documentElement.style.overscrollBehavior = previousHtmlOverscroll;
};
}, []);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/55 p-4 backdrop-blur-sm">
<div
className="flex h-full w-full flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl"
style={{ width: '920px', maxWidth: 'calc(100vw - 2rem)', height: 'min(78vh, 760px)' }}
>
<div className="shrink-0 flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-5">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Business review</p>
<h2 className="mt-2 text-2xl font-semibold text-gray-900">{name}</h2>
<p className="mt-1 text-sm text-gray-500">
{domain
? `Review the captured storefront context for ${domain}.`
: 'Review the captured storefront context before moving on.'}
</p>
</div>
<button
onClick={onClose}
className="rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-50 hover:text-gray-900"
>
Close
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
<div className="space-y-5 pb-2">
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4">
<div className="flex items-start gap-4">
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-gray-200 bg-white">
{image ? (
<img src={image} alt={name} className="h-full w-full object-cover" />
) : (
<span className="text-xl font-bold text-primary-blue">{name?.[0]?.toUpperCase() || 'B'}</span>
)}
</div>
<div className="min-w-0">
<p className="text-lg font-semibold tracking-tight text-gray-900">{name}</p>
{domain && <p className="mt-1 break-all text-sm font-medium text-gray-500">{domain}</p>}
{tagline && <p className="mt-2 text-sm leading-relaxed text-gray-700">{tagline}</p>}
<div className="mt-4 flex flex-wrap gap-2">
{tone && (
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-semibold capitalize text-gray-600">
Tone: {tone}
</span>
)}
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-semibold text-gray-600">
{cdnUrls.length} image{cdnUrls.length === 1 ? '' : 's'}
</span>
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-semibold text-gray-600">
{links.length} link{links.length === 1 ? '' : 's'}
</span>
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-semibold text-gray-600">
{colors.length} color{colors.length === 1 ? '' : 's'}
</span>
</div>
</div>
</div>
</div>
{aboutText && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">About Company</p>
<p className="mt-1 text-sm text-gray-500">A concise summary of what the brand is about, what it sells, and its overall vibe.</p>
</div>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white">
<div className="max-h-44 overflow-y-auto px-4 py-4">
<p className="whitespace-pre-line text-sm leading-relaxed text-gray-700">{aboutText}</p>
</div>
</div>
</section>
)}
{(taglines.length > 0 || colors.length > 0) && (
<section className="grid gap-5 md:grid-cols-2">
{taglines.length > 0 && (
<div className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Taglines</p>
<p className="mt-1 text-sm text-gray-500">Short brand lines captured during onboarding.</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white px-4 py-4">
<div className="space-y-2">
{taglines.map((entry, index) => (
<p key={`${entry}-${index}`} className="text-sm text-gray-700">"{entry}"</p>
))}
</div>
</div>
</div>
)}
{colors.length > 0 && (
<div className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Color Codes</p>
<p className="mt-1 text-sm text-gray-500">Detected brand colors used across the storefront.</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white px-4 py-4">
<div className="flex flex-wrap gap-3">
{colors.map((color) => (
<div key={color.key} className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2">
<span
className="h-6 w-6 rounded-md border border-gray-200"
style={{ backgroundColor: color.hex }}
title={color.name ? `${color.name}: ${color.hex}` : color.hex}
/>
<div className="flex flex-col">
{color.name && (
<span className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
{color.name}
</span>
)}
<span className="font-mono text-xs text-gray-600">{color.hex}</span>
</div>
</div>
))}
</div>
</div>
</div>
)}
</section>
)}
{cdnUrls.length > 0 && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Images</p>
<p className="mt-1 text-sm text-gray-500">Captured storefront images are available below.</p>
</div>
<CdnGallery urls={cdnUrls} compact showLabels={false} />
</section>
)}
{prettyJson && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Captured Data</p>
<p className="mt-1 text-sm text-gray-500">Raw storefront data captured during onboarding.</p>
</div>
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-950">
<pre className="max-h-56 overflow-auto px-4 py-4 text-xs leading-relaxed text-gray-100">
{prettyJson}
</pre>
</div>
</section>
)}
{links.length > 0 && (
<section className="space-y-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Links</p>
<p className="mt-1 text-sm text-gray-500">Every discovered storefront link is available below.</p>
</div>
<div className="rounded-xl border border-gray-200">
<div className="max-h-72 overflow-y-auto divide-y divide-gray-100">
{links.map((link, index) => (
<a
key={`${link.href}-${index}`}
href={link.href}
target="_blank"
rel="noreferrer"
className="block px-4 py-3 transition hover:bg-gray-50"
>
<p className="break-all text-sm font-medium text-gray-800">{link.label}</p>
<p className="mt-1 break-all text-xs text-primary-blue">{link.href}</p>
</a>
))}
</div>
</div>
</section>
)}
</div>
</div>
<div className="shrink-0 border-t border-gray-200 px-6 py-4">
<button
onClick={onClose}
className="w-full rounded-lg bg-primary-blue py-2 text-sm font-medium text-white transition hover:bg-primary-dark"
>
Continue
</button>
</div>
</div>
</div>
);
}