- Improved the useResponsiveCardWidth hook to prevent unnecessary state updates by tracking previous width. - Introduced a new useCarouselInput hook to handle touch and keyboard navigation, streamlining event management. - Updated the useInfiniteCarousel hook to utilize a ref for total items, enhancing performance and reliability. - Enhanced styling for the SimPlansContent component, including improved layout and hover effects for better user experience. - Added slide transition animations for tab changes in the PublicInternetPlans view, improving visual feedback during interactions. - Updated internet tier descriptions and features for clarity and consistency across the application.
12 KiB
Internet & SIM Service Page Redesign - Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Redesign the public internet page to use a unified card with offering selector, and improve SIM page tab transitions + plan card design.
Architecture: Two independent UI refactors. Internet page merges ConsolidatedInternetCard + AvailablePlansSection into one component with a segmented control for offering type. SIM page adds direction-aware slide transitions and richer plan cards.
Tech Stack: React 19, Tailwind CSS, shadcn/ui, lucide-react icons
Task 1: Internet - Build Unified Internet Card with Offering Selector
Files:
- Modify:
apps/portal/src/features/services/views/PublicInternetPlans.tsx
Context: This file currently has two separate sections:
ConsolidatedInternetCard(lines 59-209) - shows price range + 3 tier cardsAvailablePlansSection(lines 653-731) - 3 expandable offering headers that open tier details below- Helper components:
PlanCardHeader(266-340),ExpandedTierDetails(345-465),MobilePlanCard(470-648)
The goal is to merge these into ONE card with a segmented offering type selector.
Step 1: Replace ConsolidatedInternetCard with UnifiedInternetCard
Delete the ConsolidatedInternetCard component (lines 59-209) and replace with a new UnifiedInternetCard component that:
- Accepts both
consolidatedPlanData(for "All Plans" price ranges) ANDplansByOffering(for per-offering exact prices) - Has internal state:
selectedOffering: "all" | "home10g" | "home1g" | "apartment" - Header section:
- Same Wifi icon + "NTT Fiber Internet" title
- Price display that updates based on
selectedOffering:- "all": shows full min~max range
- specific offering: shows that offering's min~max (will often be exact price)
- Segmented control below header:
- Pills: "All Plans" | "Home 10G" | "Home 1G" | "Apartment"
- Active pill:
bg-card text-foreground shadow-sm - Inactive:
text-muted-foreground hover:text-foreground - Container:
bg-muted/60 p-0.5 rounded-lg border border-border/60(matches SIM tab style) - On mobile: horizontally scrollable with
overflow-x-auto - Each pill shows the offering icon (Home/Building) and optional speed badge
- Tier cards grid (same 3 columns: Silver, Gold, Platinum):
- When "All Plans": show price ranges per tier (current behavior from consolidated card)
- When specific offering selected: show exact prices for that offering's tiers
- Wrap the grid in a container with
transition-opacity duration-200for smooth price updates
- Footer: same setup fee + CTA button
Step 2: Remove obsolete components
Delete from PublicInternetPlans.tsx:
PlanCardHeadercomponent (lines 266-340)ExpandedTierDetailscomponent (lines 345-465)MobilePlanCardcomponent (lines 470-648)AvailablePlansSectioncomponent (lines 653-731)
Step 3: Update PublicInternetPlansContent render
In the PublicInternetPlansContent component's return JSX (starts line 961):
- Replace the
ConsolidatedInternetCardusage (lines 973-987) withUnifiedInternetCard - Pass it both
consolidatedPlanDataandplansByOffering - Remove the
AvailablePlansSectionusage (lines 989-995) entirely
Step 4: Verify and commit
cd /home/barsa/projects/customer_portal/customer-portal
pnpm type-check
pnpm lint
Expected: No type errors or lint issues.
git add apps/portal/src/features/services/views/PublicInternetPlans.tsx
git commit -m "feat: unified internet card with offering type selector"
Task 2: Internet - Clean Up Unused PublicOfferingCard
Files:
- Check:
apps/portal/src/features/services/components/internet/PublicOfferingCard.tsx
Context: After Task 1, the TierInfo type is still imported from PublicOfferingCard.tsx in PublicInternetPlans.tsx. The PublicOfferingCard component itself is no longer used.
Step 1: Move TierInfo type inline
In PublicInternetPlans.tsx, the import type { TierInfo } from "@/features/services/components/internet/PublicOfferingCard" needs to change. Define TierInfo directly in PublicInternetPlans.tsx:
interface TierInfo {
tier: "Silver" | "Gold" | "Platinum";
monthlyPrice: number;
maxMonthlyPrice?: number;
description: string;
features: string[];
pricingNote?: string;
}
Remove the import of TierInfo from PublicOfferingCard.
Step 2: Check if PublicOfferingCard is imported anywhere else
Search for all imports of PublicOfferingCard across the codebase. If no other files import it, it can be deleted. If other files import only TierInfo, move the type to a shared location or inline it.
Step 3: Verify and commit
pnpm type-check
pnpm lint
git add -A
git commit -m "refactor: inline TierInfo type, remove unused PublicOfferingCard"
Task 3: SIM - Direction-Aware Tab Transitions
Files:
- Modify:
apps/portal/src/features/services/components/sim/SimPlansContent.tsx
Context: The tab switcher is at lines 348-372. The plans grid is at lines 374-410. Currently uses animate-in fade-in duration-300 on the grid wrapper (line 377). The SIM_TABS array (lines 86-108) defines tab order: data-voice (0), data-only (1), voice-only (2).
Step 1: Add transition state tracking
In SimPlansContent component, add state to track slide direction:
import { useMemo, useRef } from "react";
// Inside SimPlansContent:
const prevTabRef = useRef(activeTab);
const slideDirection = useRef<"left" | "right">("left");
// Update direction when tab changes
if (prevTabRef.current !== activeTab) {
const tabKeys = SIM_TABS.map(t => t.key);
const prevIndex = tabKeys.indexOf(prevTabRef.current);
const nextIndex = tabKeys.indexOf(activeTab);
slideDirection.current = nextIndex > prevIndex ? "left" : "right";
prevTabRef.current = activeTab;
}
Step 2: Replace the grid animation
Replace the current animate-in fade-in duration-300 on line 377 with a keyed wrapper that triggers CSS animation:
<div
key={activeTab}
className={cn(
"space-y-8",
slideDirection.current === "left"
? "animate-slide-fade-left"
: "animate-slide-fade-right"
)}
>
Step 3: Add the CSS animations
Check if tailwind.config.ts is in apps/portal/ and add custom keyframes:
// In tailwind.config.ts extend.keyframes:
"slide-fade-left": {
"0%": { opacity: "0", transform: "translateX(24px)" },
"100%": { opacity: "1", transform: "translateX(0)" },
},
"slide-fade-right": {
"0%": { opacity: "0", transform: "translateX(-24px)" },
"100%": { opacity: "1", transform: "translateX(0)" },
},
// In extend.animation:
"slide-fade-left": "slide-fade-left 300ms ease-out",
"slide-fade-right": "slide-fade-right 300ms ease-out",
Step 4: Add overflow-hidden to the container
On the plans grid container (<div id="plans" className="min-h-[280px]"> at line 375), add overflow-hidden to prevent horizontal scrollbar during slide:
<div id="plans" className="min-h-[280px] overflow-hidden">
Step 5: Verify and commit
pnpm type-check
pnpm lint
git add -A
git commit -m "feat: direction-aware slide transitions for SIM tab switching"
Task 4: SIM - Redesign Plan Cards
Files:
- Modify:
apps/portal/src/features/services/components/sim/SimPlansContent.tsx
Context: SimPlanCardCompact is defined at lines 152-229. Currently has:
- Thin 0.5px top accent stripe (line 173-180)
- Small signal icon (w-4 h-4) in a 9x9 box + data size in text-lg (line 193-203)
- Small pricing via CardPricing component (line 207)
- Plan name in text-xs (line 214)
- Outline "Select Plan" button (line 217-225)
Step 1: Redesign the card
Replace the entire SimPlanCardCompact function (lines 152-229) with an enhanced version:
Key changes:
- Remove thin accent stripe - replace with subtle gradient overlay at top
- Data size hero: Bump from
text-lgtotext-2xl font-bold, make it the visual centerpiece - Signal icon: Keep but make slightly larger (w-5 h-5) in a bigger container
- Price: Show directly as
text-xl font-boldwith yen symbol, more prominent than current CardPricing - Plan name: Keep as subtitle in
text-xs text-muted-foreground - Hover effect: Add
hover:-translate-y-0.5 hover:shadow-lgfor lift effect - Button: Change from
variant="outline"to filledvariant="default"for regular,variant="success"for family - Background: Add subtle gradient -
bg-gradient-to-br from-sky-50/50 to-transparentfor regular (dark mode:dark:from-sky-950/20), emerald variant for family - Border: Slightly thicker on hover with primary color
function SimPlanCardCompact({
plan,
isFamily,
onSelect,
}: {
plan: SimCatalogProduct;
isFamily?: boolean;
onSelect: (sku: string) => void;
}) {
const displayPrice = plan.monthlyPrice ?? plan.unitPrice ?? plan.oneTimePrice ?? 0;
return (
<div
className={cn(
"group relative rounded-xl overflow-hidden transition-all duration-200",
"hover:-translate-y-0.5 hover:shadow-lg",
isFamily
? "border border-success/30 bg-gradient-to-br from-emerald-50/60 to-card dark:from-emerald-950/20 dark:to-card hover:border-success"
: "border border-border bg-gradient-to-br from-sky-50/40 to-card dark:from-sky-950/15 dark:to-card hover:border-primary/50"
)}
>
<div className="p-5">
{isFamily && (
<div className="flex items-center gap-1.5 mb-3">
<Users className="w-3.5 h-3.5 text-success" />
<span className="text-[10px] font-semibold text-success uppercase tracking-wider">
Family Discount
</span>
</div>
)}
{/* Data size - hero element */}
<div className="flex items-center gap-2.5 mb-4">
<div
className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center",
isFamily ? "bg-success/10" : "bg-primary/8"
)}
>
<Signal className={cn("w-5 h-5", isFamily ? "text-success" : "text-primary")} />
</div>
<span className="text-2xl font-bold text-foreground tracking-tight">
{plan.simDataSize}
</span>
</div>
{/* Price - prominent */}
<div className="mb-1">
<div className="flex items-baseline gap-0.5">
<span className="text-xl font-bold text-foreground">
¥{displayPrice.toLocaleString()}
</span>
<span className="text-xs text-muted-foreground">/mo</span>
</div>
{isFamily && (
<div className="text-[10px] text-success font-medium mt-0.5">Discounted price</div>
)}
</div>
{/* Plan name */}
<p className="text-xs text-muted-foreground mb-4 line-clamp-2 min-h-[2rem]">{plan.name}</p>
{/* CTA - filled button */}
<Button
className="w-full"
variant={isFamily ? "success" : "default"}
size="sm"
onClick={() => onSelect(plan.sku)}
rightIcon={<ArrowRight className="w-3.5 h-3.5" />}
>
Select Plan
</Button>
</div>
</div>
);
}
Step 2: Verify the Button success variant exists
Check apps/portal/src/components/atoms/button.tsx for available variants. If success doesn't exist, use variant="default" with a custom className for family cards: className="w-full bg-success hover:bg-success/90 text-success-foreground".
Step 3: Verify and commit
pnpm type-check
pnpm lint
git add apps/portal/src/features/services/components/sim/SimPlansContent.tsx
git commit -m "feat: redesign SIM plan cards with improved visual hierarchy"
Task 5: Visual QA and Polish
Step 1: Run dev server and check both pages
pnpm --filter @customer-portal/portal dev
Check in browser:
/services/internet- Verify unified card, offering selector, price updates/services/sim- Verify tab transitions, card design- Test mobile responsiveness (Chrome DevTools responsive mode)
- Test dark mode if supported
Step 2: Fix any visual issues found during QA
Common things to check:
- Segmented control alignment on mobile
- Price animation smoothness
- Slide transition not causing layout shift
- Card hover effects working
- Dark mode color contrast
Step 3: Final commit
git add -A
git commit -m "style: polish internet and SIM service page redesign"