355 lines
14 KiB
JavaScript
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>
|
|
);
|
|
}
|