Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/[locale]/cases/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EditCaseDetails, type EditCaseInitial } from '@/components/case/EditCas
import { DeadlineRemindersCard } from '@/components/case/DeadlineRemindersCard';
import { SaveCaseCard } from '@/components/case/SaveCaseCard';
import { DataRightsCard } from '@/components/case/DataRightsCard';
import { OutcomeCaptureCard } from '@/components/case/OutcomeCaptureCard';
import { AuthorityActionsCard } from '@/components/case/AuthorityActionsCard';
import { PackageStatusCard } from '@/components/case/PackageStatusCard';
import {
Expand Down Expand Up @@ -56,6 +57,7 @@ type CaseDetailData =
hasSealedBundle: boolean;
microUpiPattern: boolean;
directOwner: boolean;
latestOutcome: 'unfrozen' | 'partially_unfrozen' | 'response_received' | 'still_frozen' | null;
};

async function loadCaseDetailData(caseId: string, guestToken: string | undefined): Promise<CaseDetailData> {
Expand Down Expand Up @@ -90,6 +92,7 @@ async function loadCaseDetailData(caseId: string, guestToken: string | undefined
{ data: evidenceRows },
{ data: escalationRows },
{ data: bundleLog },
{ data: outcomeRows },
] = await Promise.all([
admin
.from('cases')
Expand All @@ -116,6 +119,13 @@ async function loadCaseDetailData(caseId: string, guestToken: string | undefined
.eq('action', 'evidence.bundle_generated')
.limit(1)
.maybeSingle(),
// Latest self-reported outcome (post-letter follow-through).
admin
.from('case_outcomes')
.select('outcome')
.eq('case_id', caseId)
.order('reported_at', { ascending: false })
.limit(1),
]);

const intake = (caseRow?.intake_json ?? {}) as Record<string, unknown>;
Expand Down Expand Up @@ -184,6 +194,13 @@ async function loadCaseDetailData(caseId: string, guestToken: string | undefined
: '') ||
(typeof intake.freeze_date === 'string' ? intake.freeze_date : ''),
hasSealedBundle: Boolean(bundleLog),
latestOutcome:
(outcomeRows?.[0]?.outcome as
| 'unfrozen'
| 'partially_unfrozen'
| 'response_received'
| 'still_frozen'
| undefined) ?? null,
microUpiPattern:
intake.micro_upi_pattern === true ||
(typeof intake.amount_inr === 'number' && intake.amount_inr > 0 && intake.amount_inr <= 5000) ||
Expand Down Expand Up @@ -352,6 +369,12 @@ export default async function CaseDetailPage({ params }: PageProps) {

{data.l1Sent ? <WhatHappensNextCard /> : null}

{/* Post-letter follow-through: one-tap outcome logging once a letter
exists. This is where the funnel used to end — keep it effortless. */}
{data.l1Sent || data.letters.some((l) => l.hasDraft) ? (
<OutcomeCaptureCard caseId={id} initialOutcome={data.latestOutcome} />
) : null}

{/* Optional reminders for the user's own follow-up dates. */}
<DeadlineRemindersCard
caseId={id}
Expand Down
13 changes: 9 additions & 4 deletions app/[locale]/demo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default async function DemoPage({ params }: Props) {
const path = getUnfreezePath("cyber_upi_chain", locale);

return (
<div className="mx-auto flex max-w-[430px] flex-col gap-4">
<div className="mx-auto flex max-w-[430px] flex-col gap-4 lg:max-w-2xl">
{/* Lead with the killer insight in plain words, not a caveat — all three
comprehension reviewers (HRs + a normal user) said the value was buried
under a legal briefing. State it first; let the cards below be the proof. */}
Expand Down Expand Up @@ -137,8 +137,13 @@ export default async function DemoPage({ params }: Props) {
frozenAmountInr="1,800"
/>

<section className="u-card p-4">
<p className="type-eyebrow">{t("sourcesTitle")}</p>
{/* Reference material — collapsed by default so the demo leads with the
usable output instead of a wall of legal text. All content preserved. */}
<details className="u-card p-4">
<summary className="type-eyebrow flex cursor-pointer list-none items-center justify-between">
{t("sourcesTitle")}
<span aria-hidden="true" className="text-[var(--ink-faint)]">+</span>
</summary>
<p className="mt-2 text-[0.8125rem] leading-relaxed text-[var(--ink-muted)]">
{t("sourcesDesc")}
</p>
Expand All @@ -152,7 +157,7 @@ export default async function DemoPage({ params }: Props) {
<div className="mt-3">
<LegalPositionNote position={DISPUTED_AMOUNT_RULE} />
</div>
</section>
</details>

<section className="u-hero animate-fade-up px-5 py-6">
<div className="relative z-[1]">
Expand Down
95 changes: 95 additions & 0 deletions app/[locale]/impact/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { StatCard } from "@/components/metrics/StatCard";
import { getPublicStats, type PublicStats } from "@/lib/metrics/public-stats";

type Props = { params: Promise<{ locale: string }> };

// Always render fresh so the counts are live (and never cache a build-time
// empty state). The page issues four cheap COUNT queries — negligible load.
export const dynamic = "force-dynamic";

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "ImpactPage" });
return { title: t("title"), description: t("intro") };
}

export default async function ImpactPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("ImpactPage");

let stats: PublicStats | null = null;
try {
stats = await getPublicStats();
} catch {
stats = null;
}

const fmt = (n: number) => n.toLocaleString("en-IN");

return (
<div className="mx-auto flex max-w-[430px] flex-col gap-4 lg:max-w-3xl">
<div className="animate-fade-up">
<p className="type-eyebrow">{t("eyebrow")}</p>
<h1 className="type-display-xl mt-1.5 text-[1.75rem] leading-tight">
{t("title")}
</h1>
<p className="mt-2.5 text-[0.9375rem] leading-relaxed text-[var(--ink-muted)]">
{t("intro")}
</p>
</div>

{!stats || stats.casesStarted === 0 ? (
<section className="u-card animate-fade-up p-5">
<p className="type-display text-[1.05rem]">{t("emptyTitle")}</p>
<p className="mt-1.5 text-[0.875rem] leading-relaxed text-[var(--ink-muted)]">
{t("emptyBody")}
</p>
<Link
href="/start"
className="u-btn u-btn-primary mt-4 flex min-h-[48px] w-full text-base font-semibold"
>
{t("startCta")}
</Link>
</section>
) : (
<>
<section className="grid grid-cols-2 gap-3">
<StatCard label={t("usersLabel")} value={fmt(stats.users)} />
<StatCard label={t("casesLabel")} value={fmt(stats.casesStarted)} />
<StatCard
label={t("lettersGeneratedLabel")}
value={fmt(stats.lettersGenerated)}
/>
<StatCard
label={t("lettersSentLabel")}
value={fmt(stats.lettersSent)}
/>
<StatCard
label={t("unfrozenLabel")}
value={fmt(stats.unfrozenReported)}
sub={t("unfrozenNote")}
/>
</section>
<p className="type-caption text-[var(--ink-faint)]">
{t("updatedPrefix")}{" "}
{new Date(stats.generatedAt).toLocaleString("en-IN")}
</p>
<Link
href="/start"
className="u-btn u-btn-primary mt-1 flex min-h-[48px] w-full text-base font-semibold lg:w-auto lg:self-start lg:px-6"
>
{t("startCta")}
</Link>
</>
)}

<p className="mt-1 text-[0.6875rem] leading-relaxed text-[var(--ink-faint)]">
{t("disclaimer")}
</p>
</div>
);
}
2 changes: 2 additions & 0 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NextIntlClientProvider, hasLocale } from "next-intl";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import "../globals.css";
import { Analytics } from "@vercel/analytics/next";
import { SiteHeader } from "@/components/layout/SiteHeader";
import { BottomTabBar } from "@/components/layout/BottomTabBar";
import { Link } from "@/i18n/navigation";
Expand Down Expand Up @@ -204,6 +205,7 @@ export default async function LocaleLayout({
</p>
</div>
</footer>
<Analytics />
</NextIntlClientProvider>
</body>
</html>
Expand Down
49 changes: 48 additions & 1 deletion app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { JsonLd } from "@/components/seo/JsonLd";
import { TrackView } from "@/components/analytics/TrackView";

type Props = { params: Promise<{ locale: string }> };

Expand Down Expand Up @@ -111,8 +112,10 @@ export default async function HomePage({ params }: Props) {
];

return (
<div className="mx-auto flex max-w-[430px] flex-col gap-4">
<div className="mx-auto w-full max-w-[430px] lg:max-w-5xl">
<JsonLd data={jsonLd} />
<TrackView event="landing_view" />
<div className="flex flex-col gap-4 lg:grid lg:grid-cols-2 lg:items-start lg:gap-8">
<section className="u-hero animate-fade-up px-5 py-6">
<div className="relative z-[1]">
<p className="type-eyebrow">{t("eyebrow")}</p>
Expand Down Expand Up @@ -155,6 +158,49 @@ export default async function HomePage({ params }: Props) {
</div>
</section>

{/* Product preview (desktop): show the plan + a draft-letter excerpt so a
visitor sees the output before starting. Hidden on mobile, where the
worked-example block below already fills this role. */}
<aside className="hidden animate-fade-up stagger-1 lg:block">
<div className="u-card p-5">
<p className="type-eyebrow">{t("previewLabel")}</p>
<div className="mt-3 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--surface)] p-3">
<p className="text-[0.8125rem] font-bold text-[var(--ink)]">
{t("previewPlanTitle")}
</p>
<ol className="mt-2 flex flex-col gap-1.5">
{steps.map((step, i) => (
<li
key={i}
className="flex items-center gap-2 text-[0.78125rem] text-[var(--ink-muted)]"
>
<span className="flex h-5 w-5 flex-none items-center justify-center rounded-full bg-[var(--color-sky-mist)] text-[0.6875rem] font-bold text-[var(--color-sky-deep)]">
{i + 1}
</span>
{step.title}
</li>
))}
</ol>
</div>
<div className="mt-3 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--surface)] p-3">
<p className="text-[0.8125rem] font-bold text-[var(--ink)]">
{t("previewLetterTitle")}
</p>
<p className="type-mono-data mt-1.5 text-[0.75rem] leading-relaxed text-[var(--ink-faint)]">
{t("previewLetterExcerpt")}
</p>
</div>
<Link
href="/demo"
className="mt-3 inline-block text-[0.8125rem] font-semibold text-[var(--color-sky-deep)] no-underline"
>
{t("demoTitle")} →
</Link>
</div>
</aside>
</div>

<div className="mt-4 flex flex-col gap-4 lg:mx-auto lg:mt-8 lg:max-w-2xl">
{/* The one loud block on the page: a reviewer/judge/curious visitor has no
case or papers — this is the only thing they can click. Solid brand blue
so it can't be scrolled past, placed in the first viewport. */}
Expand Down Expand Up @@ -264,6 +310,7 @@ export default async function HomePage({ params }: Props) {
>
{t("rightsLink")}
</Link>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions app/[locale]/start/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter } from '@/i18n/navigation';
import { ConsentScreen } from '@/components/legal/ConsentScreen';
import { IntakeWizard, type IntakeWizardResult } from '@/components/intake/IntakeWizard';
import { SAVE_CASE_STORAGE_KEY } from '@/lib/case/save-case-storage';
import { track } from '@/lib/analytics/events';

/**
* The single intake entry point: consent (blocks B + F) → 5-question wizard →
Expand Down Expand Up @@ -70,6 +71,7 @@ export default function StartPage() {
// sessionStorage full / private mode — save page still shows public_id if present
}

track('intake_completed');
router.push(`/cases/${json.id}/save`);
} catch (err) {
setError(
Expand All @@ -89,6 +91,7 @@ export default function StartPage() {
onContinue={(aiConsentAccepted) => {
setAiConsent(aiConsentAccepted);
setPhase('questions');
track('case_started');
}}
/>
) : (
Expand Down
Loading
Loading