- Replaced oklch color definitions with color-mix for improved gradient consistency across OnsiteSupportContent, HeroSection, and ServicesOverviewContent components. - Refactored ref attributes in multiple sections to remove unnecessary type assertions for better type safety. - Enhanced ServicesCarousel component by introducing a new accent color system for better visual clarity and consistency.
9.8 KiB
TrustStrip Redesign — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Replace the minimal icon+text trust strip with a bold stats section featuring large animated count-up numbers, icon circles, vertical dividers, and a gradient background.
Architecture: 3-file change — new useCountUp hook for the animation, rewritten TrustStrip.tsx component, barrel file update. Uses existing useInView hook and codebase patterns (cn utility, Tailwind, lucide-react). No new dependencies.
Tech Stack: React 19, Tailwind CSS, lucide-react, existing useInView hook
Task 1: Create the useCountUp hook
Files:
- Create:
apps/portal/src/features/landing-page/hooks/useCountUp.ts
Step 1: Write the hook
Create apps/portal/src/features/landing-page/hooks/useCountUp.ts:
import { useEffect, useState } from "react";
/**
* useCountUp — Animates a number from 0 to target over a duration.
* Respects prefers-reduced-motion. Only runs when enabled is true.
*/
export function useCountUp(target: number, duration = 1500, enabled = false): number {
const [value, setValue] = useState(0);
useEffect(() => {
if (!enabled) return;
// Respect prefers-reduced-motion
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReduced) {
setValue(target);
return;
}
let startTime: number | null = null;
let rafId: number;
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease-out cubic for smooth deceleration
const eased = 1 - Math.pow(1 - progress, 3);
setValue(Math.round(eased * target));
if (progress < 1) {
rafId = requestAnimationFrame(animate);
}
};
rafId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(rafId);
}, [target, duration, enabled]);
return value;
}
Step 2: Export from barrel file
Modify apps/portal/src/features/landing-page/hooks/index.ts — add this line:
export { useCountUp } from "./useCountUp";
Step 3: Verify it builds
Run: pnpm type-check
Expected: No errors
Step 4: Commit
git add apps/portal/src/features/landing-page/hooks/useCountUp.ts apps/portal/src/features/landing-page/hooks/index.ts
git commit -m "feat: add useCountUp hook for animated number transitions"
Task 2: Rewrite TrustStrip component
Files:
- Modify:
apps/portal/src/features/landing-page/components/TrustStrip.tsx(complete rewrite)
Step 1: Replace TrustStrip.tsx with new implementation
Rewrite apps/portal/src/features/landing-page/components/TrustStrip.tsx:
"use client";
import { Clock, CreditCard, Globe, Users } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/shared/utils";
import { useInView, useCountUp } from "@/features/landing-page/hooks";
interface StatItem {
icon: LucideIcon;
value: number;
suffix: string;
label: string;
delay: number;
formatter?: (n: number) => string;
}
const stats: StatItem[] = [
{ icon: Clock, value: 20, suffix: "+", label: "Years in Japan", delay: 0 },
{ icon: Globe, value: 100, suffix: "%", label: "English Support", delay: 100 },
{
icon: Users,
value: 10000,
suffix: "+",
label: "Customers Served",
delay: 200,
formatter: (n: number) => n.toLocaleString(),
},
];
function AnimatedStat({ stat, inView }: { stat: StatItem; inView: boolean }) {
const count = useCountUp(stat.value, 1500, inView);
return (
<span className="text-3xl sm:text-4xl font-extrabold text-primary tabular-nums">
{stat.formatter ? stat.formatter(count) : count}
{stat.suffix}
</span>
);
}
export function TrustStrip() {
const [ref, inView] = useInView();
return (
<section
ref={ref as React.RefObject<HTMLElement>}
aria-label="Company statistics"
className={cn(
"relative left-1/2 right-1/2 w-screen -translate-x-1/2 py-10 sm:py-12 overflow-hidden transition-all duration-700",
inView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
)}
>
{/* Gradient background */}
<div className="absolute inset-0 bg-gradient-to-r from-surface-sunken via-background to-info-bg/30" />
<div className="relative mx-auto max-w-6xl px-6 sm:px-10 lg:px-14">
<div className="grid grid-cols-2 gap-8 sm:flex sm:justify-between sm:items-center">
{/* Animated stats */}
{stats.map((stat, i) => (
<div
key={stat.label}
className={cn(
"flex flex-col items-center text-center gap-3 sm:flex-1",
i < stats.length - 1 && "sm:border-r sm:border-border/30"
)}
>
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10">
<stat.icon className="h-5 w-5 text-primary" />
</div>
<div className="flex flex-col items-center gap-1">
<AnimatedStat stat={stat} inView={inView} />
<span className="text-sm text-muted-foreground font-medium">{stat.label}</span>
</div>
</div>
))}
{/* Static stat — Foreign Cards */}
<div className={cn("flex flex-col items-center text-center gap-3 sm:flex-1")}>
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10">
<CreditCard className="h-5 w-5 text-primary" />
</div>
<div className="flex flex-col items-center gap-1">
<span className="text-xl sm:text-2xl font-extrabold text-primary">Foreign Cards</span>
<span className="text-sm text-muted-foreground font-medium">Accepted</span>
</div>
</div>
</div>
</div>
</section>
);
}
Key decisions:
AnimatedStatis a separate component so each stat has its ownuseCountUpinstance- The 4th stat (Foreign Cards) is rendered separately since it has no numeric animation
- Desktop: flex row with
border-rdividers on first 3 items. Mobile: 2-col grid tabular-numsprevents layout jitter during count-up animationformatteron the 10,000 stat adds comma separators
Step 2: Verify it builds
Run: pnpm type-check
Expected: No errors
Step 3: Visual check
Open localhost:3000 and verify:
- 4 stats displayed in a row on desktop, 2x2 grid on mobile
- Numbers animate from 0 to target when scrolling into view
- Vertical dividers between first 3 stats on desktop
- "Foreign Cards / Accepted" shows as static bold text
- Gradient background blends with hero above
- Each stat has a primary-tinted circular icon above it
Step 4: Commit
git add apps/portal/src/features/landing-page/components/TrustStrip.tsx
git commit -m "feat: redesign TrustStrip with bold animated stats"
Task 3: Stagger the count-up animations
The current implementation starts all counters simultaneously. Add stagger delays.
Files:
- Modify:
apps/portal/src/features/landing-page/hooks/useCountUp.ts - Modify:
apps/portal/src/features/landing-page/components/TrustStrip.tsx
Step 1: Add delay parameter to useCountUp
In useCountUp.ts, change the signature and add a delay before animation starts:
export function useCountUp(target: number, duration = 1500, enabled = false, delay = 0): number {
Update the effect body — wrap the animation start in a setTimeout:
useEffect(() => {
if (!enabled) return;
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReduced) {
setValue(target);
return;
}
let startTime: number | null = null;
let rafId: number;
let timeoutId: ReturnType<typeof setTimeout>;
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setValue(Math.round(eased * target));
if (progress < 1) {
rafId = requestAnimationFrame(animate);
}
};
timeoutId = setTimeout(() => {
rafId = requestAnimationFrame(animate);
}, delay);
return () => {
clearTimeout(timeoutId);
cancelAnimationFrame(rafId);
};
}, [target, duration, enabled, delay]);
Step 2: Pass delay from TrustStrip
In AnimatedStat, update the useCountUp call:
function AnimatedStat({ stat, inView }: { stat: StatItem; inView: boolean }) {
const count = useCountUp(stat.value, 1500, inView, stat.delay);
Step 3: Verify it builds
Run: pnpm type-check
Expected: No errors
Step 4: Visual check
Open localhost:3000, scroll to trust strip. The 3 animated numbers should start counting up in sequence: "20+" first, "100%" ~100ms later, "10,000+" ~200ms later. The stagger should feel subtle but add polish.
Step 5: Commit
git add apps/portal/src/features/landing-page/hooks/useCountUp.ts apps/portal/src/features/landing-page/components/TrustStrip.tsx
git commit -m "feat: add staggered delay to count-up animations"
Task 4: Final verification
Step 1: Run type check and lint
Run: pnpm type-check
Expected: No errors
Step 2: Visual smoke test
Check localhost:3000 at:
- Desktop (1280px): 4 stats in a row, dividers, gradient bg, count-up animation
- Tablet (768px): same row, slightly smaller numbers
- Mobile (375px): 2x2 grid, no dividers, animation still works
Step 3: Accessibility check
- Tab through the page — stats section should be announced as "Company statistics"
- Set
prefers-reduced-motion: reducein browser DevTools — numbers should show final values immediately without animation