From 61428c889ed91a436388f26cdd5edde363c9bf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:27:13 +0000 Subject: [PATCH] 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 --- .../web/src/app/[locale]/(marketing)/page.tsx | 4 +- .../web/src/modules/marketing/home/banner.tsx | 38 +-- .../marketing/home/demo-report-data.ts | 102 ++++++ .../modules/marketing/home/how-it-works.tsx | 23 +- .../modules/marketing/home/how-to-read.tsx | 195 ++++++++++++ .../src/modules/marketing/home/report-fan.tsx | 4 +- .../modules/marketing/home/report-preview.tsx | 299 +++++++++++++++--- .../modules/marketing/home/testimonials.tsx | 2 +- .../i18n/src/translations/en/marketing.json | 56 +++- .../i18n/src/translations/es/marketing.json | 56 +++- 10 files changed, 675 insertions(+), 104 deletions(-) create mode 100644 apps/web/src/modules/marketing/home/demo-report-data.ts create mode 100644 apps/web/src/modules/marketing/home/how-to-read.tsx diff --git a/apps/web/src/app/[locale]/(marketing)/page.tsx b/apps/web/src/app/[locale]/(marketing)/page.tsx index 4eb89c9..df6a8de 100644 --- a/apps/web/src/app/[locale]/(marketing)/page.tsx +++ b/apps/web/src/app/[locale]/(marketing)/page.tsx @@ -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 ( <> - + diff --git a/apps/web/src/modules/marketing/home/banner.tsx b/apps/web/src/modules/marketing/home/banner.tsx index 5ad319e..2475377 100644 --- a/apps/web/src/modules/marketing/home/banner.tsx +++ b/apps/web/src/modules/marketing/home/banner.tsx @@ -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 = () => ( - -); - 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" > - +

{t("cta.question")}

diff --git a/apps/web/src/modules/marketing/home/demo-report-data.ts b/apps/web/src/modules/marketing/home/demo-report-data.ts new file mode 100644 index 0000000..afd3663 --- /dev/null +++ b/apps/web/src/modules/marketing/home/demo-report-data.ts @@ -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 = { + 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 +}; diff --git a/apps/web/src/modules/marketing/home/how-it-works.tsx b/apps/web/src/modules/marketing/home/how-it-works.tsx index 6c46d9d..954a628 100644 --- a/apps/web/src/modules/marketing/home/how-it-works.tsx +++ b/apps/web/src/modules/marketing/home/how-it-works.tsx @@ -28,14 +28,14 @@ const GaugeMockup = () => ( fill="none" /> -
+
{["#3b82f6", "#22c55e", "#f59e0b"].map((color) => (
{
+ {/* Connector line behind all cards */} +
+
+
+ {steps.map((step, index) => { const Mockup = stepMockups[index]!; return (
- {/* Numbered circle + connector */} -
- {index < steps.length - 1 && ( -
- )} -
+ {/* Numbered circle */} +
+
{step.number}
diff --git a/apps/web/src/modules/marketing/home/how-to-read.tsx b/apps/web/src/modules/marketing/home/how-to-read.tsx new file mode 100644 index 0000000..ee5e443 --- /dev/null +++ b/apps/web/src/modules/marketing/home/how-to-read.tsx @@ -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: "0–39", width: "40%", color: "#ef4444", key: "scoreCritical" }, + { range: "40–59", width: "20%", color: "#f97316", key: "scorePoor" }, + { range: "60–74", width: "15%", color: "#f59e0b", key: "scoreFair" }, + { range: "75–89", width: "15%", color: "#22c55e", key: "scoreGood" }, + { range: "90–100", 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 ( +
+ + {t("howToRead.label")} + {t("howToRead.title")} + {t("howToRead.description")} + + +
+ {/* Score Spectrum */} +
+

+ {t("howToRead.scoreTitle")} +

+
+ {SCORE_BANDS.map((band) => ( +
+ {t(`howToRead.${band.key}` as const)} +
+ ))} +
+
+ 0 + 40 + 60 + 75 + 90 + 100 +
+

+ {t("howToRead.scoreDesc")} +

+
+ + {/* 2x2 Grid */} +
+ {/* Sentiment Markers */} +
+

+ {t("howToRead.valenceTitle")} +

+

+ {t("howToRead.valenceDesc")} +

+
+ {VALENCE_MARKERS.map((v) => ( +
+ + {v.symbol} + + + {t(`howToRead.${v.key}` as const)} + +
+ ))} +
+
+ + {/* Experience Domains */} +
+

+ {t("howToRead.domainsTitle")} +

+

+ {t("howToRead.domainsDesc")} +

+
+ {EXPERIENCE_DOMAINS.map((d) => ( +
+ + {d.code} + + + {t(`howToRead.${d.key}` as const)} + +
+ ))} +
+
+ + {/* Intensity Levels */} +
+

+ {t("howToRead.intensityTitle")} +

+

+ {t("howToRead.intensityDesc")} +

+
+ {INTENSITY_LEVELS.map((il) => ( +
+
+ {Array.from({ length: 3 }, (_, i) => ( +
+ ))} +
+ + {t(`howToRead.${il.key}` as const)} + +
+ ))} +
+
+ + {/* Reading Tips */} +
+

+ {t("howToRead.tipsTitle")} +

+
+ {(["howToRead.tip1", "howToRead.tip2", "howToRead.tip3", "howToRead.tip4"] as const).map((key, i) => ( +
+ + {i + 1}. + + + {t(key)} + +
+ ))} +
+
+
+
+
+ ); +}; diff --git a/apps/web/src/modules/marketing/home/report-fan.tsx b/apps/web/src/modules/marketing/home/report-fan.tsx index 13ed18f..405183d 100644 --- a/apps/web/src/modules/marketing/home/report-fan.tsx +++ b/apps/web/src/modules/marketing/home/report-fan.tsx @@ -2,9 +2,9 @@ import Image from "next/image"; export const ReportFan = () => { return ( -
+
{/* Ambient glow behind the fan */} -
+
diff --git a/apps/web/src/modules/marketing/home/report-preview.tsx b/apps/web/src/modules/marketing/home/report-preview.tsx index 3dca1f1..f90c777 100644 --- a/apps/web/src/modules/marketing/home/report-preview.tsx +++ b/apps/web/src/modules/marketing/home/report-preview.tsx @@ -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 = { }; const EFFORT_COLORS: Record = { + low: "#22c55e", + medium: "#f59e0b", + high: "#ef4444", Low: "#22c55e", Medium: "#f59e0b", High: "#ef4444", + Bajo: "#22c55e", + Medio: "#f59e0b", + Alto: "#ef4444", }; + const IMPACT_COLORS: Record = { + low: "#6b7280", + medium: "#f59e0b", + high: "#22c55e", Low: "#6b7280", Medium: "#f59e0b", High: "#22c55e", + Bajo: "#6b7280", + Medio: "#f59e0b", + Alto: "#22c55e", }; const DOMAIN_COLORS: Record = { @@ -60,7 +92,7 @@ const DOMAIN_COLORS: Record = { 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 (
{/* Left: Gauge + band */} -
+
{t("reportPreview.demo.businessName")} -
+
-
+
-
+
+
+ 0 + 40 + 60 + 75 + 90 + 100 +
@@ -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(); + for (const theme of DEMO_THEMES) { + mentionMap.set(theme.primitive, theme.count); + } return (
- {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 (
-
- {idx + 1} -
+
+
+ {idx + 1} +
-
-

- {t(`reportPreview.demo.actions.${actionKey}.title` as const)} -

+
+

+ {action.action} +

-
- - {effort} effort - - - {impact} impact - +
+ + {action.owner} + + {mentions != null && ( + + {mentions} {t("reportPreview.demo.themes.mentions" as const)} + + )} + + {action.timeline} + +
+ +
+ + {action.effort} effort + + + {action.impact} impact + +
+ + {action.success_metric && ( +
+

+ Target: {action.success_metric} +

+
+ )}
); })} @@ -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 ( +
+ {/* Domain legend */} +
+ {[ + { label: "People/Service", domain: "P" }, + { label: "Journey", domain: "J" }, + { label: "Value", domain: "V" }, + { label: "Product", domain: "O" }, + { label: "Environment", domain: "E" }, + ].map((d) => ( + + + {d.label} + + ))} +
+ + + + + + + + + + + + + + + + + + + + + { + if (!active || !payload?.length) return null; + const point = (payload[0] as Record) + ?.payload as ScatterPoint | undefined; + if (!point) return null; + return ( +
+
+ {point.name} +
+
+ {t("reportPreview.demo.themes.mentions" as const)}: {point.x} +
+
+ {t("reportPreview.demo.themes.netSentiment" as const)}: {point.y}% +
+
+ ); + }} + /> + + {data.map((entry, i) => ( + + ))} + + +
+
+
+ ); +} + // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- @@ -372,7 +580,7 @@ export const ReportPreview = () => {
{/* Ambient glow */} -
+
@@ -408,6 +616,7 @@ export const ReportPreview = () => { {activeTab === "domains" && } {activeTab === "issues" && } {activeTab === "actions" && } + {activeTab === "themes" && }
diff --git a/apps/web/src/modules/marketing/home/testimonials.tsx b/apps/web/src/modules/marketing/home/testimonials.tsx index 85cfc6e..ea6bccc 100644 --- a/apps/web/src/modules/marketing/home/testimonials.tsx +++ b/apps/web/src/modules/marketing/home/testimonials.tsx @@ -89,7 +89,7 @@ export const Testimonials = () => {
-
+
{rows.map((row, index) => (