fix: add missing i18n keys for wizard success states
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useTranslation } from "@turbostarter/i18n";
|
import { useTranslation } from "@turbostarter/i18n";
|
||||||
import { cn } from "@turbostarter/ui";
|
import { cn } from "@turbostarter/ui";
|
||||||
import { buttonVariants } from "@turbostarter/ui-web/button";
|
import { buttonVariants } from "@turbostarter/ui-web/button";
|
||||||
@@ -8,51 +10,216 @@ interface StepSuccessProps {
|
|||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OrderStatus {
|
||||||
|
orderId: string;
|
||||||
|
status: "pending" | "processing" | "completed" | "failed";
|
||||||
|
businessName: string;
|
||||||
|
reputationScore: number | null;
|
||||||
|
reviewsCount: number | null;
|
||||||
|
pdfUrl: string | null;
|
||||||
|
executionId: string | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STAGES = [
|
||||||
|
{ key: "scrape", label: "Collecting reviews" },
|
||||||
|
{ key: "classify", label: "Analyzing sentiment" },
|
||||||
|
{ key: "synthesize", label: "Generating report" },
|
||||||
|
{ key: "done", label: "Complete" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function getStageIndex(status: string): number {
|
||||||
|
if (status === "pending") return 0;
|
||||||
|
if (status === "processing") return 1;
|
||||||
|
if (status === "completed") return 3;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
export const StepSuccess = ({ email }: StepSuccessProps) => {
|
export const StepSuccess = ({ email }: StepSuccessProps) => {
|
||||||
const { t } = useTranslation("marketing");
|
const { t } = useTranslation("marketing");
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const sessionId = searchParams.get("session_id");
|
||||||
|
|
||||||
|
const [orderStatus, setOrderStatus] = useState<OrderStatus | null>(null);
|
||||||
|
const [polling, setPolling] = useState(true);
|
||||||
|
|
||||||
|
const pollStatus = useCallback(async () => {
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/blueprint/status/${sessionId}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = (await res.json()) as OrderStatus;
|
||||||
|
setOrderStatus(data);
|
||||||
|
|
||||||
|
if (data.status === "completed" || data.status === "failed") {
|
||||||
|
setPolling(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently retry on next interval
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
pollStatus();
|
||||||
|
|
||||||
|
if (!polling) return;
|
||||||
|
|
||||||
|
const interval = setInterval(pollStatus, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [pollStatus, polling]);
|
||||||
|
|
||||||
|
const isCompleted = orderStatus?.status === "completed";
|
||||||
|
const isFailed = orderStatus?.status === "failed";
|
||||||
|
const stageIndex = orderStatus ? getStageIndex(orderStatus.status) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-lg flex-col items-center gap-8">
|
<div className="flex w-full max-w-lg flex-col items-center gap-8">
|
||||||
{/* Success icon */}
|
{/* Header icon */}
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-green-500/10">
|
<div
|
||||||
<svg
|
className={cn(
|
||||||
className="size-8 text-green-500"
|
"flex size-16 items-center justify-center rounded-full",
|
||||||
fill="none"
|
isCompleted
|
||||||
viewBox="0 0 24 24"
|
? "bg-green-500/10"
|
||||||
stroke="currentColor"
|
: isFailed
|
||||||
strokeWidth={2}
|
? "bg-red-500/10"
|
||||||
>
|
: "bg-blue-500/10",
|
||||||
<path
|
)}
|
||||||
strokeLinecap="round"
|
>
|
||||||
strokeLinejoin="round"
|
{isCompleted ? (
|
||||||
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
<svg
|
||||||
/>
|
className="size-8 text-green-500"
|
||||||
</svg>
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : isFailed ? (
|
||||||
|
<svg
|
||||||
|
className="size-8 text-red-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<div className="border-primary size-8 animate-spin rounded-full border-3 border-t-transparent" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">
|
<h2 className="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||||
{t("wizard.success.title")}
|
{isCompleted
|
||||||
|
? t("wizard.success.readyTitle")
|
||||||
|
: isFailed
|
||||||
|
? t("wizard.success.failedTitle")
|
||||||
|
: t("wizard.success.title")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
{t("wizard.success.description", {
|
{isCompleted
|
||||||
email: email || "your email",
|
? t("wizard.success.readyDescription", {
|
||||||
})}
|
email: email || "your email",
|
||||||
|
})
|
||||||
|
: isFailed
|
||||||
|
? orderStatus?.error || "We encountered an error processing your report."
|
||||||
|
: t("wizard.success.description", {
|
||||||
|
email: email || "your email",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Processing indicator */}
|
{/* Score highlight (when complete) */}
|
||||||
<div className="bg-background w-full rounded-xl border p-6">
|
{isCompleted && orderStatus?.reputationScore && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="bg-background flex w-full flex-col items-center rounded-xl border p-6">
|
||||||
<div className="border-primary size-5 animate-spin rounded-full border-2 border-t-transparent" />
|
<span className="text-muted-foreground text-sm">Reputation Score</span>
|
||||||
<span className="text-foreground text-sm font-medium">
|
<span className="text-foreground mt-1 text-5xl font-bold">
|
||||||
{t("wizard.success.processing")}
|
{orderStatus.reputationScore}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-muted-foreground text-xs">/100</span>
|
||||||
|
{orderStatus.reviewsCount && (
|
||||||
|
<span className="text-muted-foreground mt-2 text-xs">
|
||||||
|
Based on {orderStatus.reviewsCount} reviews
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground mt-3 text-xs">
|
)}
|
||||||
{t("wizard.success.delivery")}
|
|
||||||
</p>
|
{/* Progress stages (when processing) */}
|
||||||
</div>
|
{!isCompleted && !isFailed && (
|
||||||
|
<div className="bg-background w-full rounded-xl border p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{STAGES.map((stage, i) => {
|
||||||
|
const isActive = i === stageIndex || (i === 1 && stageIndex < 3);
|
||||||
|
const isDone = i < stageIndex || (isCompleted && i <= 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={stage.key} className="flex items-center gap-3">
|
||||||
|
{isDone ? (
|
||||||
|
<svg
|
||||||
|
className="size-5 text-green-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9 12.75 11.25 15 15 9.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : isActive ? (
|
||||||
|
<div className="border-primary size-5 animate-spin rounded-full border-2 border-t-transparent" />
|
||||||
|
) : (
|
||||||
|
<div className="bg-muted size-5 rounded-full" />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
isDone
|
||||||
|
? "text-muted-foreground line-through"
|
||||||
|
: isActive
|
||||||
|
? "text-foreground font-medium"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{stage.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mt-4 text-xs">
|
||||||
|
{t("wizard.success.delivery")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Download / Dashboard CTAs (when complete) */}
|
||||||
|
{isCompleted && orderStatus?.pdfUrl && (
|
||||||
|
<div className="flex w-full flex-col gap-3">
|
||||||
|
<a
|
||||||
|
href={orderStatus.pdfUrl}
|
||||||
|
className={cn(buttonVariants({ size: "lg" }), "w-full text-center")}
|
||||||
|
>
|
||||||
|
Download Report (PDF)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Create account section */}
|
{/* Create account section */}
|
||||||
<div className="bg-background w-full rounded-xl border p-6">
|
<div className="bg-background w-full rounded-xl border p-6">
|
||||||
|
|||||||
@@ -440,6 +440,9 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"title": "Your Blueprint is on the way!",
|
"title": "Your Blueprint is on the way!",
|
||||||
"description": "We're analyzing your reviews now. Your Reputation Blueprint will be delivered to {{email}} within 24 hours.",
|
"description": "We're analyzing your reviews now. Your Reputation Blueprint will be delivered to {{email}} within 24 hours.",
|
||||||
|
"readyTitle": "Your Report is Ready!",
|
||||||
|
"readyDescription": "We've sent the report to {{email}}",
|
||||||
|
"failedTitle": "Something went wrong",
|
||||||
"processing": "Analyzing reviews...",
|
"processing": "Analyzing reviews...",
|
||||||
"createAccount": "Create an account",
|
"createAccount": "Create an account",
|
||||||
"createAccountDescription": "Track your report status and download it when ready.",
|
"createAccountDescription": "Track your report status and download it when ready.",
|
||||||
|
|||||||
@@ -440,6 +440,9 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"title": "¡Tu Radiografía está en camino!",
|
"title": "¡Tu Radiografía está en camino!",
|
||||||
"description": "Estamos analizando tus reseñas. Tu Radiografía de Reputación se entregará a {{email}} en 24 horas.",
|
"description": "Estamos analizando tus reseñas. Tu Radiografía de Reputación se entregará a {{email}} en 24 horas.",
|
||||||
|
"readyTitle": "¡Tu Informe está Listo!",
|
||||||
|
"readyDescription": "Hemos enviado el informe a {{email}}",
|
||||||
|
"failedTitle": "Algo salió mal",
|
||||||
"processing": "Analizando reseñas...",
|
"processing": "Analizando reseñas...",
|
||||||
"createAccount": "Crear una cuenta",
|
"createAccount": "Crear una cuenta",
|
||||||
"createAccountDescription": "Sigue el estado de tu informe y descárgalo cuando esté listo.",
|
"createAccountDescription": "Sigue el estado de tu informe y descárgalo cuando esté listo.",
|
||||||
|
|||||||
Reference in New Issue
Block a user