feat: add themes scatter chart, HowToRead section, and fix 9 UI annotation tasks

- Add ThemesTab with recharts ScatterChart (domain-colored, labeled dots)
- Add HowToRead standalone section (score spectrum, valence markers, domains, intensity, tips)
- Add demo-report-data.ts with anonymized synthesis data
- Fix report fan: use overflow-x-clip instead of overflow-hidden to preserve shadows
- Fix HowItWorks: move connector lines behind cards, fix SVG gauge arc alignment
- Fix ScoreTab: increase gauge spacing, add score band labels (0-100)
- Fix testimonials: use overflow-clip to allow page scroll through section
- Fix banner: increase logo size
- Add EN/ES i18n keys for themes tab and HowToRead section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-22 20:27:13 +00:00
parent f73717922c
commit 61428c889e
10 changed files with 675 additions and 104 deletions

View File

@@ -2,18 +2,18 @@ import { Banner } from "~/modules/marketing/home/banner";
import { Faq } from "~/modules/marketing/home/faq";
import { Hero } from "~/modules/marketing/home/hero";
import { HowItWorks } from "~/modules/marketing/home/how-it-works";
import { HowToRead } from "~/modules/marketing/home/how-to-read";
import { ReportPreview } from "~/modules/marketing/home/report-preview";
import { ReviewEvidence } from "~/modules/marketing/home/review-evidence";
import { SocialProof } from "~/modules/marketing/home/social-proof";
import { Testimonials } from "~/modules/marketing/home/testimonials";
const HomePage = () => {
return (
<>
<Hero />
<SocialProof />
<HowItWorks />
<ReportPreview />
<HowToRead />
<ReviewEvidence />
<Testimonials />
<Faq />

View File

@@ -2,39 +2,10 @@ import { getTranslation } from "@turbostarter/i18n/server";
import { cn } from "@turbostarter/ui";
import { buttonVariants } from "@turbostarter/ui-web/button";
import { WhyRatingLogo } from "~/modules/common/whyrating-logo";
import { CtaButton } from "~/modules/marketing/layout/cta-button";
import { Section } from "~/modules/marketing/layout/section";
const ScoreGauge = () => (
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" aria-hidden="true">
{/* Background arc */}
<path
d="M8 36 A16 16 0 1 1 40 36"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
opacity="0.3"
/>
{/* Filled arc (~83%) */}
<path
d="M8 36 A16 16 0 1 1 38.5 30"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
/>
<text
x="24"
y="30"
textAnchor="middle"
fill="currentColor"
fontSize="13"
fontWeight="600"
>
83
</text>
</svg>
);
export const Banner = async () => {
const { t } = await getTranslation({ ns: "marketing" });
return (
@@ -42,7 +13,12 @@ export const Banner = async () => {
id="banner"
className="bg-primary text-primary-foreground !max-w-full gap-4 sm:gap-6 md:gap-8 lg:gap-10"
>
<ScoreGauge />
<WhyRatingLogo
colorScheme="dark"
className="gap-3"
iconClassName="h-16 w-16 sm:h-20 sm:w-20"
wordmarkClassName="text-3xl sm:text-5xl"
/>
<h3 className="text-3xl leading-[0.95] font-semibold tracking-tighter text-balance md:text-4xl lg:text-5xl">
{t("cta.question")}
</h3>

View File

@@ -0,0 +1,102 @@
// ---------------------------------------------------------------------------
// Anonymized demo data for the marketing landing page.
// Source: real compramostucoche.es report, anonymized as "Bistro El Sol".
// Stripped: top_quotes, evidence, detail (contain real customer text).
// ---------------------------------------------------------------------------
export interface DemoTheme {
label: string;
domain: string;
count: number;
weight: number;
primitive: string;
valence: { positive: number; negative: number; neutral: number; mixed: number };
}
export interface DemoAction {
action: string;
owner: string;
effort: "low" | "medium" | "high";
impact: "low" | "medium" | "high";
source: string;
timeline: string;
success_metric: string;
}
// Top 15 themes (dropped tail with count ≤ 2)
export const DEMO_THEMES: DemoTheme[] = [
{ label: "Manner/Attitude", domain: "P", count: 471, weight: 1.0, primitive: "MANNER", valence: { positive: 462, negative: 9, neutral: 0, mixed: 0 } },
{ label: "Speed/Wait", domain: "J", count: 129, weight: 1.0, primitive: "SPEED", valence: { positive: 129, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Competence", domain: "P", count: 47, weight: 1.0, primitive: "COMPETENCE", valence: { positive: 47, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Recommend", domain: "meta", count: 45, weight: 1.0, primitive: "RECOMMEND", valence: { positive: 45, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Price Fairness", domain: "V", count: 41, weight: 1.0, primitive: "PRICE_FAIRNESS", valence: { positive: 38, negative: 3, neutral: 0, mixed: 0 } },
{ label: "Attentiveness", domain: "P", count: 27, weight: 1.0, primitive: "ATTENTIVENESS", valence: { positive: 26, negative: 1, neutral: 0, mixed: 0 } },
{ label: "Value for Money", domain: "V", count: 24, weight: 1.0, primitive: "VALUE_FOR_MONEY", valence: { positive: 19, negative: 5, neutral: 0, mixed: 0 } },
{ label: "Effectiveness", domain: "O", count: 23, weight: 1.0, primitive: "EFFECTIVENESS", valence: { positive: 22, negative: 1, neutral: 0, mixed: 0 } },
{ label: "Friction", domain: "J", count: 16, weight: 1.0, primitive: "FRICTION", valence: { positive: 13, negative: 3, neutral: 0, mixed: 0 } },
{ label: "Reliability", domain: "J", count: 15, weight: 1.0, primitive: "RELIABILITY", valence: { positive: 14, negative: 1, neutral: 0, mixed: 0 } },
{ label: "Communication", domain: "P", count: 14, weight: 1.0, primitive: "COMMUNICATION", valence: { positive: 14, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Accuracy", domain: "O", count: 11, weight: 1.0, primitive: "ACCURACY", valence: { positive: 11, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Price Transparency", domain: "V", count: 8, weight: 1.0, primitive: "PRICE_TRANSPARENCY", valence: { positive: 8, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Honesty", domain: "meta", count: 6, weight: 1.0, primitive: "HONESTY", valence: { positive: 6, negative: 0, neutral: 0, mixed: 0 } },
{ label: "Taste/Flavor", domain: "O", count: 5, weight: 1.0, primitive: "TASTE", valence: { positive: 4, negative: 1, neutral: 0, mixed: 0 } },
];
// 5 anonymized actions (stripped evidence + detail)
export const DEMO_ACTIONS: DemoAction[] = [
{
action: "Address staff attitude during assessments",
owner: "Manager",
effort: "medium",
impact: "high",
source: "MANNER",
timeline: "This month",
success_metric: "50% reduction in negative attitude mentions within 60 days",
},
{
action: "Improve online process documentation",
owner: "Manager",
effort: "medium",
impact: "medium",
source: "FRICTION",
timeline: "This month",
success_metric: "80% of customers report a clearer process within 90 days",
},
{
action: "Review and adjust pricing estimates",
owner: "Manager",
effort: "high",
impact: "high",
source: "VALUE_FOR_MONEY",
timeline: "This quarter",
success_metric: "30% increase in positive pricing mentions within 90 days",
},
{
action: "Strengthen post-service communication",
owner: "Manager",
effort: "medium",
impact: "high",
source: "RELIABILITY",
timeline: "This month",
success_metric: "95% satisfaction in payment communication within 60 days",
},
{
action: "Reinforce customer service training",
owner: "Manager",
effort: "medium",
impact: "medium",
source: "ATTENTIVENESS",
timeline: "This month",
success_metric: "90% positive customer attention mentions within 90 days",
},
];
// Domain color map (same as ThemeMatrix source)
export const DOMAIN_COLORS: Record<string, string> = {
P: "#3b82f6", // People/Service — blue
J: "#f59e0b", // Journey/Process — amber
V: "#f43f5e", // Value — rose
O: "#22c55e", // Output/Product — green
E: "#8b5cf6", // Environment — violet
meta: "#6b7280", // Meta — gray
};

View File

@@ -28,14 +28,14 @@ const GaugeMockup = () => (
fill="none"
/>
<path
d="M6 32 A26 26 0 0 1 50 10"
d="M6 32 A26 26 0 0 1 50.4 13.6"
stroke="#4285F4"
strokeWidth="6"
strokeLinecap="round"
fill="none"
/>
</svg>
<div className="flex w-full items-center justify-center gap-1.5">
<div className="flex w-full items-center justify-center gap-1">
{["#3b82f6", "#22c55e", "#f59e0b"].map((color) => (
<div
key={color}
@@ -85,22 +85,27 @@ export const HowItWorks = async () => {
</SectionHeader>
<div className="relative grid w-full max-w-5xl grid-cols-1 gap-8 md:grid-cols-3 md:gap-6 lg:gap-10">
{/* Connector line behind all cards */}
<div
className="pointer-events-none absolute hidden md:block"
style={{ top: "calc(1.5rem + 1.25rem)", left: "16.67%", right: "16.67%" }}
>
<div className="h-px w-full border-t-2 border-dashed border-muted-foreground/20" />
</div>
{steps.map((step, index) => {
const Mockup = stepMockups[index]!;
return (
<div
key={step.key}
className={cn(
"relative flex flex-col items-center gap-4 rounded-xl border bg-card p-6 text-center",
"relative z-10 flex flex-col items-center gap-4 rounded-xl border bg-card p-6 text-center",
"transition-all hover:-translate-y-1 hover:shadow-lg",
)}
>
{/* Numbered circle + connector */}
<div className="relative flex w-full items-center justify-center">
{index < steps.length - 1 && (
<div className="absolute top-1/2 left-[calc(50%+24px)] hidden h-px w-[calc(100%-24px)] border-t-2 border-dashed border-muted-foreground/20 md:block" />
)}
<div className="relative z-10 flex size-10 items-center justify-center rounded-full bg-[#4285F4] text-sm font-bold text-white">
{/* Numbered circle */}
<div className="flex w-full items-center justify-center">
<div className="flex size-10 items-center justify-center rounded-full bg-[#4285F4] text-sm font-bold text-white">
{step.number}
</div>
</div>

View File

@@ -0,0 +1,195 @@
"use client";
import { useTranslation } from "@turbostarter/i18n";
import {
Section,
SectionBadge,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
// ---------------------------------------------------------------------------
// Static data
// ---------------------------------------------------------------------------
const SCORE_BANDS = [
{ range: "039", width: "40%", color: "#ef4444", key: "scoreCritical" },
{ range: "4059", width: "20%", color: "#f97316", key: "scorePoor" },
{ range: "6074", width: "15%", color: "#f59e0b", key: "scoreFair" },
{ range: "7589", width: "15%", color: "#22c55e", key: "scoreGood" },
{ range: "90100", width: "10%", color: "#059669", key: "scoreExcellent" },
] as const;
const VALENCE_MARKERS = [
{ symbol: "+", color: "#22c55e", bg: "bg-green-50 dark:bg-green-950/30", border: "border-green-300 dark:border-green-800", key: "valencePos" },
{ symbol: "\u2212", color: "#ef4444", bg: "bg-red-50 dark:bg-red-950/30", border: "border-red-300 dark:border-red-800", key: "valenceNeg" },
{ symbol: "0", color: "#9ca3af", bg: "bg-gray-50 dark:bg-gray-900/30", border: "border-gray-300 dark:border-gray-700", key: "valenceNeu" },
{ symbol: "\u00b1", color: "#f59e0b", bg: "bg-amber-50 dark:bg-amber-950/30", border: "border-amber-300 dark:border-amber-800", key: "valenceMix" },
] as const;
const EXPERIENCE_DOMAINS = [
{ code: "O", color: "#3b82f6", key: "domainO" },
{ code: "P", color: "#22c55e", key: "domainP" },
{ code: "J", color: "#f59e0b", key: "domainJ" },
{ code: "E", color: "#8b5cf6", key: "domainE" },
{ code: "V", color: "#f43f5e", key: "domainV" },
] as const;
const INTENSITY_LEVELS = [
{ bars: 1, key: "intensity1" },
{ bars: 2, key: "intensity2" },
{ bars: 3, key: "intensity3" },
] as const;
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export const HowToRead = () => {
const { t } = useTranslation("marketing");
return (
<Section id="how-to-read">
<SectionHeader>
<SectionBadge>{t("howToRead.label")}</SectionBadge>
<SectionTitle>{t("howToRead.title")}</SectionTitle>
<SectionDescription>{t("howToRead.description")}</SectionDescription>
</SectionHeader>
<div className="w-full max-w-3xl space-y-4">
{/* Score Spectrum */}
<div className="bg-muted/20 rounded-xl border p-5">
<h3 className="text-foreground mb-3 text-sm font-bold">
{t("howToRead.scoreTitle")}
</h3>
<div className="mb-1.5 flex h-7 overflow-hidden rounded-md">
{SCORE_BANDS.map((band) => (
<div
key={band.range}
className="flex items-center justify-center text-[9px] font-semibold text-white"
style={{ width: band.width, backgroundColor: band.color }}
>
{t(`howToRead.${band.key}` as const)}
</div>
))}
</div>
<div className="text-muted-foreground mb-2 flex justify-between text-[9px]">
<span>0</span>
<span>40</span>
<span>60</span>
<span>75</span>
<span>90</span>
<span>100</span>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">
{t("howToRead.scoreDesc")}
</p>
</div>
{/* 2x2 Grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* Sentiment Markers */}
<div className="bg-muted/20 rounded-xl border p-5">
<h3 className="text-foreground mb-1 text-sm font-bold">
{t("howToRead.valenceTitle")}
</h3>
<p className="text-muted-foreground mb-3 text-xs leading-relaxed">
{t("howToRead.valenceDesc")}
</p>
<div className="flex flex-col gap-2">
{VALENCE_MARKERS.map((v) => (
<div key={v.symbol} className="flex items-center gap-2.5">
<span
className={`flex size-6 shrink-0 items-center justify-center rounded border text-sm font-bold ${v.bg} ${v.border}`}
style={{ color: v.color }}
>
{v.symbol}
</span>
<span className="text-muted-foreground text-xs">
{t(`howToRead.${v.key}` as const)}
</span>
</div>
))}
</div>
</div>
{/* Experience Domains */}
<div className="bg-muted/20 rounded-xl border p-5">
<h3 className="text-foreground mb-1 text-sm font-bold">
{t("howToRead.domainsTitle")}
</h3>
<p className="text-muted-foreground mb-3 text-xs leading-relaxed">
{t("howToRead.domainsDesc")}
</p>
<div className="flex flex-col gap-2">
{EXPERIENCE_DOMAINS.map((d) => (
<div key={d.code} className="flex items-center gap-2.5">
<span
className="flex size-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
style={{ backgroundColor: d.color }}
>
{d.code}
</span>
<span className="text-muted-foreground text-xs">
{t(`howToRead.${d.key}` as const)}
</span>
</div>
))}
</div>
</div>
{/* Intensity Levels */}
<div className="bg-muted/20 rounded-xl border p-5">
<h3 className="text-foreground mb-1 text-sm font-bold">
{t("howToRead.intensityTitle")}
</h3>
<p className="text-muted-foreground mb-3 text-xs leading-relaxed">
{t("howToRead.intensityDesc")}
</p>
<div className="flex flex-col gap-2">
{INTENSITY_LEVELS.map((il) => (
<div key={il.bars} className="flex items-center gap-2.5">
<div className="flex w-6 shrink-0 justify-center gap-0.5">
{Array.from({ length: 3 }, (_, i) => (
<div
key={i}
className="h-3.5 w-[5px] rounded-sm"
style={{
backgroundColor: i < il.bars ? "#4285F4" : "#e2e8f0",
}}
/>
))}
</div>
<span className="text-muted-foreground text-xs">
{t(`howToRead.${il.key}` as const)}
</span>
</div>
))}
</div>
</div>
{/* Reading Tips */}
<div className="bg-muted/20 rounded-xl border p-5">
<h3 className="text-foreground mb-3 text-sm font-bold">
{t("howToRead.tipsTitle")}
</h3>
<div className="flex flex-col gap-2.5">
{(["howToRead.tip1", "howToRead.tip2", "howToRead.tip3", "howToRead.tip4"] as const).map((key, i) => (
<div key={key} className="flex gap-2">
<span className="shrink-0 text-xs font-bold" style={{ color: "#4285F4" }}>
{i + 1}.
</span>
<span className="text-muted-foreground text-xs leading-relaxed">
{t(key)}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</Section>
);
};

View File

@@ -2,9 +2,9 @@ import Image from "next/image";
export const ReportFan = () => {
return (
<div className="animate-fade-in relative mx-auto mt-12 -translate-y-4 opacity-0 [--animation-delay:800ms] sm:mt-16 md:mt-20">
<div className="animate-fade-in relative mx-auto mt-12 -translate-y-4 overflow-x-clip opacity-0 [--animation-delay:800ms] sm:mt-16 md:mt-20">
{/* Ambient glow behind the fan */}
<div className="pointer-events-none absolute inset-0 -inset-x-20 -bottom-10">
<div className="pointer-events-none absolute inset-0 -bottom-10">
<div className="absolute top-1/2 left-1/2 h-[500px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/8 blur-[100px]" />
<div className="absolute top-1/3 left-1/3 h-[300px] w-[300px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/10 blur-[80px]" />
</div>

View File

@@ -3,6 +3,20 @@
import { useState } from "react";
import { useTranslation } from "@turbostarter/i18n";
import { cn } from "@turbostarter/ui";
import {
ScatterChart,
Scatter,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ZAxis,
Cell,
ReferenceLine,
ReferenceArea,
LabelList,
} from "recharts";
import { CtaButton } from "~/modules/marketing/layout/cta-button";
import {
@@ -12,6 +26,11 @@ import {
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
import {
DEMO_THEMES,
DEMO_ACTIONS,
DOMAIN_COLORS as THEME_DOMAIN_COLORS,
} from "./demo-report-data";
// ---------------------------------------------------------------------------
// Demo data (hardcoded for "Bistro El Sol")
@@ -44,14 +63,27 @@ const SEVERITY_COLORS: Record<string, string> = {
};
const EFFORT_COLORS: Record<string, string> = {
low: "#22c55e",
medium: "#f59e0b",
high: "#ef4444",
Low: "#22c55e",
Medium: "#f59e0b",
High: "#ef4444",
Bajo: "#22c55e",
Medio: "#f59e0b",
Alto: "#ef4444",
};
const IMPACT_COLORS: Record<string, string> = {
low: "#6b7280",
medium: "#f59e0b",
high: "#22c55e",
Low: "#6b7280",
Medium: "#f59e0b",
High: "#22c55e",
Bajo: "#6b7280",
Medio: "#f59e0b",
Alto: "#22c55e",
};
const DOMAIN_COLORS: Record<string, string> = {
@@ -60,7 +92,7 @@ const DOMAIN_COLORS: Record<string, string> = {
Environment: "#8b5cf6",
};
const TABS = ["score", "domains", "issues", "actions"] as const;
const TABS = ["score", "domains", "issues", "actions", "themes"] as const;
type Tab = (typeof TABS)[number];
// ---------------------------------------------------------------------------
@@ -79,16 +111,16 @@ function ScoreTab() {
return (
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start sm:gap-10">
{/* Left: Gauge + band */}
<div className="flex shrink-0 flex-col items-center gap-2">
<div className="flex shrink-0 flex-col items-center gap-3">
<span className="text-muted-foreground text-xs tracking-wide uppercase">
{t("reportPreview.demo.businessName")}
</span>
<div className="relative" style={{ width: 200, height: 120 }}>
<div className="relative" style={{ width: 200, height: 130 }}>
<svg
width="200"
height="120"
viewBox="0 0 200 120"
height="130"
viewBox="0 0 200 130"
className="overflow-visible"
>
<circle
@@ -118,7 +150,7 @@ function ScoreTab() {
className="transition-all duration-1000 ease-out"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-end pb-1">
<div className="absolute inset-0 flex flex-col items-center justify-end pb-3">
<span
className="text-6xl font-bold tabular-nums"
style={{ color: SCORE_COLOR }}
@@ -139,7 +171,7 @@ function ScoreTab() {
<div style={{ width: "20%", backgroundColor: "#22c55e" }} />
<div style={{ width: "40%", backgroundColor: "#059669" }} />
</div>
<div className="relative h-2.5">
<div className="relative h-3">
<div
className="absolute top-0"
style={{
@@ -153,6 +185,14 @@ function ScoreTab() {
}}
/>
</div>
<div className="relative -mt-0.5 flex text-[7px] text-muted-foreground">
<span className="absolute left-0 -translate-x-1/2">0</span>
<span className="absolute" style={{ left: "10%" }}>40</span>
<span className="absolute" style={{ left: "25%" }}>60</span>
<span className="absolute" style={{ left: "40%" }}>75</span>
<span className="absolute" style={{ left: "60%" }}>90</span>
<span className="absolute right-0 translate-x-1/2">100</span>
</div>
</div>
</div>
@@ -292,59 +332,84 @@ function IssuesTab() {
}
// ---------------------------------------------------------------------------
// Actions Tab
// Actions Tab (enhanced with real report data)
// ---------------------------------------------------------------------------
function ActionsTab() {
const { t } = useTranslation("marketing");
const actions = [
{ key: "action1" as const, idx: 0 },
{ key: "action2" as const, idx: 1 },
{ key: "action3" as const, idx: 2 },
];
// Build mention count map from demo themes
const mentionMap = new Map<string, number>();
for (const theme of DEMO_THEMES) {
mentionMap.set(theme.primitive, theme.count);
}
return (
<div className="flex flex-col gap-4">
{actions.map(({ key: actionKey, idx }) => {
const effort = t(`reportPreview.demo.actions.${actionKey}.effort` as const);
const impact = t(`reportPreview.demo.actions.${actionKey}.impact` as const);
{DEMO_ACTIONS.map((action, idx) => {
const effortColor = EFFORT_COLORS[action.effort] ?? "#f59e0b";
const impactColor = IMPACT_COLORS[action.impact] ?? "#f59e0b";
const mentions = mentionMap.get(action.source);
return (
<div
key={actionKey}
className="bg-muted/20 flex gap-3 rounded-lg border p-3"
key={idx}
className="bg-muted/20 overflow-hidden rounded-lg border"
>
<div className="bg-primary/10 text-primary flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-bold">
{idx + 1}
</div>
<div className="flex gap-3 p-3">
<div className="bg-primary/10 text-primary flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-bold">
{idx + 1}
</div>
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-semibold leading-tight">
{t(`reportPreview.demo.actions.${actionKey}.title` as const)}
</p>
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-semibold leading-tight">
{action.action}
</p>
<div className="mt-2 flex flex-wrap gap-2">
<span
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
style={{
backgroundColor: `${EFFORT_COLORS[effort] ?? "#f59e0b"}18`,
color: EFFORT_COLORS[effort] ?? "#f59e0b",
}}
>
{effort} effort
</span>
<span
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
style={{
backgroundColor: `${IMPACT_COLORS[impact] ?? "#f59e0b"}18`,
color: IMPACT_COLORS[impact] ?? "#f59e0b",
}}
>
{impact} impact
</span>
<div className="mt-1.5 flex flex-wrap items-center gap-2">
<span className="text-muted-foreground text-xs">
{action.owner}
</span>
{mentions != null && (
<span className="text-xs font-medium" style={{ color: "#4285F4" }}>
{mentions} {t("reportPreview.demo.themes.mentions" as const)}
</span>
)}
<span className="text-muted-foreground text-xs">
{action.timeline}
</span>
</div>
<div className="mt-2 flex flex-wrap gap-2">
<span
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
style={{
backgroundColor: `${effortColor}18`,
color: effortColor,
}}
>
{action.effort} effort
</span>
<span
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
style={{
backgroundColor: `${impactColor}18`,
color: impactColor,
}}
>
{action.impact} impact
</span>
</div>
</div>
</div>
{action.success_metric && (
<div className="bg-muted/30 border-t px-4 py-2">
<p className="text-muted-foreground text-xs leading-relaxed">
<span className="font-semibold">Target:</span> {action.success_metric}
</p>
</div>
)}
</div>
);
})}
@@ -352,6 +417,149 @@ function ActionsTab() {
);
}
// ---------------------------------------------------------------------------
// Themes Tab (ThemeMatrix scatter chart)
// ---------------------------------------------------------------------------
interface ScatterPoint {
x: number;
y: number;
z: number;
name: string;
domain: string;
color: string;
}
function ThemesTab() {
const { t } = useTranslation("marketing");
const data: ScatterPoint[] = DEMO_THEMES
.filter((theme) => theme.domain !== "meta")
.map((theme) => {
const total =
theme.valence.positive +
theme.valence.negative +
theme.valence.neutral +
theme.valence.mixed;
const sentimentRatio =
total > 0
? ((theme.valence.positive - theme.valence.negative) / total) * 100
: 0;
return {
x: theme.count,
y: Math.round(sentimentRatio),
z: theme.weight * theme.count,
name: theme.label,
domain: theme.domain,
color: THEME_DOMAIN_COLORS[theme.domain] ?? "#6b7280",
};
})
.sort((a, b) => b.z - a.z);
const yValues = data.map((d) => d.y);
const yMin = Math.min(-20, ...yValues) - 10;
const yMax = Math.max(20, ...yValues) + 10;
return (
<div>
{/* Domain legend */}
<div className="mb-4 flex flex-wrap justify-center gap-x-4 gap-y-1">
{[
{ label: "People/Service", domain: "P" },
{ label: "Journey", domain: "J" },
{ label: "Value", domain: "V" },
{ label: "Product", domain: "O" },
{ label: "Environment", domain: "E" },
].map((d) => (
<span key={d.domain} className="inline-flex items-center gap-1.5 text-xs">
<span
className="inline-block size-2 rounded-full"
style={{ backgroundColor: THEME_DOMAIN_COLORS[d.domain] }}
/>
<span className="text-muted-foreground">{d.label}</span>
</span>
))}
</div>
<ResponsiveContainer width="100%" height={300}>
<ScatterChart margin={{ top: 20, right: 30, left: 10, bottom: 10 }}>
<defs>
<linearGradient id="tmPositiveZone" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stopColor="#22c55e" stopOpacity={0.02} />
<stop offset="100%" stopColor="#22c55e" stopOpacity={0.08} />
</linearGradient>
<linearGradient id="tmNegativeZone" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.02} />
<stop offset="100%" stopColor="#ef4444" stopOpacity={0.08} />
</linearGradient>
</defs>
<ReferenceArea y1={0} y2={yMax} fill="url(#tmPositiveZone)" strokeOpacity={0} />
<ReferenceArea y1={yMin} y2={0} fill="url(#tmNegativeZone)" strokeOpacity={0} />
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis
type="number"
dataKey="x"
tick={{ fill: "#6b7280", fontSize: 11 }}
tickLine={false}
axisLine={false}
label={{
value: t("reportPreview.demo.themes.frequency" as const),
position: "insideBottom",
offset: -5,
fill: "#9ca3af",
fontSize: 12,
}}
/>
<YAxis
type="number"
dataKey="y"
domain={[yMin, yMax]}
tick={{ fill: "#6b7280", fontSize: 11 }}
tickLine={false}
axisLine={false}
label={{
value: t("reportPreview.demo.themes.sentiment" as const),
angle: -90,
position: "insideLeft",
fill: "#9ca3af",
fontSize: 12,
}}
/>
<ReferenceLine y={0} stroke="#94a3b8" strokeWidth={1.5} />
<ZAxis type="number" dataKey="z" range={[80, 500]} />
<Tooltip
content={({ active, payload }) => {
if (!active || !payload?.length) return null;
const point = (payload[0] as Record<string, unknown>)
?.payload as ScatterPoint | undefined;
if (!point) return null;
return (
<div className="bg-background rounded-lg border p-2 shadow-md">
<div className="text-foreground text-[13px] font-bold">
{point.name}
</div>
<div className="text-muted-foreground text-xs">
{t("reportPreview.demo.themes.mentions" as const)}: <strong>{point.x}</strong>
</div>
<div className="text-muted-foreground text-xs">
{t("reportPreview.demo.themes.netSentiment" as const)}: <strong>{point.y}%</strong>
</div>
</div>
);
}}
/>
<Scatter data={data}>
{data.map((entry, i) => (
<Cell key={i} fill={entry.color} fillOpacity={0.7} />
))}
<LabelList dataKey="name" position="top" fontSize={9} fill="#6b7280" offset={8} />
</Scatter>
</ScatterChart>
</ResponsiveContainer>
</div>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
@@ -372,7 +580,7 @@ export const ReportPreview = () => {
<div className="relative w-full max-w-3xl">
{/* Ambient glow */}
<div className="pointer-events-none absolute inset-0 -inset-x-20 -bottom-10">
<div className="pointer-events-none absolute inset-0 -bottom-10">
<div className="absolute top-1/2 left-1/2 h-[400px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/8 blur-[100px]" />
<div className="absolute top-1/3 left-1/3 h-[250px] w-[250px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/10 blur-[80px]" />
</div>
@@ -408,6 +616,7 @@ export const ReportPreview = () => {
{activeTab === "domains" && <DomainsTab />}
{activeTab === "issues" && <IssuesTab />}
{activeTab === "actions" && <ActionsTab />}
{activeTab === "themes" && <ThemesTab />}
</div>
</div>
</div>

View File

@@ -89,7 +89,7 @@ export const Testimonials = () => {
</div>
</SectionHeader>
<div className="relative flex h-[600px] grow flex-row items-center justify-center overflow-hidden lg:basis-0">
<div className="relative flex h-[600px] grow flex-row items-center justify-center overflow-clip lg:basis-0">
{rows.map((row, index) => (
<Marquee
key={index}

View File

@@ -41,7 +41,7 @@
},
"receive": {
"title": "Get your Blueprint",
"description": "Receive a clear, actionable Reputation Blueprint with scores, trends, and a prioritized fix list."
"description": "Receive a clear Reputation Blueprint with scores, trends, and a prioritized fix list."
}
}
},
@@ -59,15 +59,16 @@
},
"reportPreview": {
"label": "See Your Report",
"title": "This is what you get",
"title": "What you get",
"description": "A real preview of the Reputation Blueprint — the exact analysis your business receives.",
"tabs": {
"score": "Score",
"domains": "Domains",
"issues": "Issues",
"actions": "Actions"
"actions": "Actions",
"themes": "Themes"
},
"cta": "Get yours for your business",
"cta": "Get yours",
"demo": {
"businessName": "Bistro El Sol",
"scoreLabel": "Reputation Score",
@@ -125,9 +126,50 @@
"effort": "Medium",
"impact": "Medium"
}
},
"themes": {
"frequency": "Frequency",
"sentiment": "Net Sentiment %",
"mentions": "mentions",
"netSentiment": "Net Sentiment"
}
}
},
"howToRead": {
"label": "Reading Your Blueprint",
"title": "How to read your report",
"description": "A quick guide to the symbols, colors, and scores in your Reputation Blueprint.",
"scoreTitle": "Score Spectrum",
"scoreDesc": "Your overall Reputation Score sits on this 0100 scale. Higher means stronger reputation.",
"scoreCritical": "Critical",
"scorePoor": "Poor",
"scoreFair": "Fair",
"scoreGood": "Good",
"scoreExcellent": "Excellent",
"valenceTitle": "Sentiment Markers",
"valenceDesc": "Each review mention is tagged with a sentiment marker.",
"valencePos": "Positive — customer praised this aspect",
"valenceNeg": "Negative — customer complained about this",
"valenceNeu": "Neutral — mentioned without sentiment",
"valenceMix": "Mixed — both positive and negative signals",
"domainsTitle": "Experience Domains",
"domainsDesc": "We group every theme into five experience domains.",
"domainO": "Operations — how the business runs",
"domainP": "People/Service — staff and service quality",
"domainJ": "Journey — the customer process",
"domainE": "Environment — physical space and atmosphere",
"domainV": "Value — pricing and perceived worth",
"intensityTitle": "Intensity Levels",
"intensityDesc": "How strongly customers feel about each theme.",
"intensity1": "Mild — casual mention",
"intensity2": "Moderate — clear opinion",
"intensity3": "Strong — emphatic statement",
"tipsTitle": "Reading Tips",
"tip1": "Start with the Score tab for the big picture, then drill into Domains.",
"tip2": "Large bubbles in the Theme Matrix mean high-frequency topics — prioritize these.",
"tip3": "Bubbles below the zero line indicate net-negative sentiment — these are your urgent fixes.",
"tip4": "The Action Plan is already sorted by impact — start from the top."
},
"reviewEvidence": {
"label": "Deep Review Analysis",
"title": "We read what you can't",
@@ -202,11 +244,11 @@
},
"howScoringWorks": {
"question": "How does the 0100 scoring work?",
"answer": "Your Reputation Score combines sentiment analysis, topic frequency, trend direction, and response patterns across 37 primitives. A score of 70+ indicates strong reputation health. We also break it down across 6 domains so you can see exactly where you stand."
"answer": "Your Reputation Score combines sentiment analysis, topic frequency, trend direction, and response patterns across 37 dimensions. A score above 70 means your reputation is strong. We also break it down across 6 domains so you can see exactly where you stand."
},
"deliveryTime": {
"question": "How long does it take to get my report?",
"answer": "Most reports are delivered within 24 hours. For businesses with more than 500 reviews, it may take up to 48 hours to ensure thoroughness."
"answer": "Most reports arrive within 24 hours. For businesses with more than 500 reviews, it may take up to 48 hours so we can analyze every review."
},
"whatBusinessTypes": {
"question": "What types of businesses can use this?",
@@ -222,7 +264,7 @@
},
"reportContents": {
"question": "What's included in the Reputation Blueprint?",
"answer": "Your Blueprint includes: a 0100 Reputation Score, scores across 6 domains (37 primitives), a staff mention leaderboard, sentiment trends over time, a prioritized action plan, and specific review quotes that illustrate each finding."
"answer": "Your Blueprint includes: a 0100 Reputation Score, scores across 6 domains (37 dimensions), a staff mention leaderboard, sentiment trends over time, a prioritized action plan, and specific review quotes that illustrate each finding."
}
}
},

View File

@@ -41,7 +41,7 @@
},
"receive": {
"title": "Recibe tu Radiografía",
"description": "Recibe una Radiografía de Reputación clara y accionable con puntuaciones, tendencias y una lista priorizada de mejoras."
"description": "Recibe una Radiografía de Reputación clara con puntuaciones, tendencias y una lista priorizada de mejoras."
}
}
},
@@ -59,15 +59,16 @@
},
"reportPreview": {
"label": "Ve Tu Informe",
"title": "Esto es lo que recibes",
"title": "Lo que recibes",
"description": "Una vista real de la Radiografía de Reputación — el análisis exacto que recibe tu negocio.",
"tabs": {
"score": "Puntuación",
"domains": "Dominios",
"issues": "Problemas",
"actions": "Acciones"
"actions": "Acciones",
"themes": "Temas"
},
"cta": "Obtén el tuyo para tu negocio",
"cta": "Obtén el tuyo",
"demo": {
"businessName": "Bistro El Sol",
"scoreLabel": "Puntuación de Reputación",
@@ -125,9 +126,50 @@
"effort": "Medio",
"impact": "Medio"
}
},
"themes": {
"frequency": "Frecuencia",
"sentiment": "Sentimiento Neto %",
"mentions": "menciones",
"netSentiment": "Sentimiento Neto"
}
}
},
"howToRead": {
"label": "Leyendo Tu Radiografía",
"title": "Cómo leer tu informe",
"description": "Una guía rápida de los símbolos, colores y puntuaciones de tu Radiografía de Reputación.",
"scoreTitle": "Espectro de Puntuación",
"scoreDesc": "Tu Puntuación de Reputación general se ubica en esta escala de 0 a 100. Mayor significa mejor reputación.",
"scoreCritical": "Crítico",
"scorePoor": "Pobre",
"scoreFair": "Regular",
"scoreGood": "Bueno",
"scoreExcellent": "Excelente",
"valenceTitle": "Marcadores de Sentimiento",
"valenceDesc": "Cada mención en una reseña se etiqueta con un marcador de sentimiento.",
"valencePos": "Positivo — el cliente elogió este aspecto",
"valenceNeg": "Negativo — el cliente se quejó de esto",
"valenceNeu": "Neutro — mencionado sin sentimiento",
"valenceMix": "Mixto — señales positivas y negativas",
"domainsTitle": "Dominios de Experiencia",
"domainsDesc": "Agrupamos cada tema en cinco dominios de experiencia.",
"domainO": "Operaciones — cómo funciona el negocio",
"domainP": "Personas/Servicio — personal y calidad del servicio",
"domainJ": "Recorrido — el proceso del cliente",
"domainE": "Ambiente — espacio físico y atmósfera",
"domainV": "Valor — precios y valor percibido",
"intensityTitle": "Niveles de Intensidad",
"intensityDesc": "Cuán fuertemente sienten los clientes sobre cada tema.",
"intensity1": "Leve — mención casual",
"intensity2": "Moderado — opinión clara",
"intensity3": "Fuerte — afirmación enfática",
"tipsTitle": "Consejos de Lectura",
"tip1": "Empieza con la pestaña Puntuación para la visión general, luego profundiza en Dominios.",
"tip2": "Las burbujas grandes en la Matriz de Temas significan temas frecuentes — prioriza estos.",
"tip3": "Las burbujas debajo de la línea cero indican sentimiento negativo neto — estas son tus correcciones urgentes.",
"tip4": "El Plan de Acción ya está ordenado por impacto — empieza desde arriba."
},
"reviewEvidence": {
"label": "Análisis Profundo",
"title": "Leemos lo que tú no puedes",
@@ -202,11 +244,11 @@
},
"howScoringWorks": {
"question": "¿Cómo funciona la puntuación de 0 a 100?",
"answer": "Tu Puntuación de Reputación combina análisis de sentimiento, frecuencia de temas, dirección de tendencias y patrones de respuesta en 37 primitivas. Una puntuación de 70+ indica una reputación saludable. También la desglosamos en 6 dominios para que veas exactamente dónde te encuentras."
"answer": "Tu Puntuación de Reputación combina análisis de sentimiento, frecuencia de temas, dirección de tendencias y patrones de respuesta en 37 dimensiones. Una puntuación superior a 70 significa que tu reputación es sólida. También la desglosamos en 6 dominios para que veas exactamente dónde te encuentras."
},
"deliveryTime": {
"question": "¿Cuánto tiempo tarda en llegar mi informe?",
"answer": "La mayoría de los informes se entregan en 24 horas. Para negocios con más de 500 reseñas, puede tomar hasta 48 horas para garantizar un análisis completo."
"answer": "La mayoría de los informes llegan en 24 horas. Para negocios con más de 500 reseñas, puede tomar hasta 48 horas para analizar cada reseña."
},
"whatBusinessTypes": {
"question": "¿Qué tipos de negocios pueden usar esto?",
@@ -222,7 +264,7 @@
},
"reportContents": {
"question": "¿Qué incluye la Radiografía de Reputación?",
"answer": "Tu Radiografía incluye: una Puntuación de Reputación de 0 a 100, puntuaciones en 6 dominios (37 primitivas), un ranking de menciones del equipo, tendencias de sentimiento a lo largo del tiempo, un plan de acción priorizado y citas de reseñas específicas que ilustran cada hallazgo."
"answer": "Tu Radiografía incluye: una Puntuación de Reputación de 0 a 100, puntuaciones en 6 dominios (37 dimensiones), un ranking de menciones del equipo, tendencias de sentimiento a lo largo del tiempo, un plan de acción priorizado y citas de reseñas específicas que ilustran cada hallazgo."
}
}
},