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:
@@ -2,18 +2,18 @@ import { Banner } from "~/modules/marketing/home/banner";
|
|||||||
import { Faq } from "~/modules/marketing/home/faq";
|
import { Faq } from "~/modules/marketing/home/faq";
|
||||||
import { Hero } from "~/modules/marketing/home/hero";
|
import { Hero } from "~/modules/marketing/home/hero";
|
||||||
import { HowItWorks } from "~/modules/marketing/home/how-it-works";
|
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 { ReportPreview } from "~/modules/marketing/home/report-preview";
|
||||||
import { ReviewEvidence } from "~/modules/marketing/home/review-evidence";
|
import { ReviewEvidence } from "~/modules/marketing/home/review-evidence";
|
||||||
import { SocialProof } from "~/modules/marketing/home/social-proof";
|
|
||||||
import { Testimonials } from "~/modules/marketing/home/testimonials";
|
import { Testimonials } from "~/modules/marketing/home/testimonials";
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero />
|
||||||
<SocialProof />
|
|
||||||
<HowItWorks />
|
<HowItWorks />
|
||||||
<ReportPreview />
|
<ReportPreview />
|
||||||
|
<HowToRead />
|
||||||
<ReviewEvidence />
|
<ReviewEvidence />
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
<Faq />
|
<Faq />
|
||||||
|
|||||||
@@ -2,39 +2,10 @@ import { getTranslation } from "@turbostarter/i18n/server";
|
|||||||
import { cn } from "@turbostarter/ui";
|
import { cn } from "@turbostarter/ui";
|
||||||
import { buttonVariants } from "@turbostarter/ui-web/button";
|
import { buttonVariants } from "@turbostarter/ui-web/button";
|
||||||
|
|
||||||
|
import { WhyRatingLogo } from "~/modules/common/whyrating-logo";
|
||||||
import { CtaButton } from "~/modules/marketing/layout/cta-button";
|
import { CtaButton } from "~/modules/marketing/layout/cta-button";
|
||||||
import { Section } from "~/modules/marketing/layout/section";
|
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 () => {
|
export const Banner = async () => {
|
||||||
const { t } = await getTranslation({ ns: "marketing" });
|
const { t } = await getTranslation({ ns: "marketing" });
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +13,12 @@ export const Banner = async () => {
|
|||||||
id="banner"
|
id="banner"
|
||||||
className="bg-primary text-primary-foreground !max-w-full gap-4 sm:gap-6 md:gap-8 lg:gap-10"
|
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">
|
<h3 className="text-3xl leading-[0.95] font-semibold tracking-tighter text-balance md:text-4xl lg:text-5xl">
|
||||||
{t("cta.question")}
|
{t("cta.question")}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
102
apps/web/src/modules/marketing/home/demo-report-data.ts
Normal file
102
apps/web/src/modules/marketing/home/demo-report-data.ts
Normal 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
|
||||||
|
};
|
||||||
@@ -28,14 +28,14 @@ const GaugeMockup = () => (
|
|||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
<path
|
<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"
|
stroke="#4285F4"
|
||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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) => (
|
{["#3b82f6", "#22c55e", "#f59e0b"].map((color) => (
|
||||||
<div
|
<div
|
||||||
key={color}
|
key={color}
|
||||||
@@ -85,22 +85,27 @@ export const HowItWorks = async () => {
|
|||||||
</SectionHeader>
|
</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">
|
<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) => {
|
{steps.map((step, index) => {
|
||||||
const Mockup = stepMockups[index]!;
|
const Mockup = stepMockups[index]!;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={step.key}
|
key={step.key}
|
||||||
className={cn(
|
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",
|
"transition-all hover:-translate-y-1 hover:shadow-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Numbered circle + connector */}
|
{/* Numbered circle */}
|
||||||
<div className="relative flex w-full items-center justify-center">
|
<div className="flex w-full items-center justify-center">
|
||||||
{index < steps.length - 1 && (
|
<div className="flex size-10 items-center justify-center rounded-full bg-[#4285F4] text-sm font-bold text-white">
|
||||||
<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">
|
|
||||||
{step.number}
|
{step.number}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
195
apps/web/src/modules/marketing/home/how-to-read.tsx
Normal file
195
apps/web/src/modules/marketing/home/how-to-read.tsx
Normal 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: "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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,9 +2,9 @@ import Image from "next/image";
|
|||||||
|
|
||||||
export const ReportFan = () => {
|
export const ReportFan = () => {
|
||||||
return (
|
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 */}
|
{/* 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/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 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>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,20 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "@turbostarter/i18n";
|
import { useTranslation } from "@turbostarter/i18n";
|
||||||
import { cn } from "@turbostarter/ui";
|
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 { CtaButton } from "~/modules/marketing/layout/cta-button";
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +26,11 @@ import {
|
|||||||
SectionHeader,
|
SectionHeader,
|
||||||
SectionTitle,
|
SectionTitle,
|
||||||
} from "~/modules/marketing/layout/section";
|
} 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")
|
// Demo data (hardcoded for "Bistro El Sol")
|
||||||
@@ -44,14 +63,27 @@ const SEVERITY_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EFFORT_COLORS: Record<string, string> = {
|
const EFFORT_COLORS: Record<string, string> = {
|
||||||
|
low: "#22c55e",
|
||||||
|
medium: "#f59e0b",
|
||||||
|
high: "#ef4444",
|
||||||
Low: "#22c55e",
|
Low: "#22c55e",
|
||||||
Medium: "#f59e0b",
|
Medium: "#f59e0b",
|
||||||
High: "#ef4444",
|
High: "#ef4444",
|
||||||
|
Bajo: "#22c55e",
|
||||||
|
Medio: "#f59e0b",
|
||||||
|
Alto: "#ef4444",
|
||||||
};
|
};
|
||||||
|
|
||||||
const IMPACT_COLORS: Record<string, string> = {
|
const IMPACT_COLORS: Record<string, string> = {
|
||||||
|
low: "#6b7280",
|
||||||
|
medium: "#f59e0b",
|
||||||
|
high: "#22c55e",
|
||||||
Low: "#6b7280",
|
Low: "#6b7280",
|
||||||
Medium: "#f59e0b",
|
Medium: "#f59e0b",
|
||||||
High: "#22c55e",
|
High: "#22c55e",
|
||||||
|
Bajo: "#6b7280",
|
||||||
|
Medio: "#f59e0b",
|
||||||
|
Alto: "#22c55e",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DOMAIN_COLORS: Record<string, string> = {
|
const DOMAIN_COLORS: Record<string, string> = {
|
||||||
@@ -60,7 +92,7 @@ const DOMAIN_COLORS: Record<string, string> = {
|
|||||||
Environment: "#8b5cf6",
|
Environment: "#8b5cf6",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TABS = ["score", "domains", "issues", "actions"] as const;
|
const TABS = ["score", "domains", "issues", "actions", "themes"] as const;
|
||||||
type Tab = (typeof TABS)[number];
|
type Tab = (typeof TABS)[number];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -79,16 +111,16 @@ function ScoreTab() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start sm:gap-10">
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start sm:gap-10">
|
||||||
{/* Left: Gauge + band */}
|
{/* 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">
|
<span className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||||
{t("reportPreview.demo.businessName")}
|
{t("reportPreview.demo.businessName")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="relative" style={{ width: 200, height: 120 }}>
|
<div className="relative" style={{ width: 200, height: 130 }}>
|
||||||
<svg
|
<svg
|
||||||
width="200"
|
width="200"
|
||||||
height="120"
|
height="130"
|
||||||
viewBox="0 0 200 120"
|
viewBox="0 0 200 130"
|
||||||
className="overflow-visible"
|
className="overflow-visible"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
@@ -118,7 +150,7 @@ function ScoreTab() {
|
|||||||
className="transition-all duration-1000 ease-out"
|
className="transition-all duration-1000 ease-out"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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
|
<span
|
||||||
className="text-6xl font-bold tabular-nums"
|
className="text-6xl font-bold tabular-nums"
|
||||||
style={{ color: SCORE_COLOR }}
|
style={{ color: SCORE_COLOR }}
|
||||||
@@ -139,7 +171,7 @@ function ScoreTab() {
|
|||||||
<div style={{ width: "20%", backgroundColor: "#22c55e" }} />
|
<div style={{ width: "20%", backgroundColor: "#22c55e" }} />
|
||||||
<div style={{ width: "40%", backgroundColor: "#059669" }} />
|
<div style={{ width: "40%", backgroundColor: "#059669" }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-2.5">
|
<div className="relative h-3">
|
||||||
<div
|
<div
|
||||||
className="absolute top-0"
|
className="absolute top-0"
|
||||||
style={{
|
style={{
|
||||||
@@ -153,6 +185,14 @@ function ScoreTab() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -292,59 +332,84 @@ function IssuesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Actions Tab
|
// Actions Tab (enhanced with real report data)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function ActionsTab() {
|
function ActionsTab() {
|
||||||
const { t } = useTranslation("marketing");
|
const { t } = useTranslation("marketing");
|
||||||
|
|
||||||
const actions = [
|
// Build mention count map from demo themes
|
||||||
{ key: "action1" as const, idx: 0 },
|
const mentionMap = new Map<string, number>();
|
||||||
{ key: "action2" as const, idx: 1 },
|
for (const theme of DEMO_THEMES) {
|
||||||
{ key: "action3" as const, idx: 2 },
|
mentionMap.set(theme.primitive, theme.count);
|
||||||
];
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{actions.map(({ key: actionKey, idx }) => {
|
{DEMO_ACTIONS.map((action, idx) => {
|
||||||
const effort = t(`reportPreview.demo.actions.${actionKey}.effort` as const);
|
const effortColor = EFFORT_COLORS[action.effort] ?? "#f59e0b";
|
||||||
const impact = t(`reportPreview.demo.actions.${actionKey}.impact` as const);
|
const impactColor = IMPACT_COLORS[action.impact] ?? "#f59e0b";
|
||||||
|
const mentions = mentionMap.get(action.source);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={actionKey}
|
key={idx}
|
||||||
className="bg-muted/20 flex gap-3 rounded-lg border p-3"
|
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">
|
<div className="flex gap-3 p-3">
|
||||||
{idx + 1}
|
<div className="bg-primary/10 text-primary flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-bold">
|
||||||
</div>
|
{idx + 1}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-foreground text-sm font-semibold leading-tight">
|
<p className="text-foreground text-sm font-semibold leading-tight">
|
||||||
{t(`reportPreview.demo.actions.${actionKey}.title` as const)}
|
{action.action}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span className="text-muted-foreground text-xs">
|
||||||
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
|
{action.owner}
|
||||||
style={{
|
</span>
|
||||||
backgroundColor: `${EFFORT_COLORS[effort] ?? "#f59e0b"}18`,
|
{mentions != null && (
|
||||||
color: EFFORT_COLORS[effort] ?? "#f59e0b",
|
<span className="text-xs font-medium" style={{ color: "#4285F4" }}>
|
||||||
}}
|
{mentions} {t("reportPreview.demo.themes.mentions" as const)}
|
||||||
>
|
</span>
|
||||||
{effort} effort
|
)}
|
||||||
</span>
|
<span className="text-muted-foreground text-xs">
|
||||||
<span
|
{action.timeline}
|
||||||
className="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold"
|
</span>
|
||||||
style={{
|
</div>
|
||||||
backgroundColor: `${IMPACT_COLORS[impact] ?? "#f59e0b"}18`,
|
|
||||||
color: IMPACT_COLORS[impact] ?? "#f59e0b",
|
<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"
|
||||||
{impact} impact
|
style={{
|
||||||
</span>
|
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>
|
||||||
</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>
|
</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
|
// Main component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -372,7 +580,7 @@ export const ReportPreview = () => {
|
|||||||
|
|
||||||
<div className="relative w-full max-w-3xl">
|
<div className="relative w-full max-w-3xl">
|
||||||
{/* Ambient glow */}
|
{/* 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/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 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>
|
</div>
|
||||||
@@ -408,6 +616,7 @@ export const ReportPreview = () => {
|
|||||||
{activeTab === "domains" && <DomainsTab />}
|
{activeTab === "domains" && <DomainsTab />}
|
||||||
{activeTab === "issues" && <IssuesTab />}
|
{activeTab === "issues" && <IssuesTab />}
|
||||||
{activeTab === "actions" && <ActionsTab />}
|
{activeTab === "actions" && <ActionsTab />}
|
||||||
|
{activeTab === "themes" && <ThemesTab />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const Testimonials = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SectionHeader>
|
</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) => (
|
{rows.map((row, index) => (
|
||||||
<Marquee
|
<Marquee
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"receive": {
|
"receive": {
|
||||||
"title": "Get your Blueprint",
|
"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": {
|
"reportPreview": {
|
||||||
"label": "See Your Report",
|
"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.",
|
"description": "A real preview of the Reputation Blueprint — the exact analysis your business receives.",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"issues": "Issues",
|
"issues": "Issues",
|
||||||
"actions": "Actions"
|
"actions": "Actions",
|
||||||
|
"themes": "Themes"
|
||||||
},
|
},
|
||||||
"cta": "Get yours for your business",
|
"cta": "Get yours",
|
||||||
"demo": {
|
"demo": {
|
||||||
"businessName": "Bistro El Sol",
|
"businessName": "Bistro El Sol",
|
||||||
"scoreLabel": "Reputation Score",
|
"scoreLabel": "Reputation Score",
|
||||||
@@ -125,9 +126,50 @@
|
|||||||
"effort": "Medium",
|
"effort": "Medium",
|
||||||
"impact": "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 0–100 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": {
|
"reviewEvidence": {
|
||||||
"label": "Deep Review Analysis",
|
"label": "Deep Review Analysis",
|
||||||
"title": "We read what you can't",
|
"title": "We read what you can't",
|
||||||
@@ -202,11 +244,11 @@
|
|||||||
},
|
},
|
||||||
"howScoringWorks": {
|
"howScoringWorks": {
|
||||||
"question": "How does the 0–100 scoring work?",
|
"question": "How does the 0–100 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": {
|
"deliveryTime": {
|
||||||
"question": "How long does it take to get my report?",
|
"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": {
|
"whatBusinessTypes": {
|
||||||
"question": "What types of businesses can use this?",
|
"question": "What types of businesses can use this?",
|
||||||
@@ -222,7 +264,7 @@
|
|||||||
},
|
},
|
||||||
"reportContents": {
|
"reportContents": {
|
||||||
"question": "What's included in the Reputation Blueprint?",
|
"question": "What's included in the Reputation Blueprint?",
|
||||||
"answer": "Your Blueprint includes: a 0–100 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 0–100 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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"receive": {
|
"receive": {
|
||||||
"title": "Recibe tu Radiografía",
|
"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": {
|
"reportPreview": {
|
||||||
"label": "Ve Tu Informe",
|
"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.",
|
"description": "Una vista real de la Radiografía de Reputación — el análisis exacto que recibe tu negocio.",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"score": "Puntuación",
|
"score": "Puntuación",
|
||||||
"domains": "Dominios",
|
"domains": "Dominios",
|
||||||
"issues": "Problemas",
|
"issues": "Problemas",
|
||||||
"actions": "Acciones"
|
"actions": "Acciones",
|
||||||
|
"themes": "Temas"
|
||||||
},
|
},
|
||||||
"cta": "Obtén el tuyo para tu negocio",
|
"cta": "Obtén el tuyo",
|
||||||
"demo": {
|
"demo": {
|
||||||
"businessName": "Bistro El Sol",
|
"businessName": "Bistro El Sol",
|
||||||
"scoreLabel": "Puntuación de Reputación",
|
"scoreLabel": "Puntuación de Reputación",
|
||||||
@@ -125,9 +126,50 @@
|
|||||||
"effort": "Medio",
|
"effort": "Medio",
|
||||||
"impact": "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": {
|
"reviewEvidence": {
|
||||||
"label": "Análisis Profundo",
|
"label": "Análisis Profundo",
|
||||||
"title": "Leemos lo que tú no puedes",
|
"title": "Leemos lo que tú no puedes",
|
||||||
@@ -202,11 +244,11 @@
|
|||||||
},
|
},
|
||||||
"howScoringWorks": {
|
"howScoringWorks": {
|
||||||
"question": "¿Cómo funciona la puntuación de 0 a 100?",
|
"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": {
|
"deliveryTime": {
|
||||||
"question": "¿Cuánto tiempo tarda en llegar mi informe?",
|
"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": {
|
"whatBusinessTypes": {
|
||||||
"question": "¿Qué tipos de negocios pueden usar esto?",
|
"question": "¿Qué tipos de negocios pueden usar esto?",
|
||||||
@@ -222,7 +264,7 @@
|
|||||||
},
|
},
|
||||||
"reportContents": {
|
"reportContents": {
|
||||||
"question": "¿Qué incluye la Radiografía de Reputación?",
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user