364 lines
12 KiB
Markdown
364 lines
12 KiB
Markdown
|
|
# 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:
|
||
|
|
|
||
|
|
1. `ConsolidatedInternetCard` (lines 59-209) - shows price range + 3 tier cards
|
||
|
|
2. `AvailablePlansSection` (lines 653-731) - 3 expandable offering headers that open tier details below
|
||
|
|
3. 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:
|
||
|
|
|
||
|
|
1. Accepts both `consolidatedPlanData` (for "All Plans" price ranges) AND `plansByOffering` (for per-offering exact prices)
|
||
|
|
2. Has internal state: `selectedOffering: "all" | "home10g" | "home1g" | "apartment"`
|
||
|
|
3. 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)
|
||
|
|
4. 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
|
||
|
|
5. 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-200` for smooth price updates
|
||
|
|
6. Footer: same setup fee + CTA button
|
||
|
|
|
||
|
|
**Step 2: Remove obsolete components**
|
||
|
|
|
||
|
|
Delete from `PublicInternetPlans.tsx`:
|
||
|
|
|
||
|
|
- `PlanCardHeader` component (lines 266-340)
|
||
|
|
- `ExpandedTierDetails` component (lines 345-465)
|
||
|
|
- `MobilePlanCard` component (lines 470-648)
|
||
|
|
- `AvailablePlansSection` component (lines 653-731)
|
||
|
|
|
||
|
|
**Step 3: Update PublicInternetPlansContent render**
|
||
|
|
|
||
|
|
In the `PublicInternetPlansContent` component's return JSX (starts line 961):
|
||
|
|
|
||
|
|
- Replace the `ConsolidatedInternetCard` usage (lines 973-987) with `UnifiedInternetCard`
|
||
|
|
- Pass it both `consolidatedPlanData` and `plansByOffering`
|
||
|
|
- Remove the `AvailablePlansSection` usage (lines 989-995) entirely
|
||
|
|
|
||
|
|
**Step 4: Verify and commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
cd /home/barsa/projects/customer_portal/customer-portal
|
||
|
|
pnpm type-check
|
||
|
|
pnpm lint
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: No type errors or lint issues.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<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:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
<div id="plans" className="min-h-[280px] overflow-hidden">
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 5: Verify and commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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:
|
||
|
|
|
||
|
|
1. **Remove thin accent stripe** - replace with subtle gradient overlay at top
|
||
|
|
2. **Data size hero**: Bump from `text-lg` to `text-2xl font-bold`, make it the visual centerpiece
|
||
|
|
3. **Signal icon**: Keep but make slightly larger (w-5 h-5) in a bigger container
|
||
|
|
4. **Price**: Show directly as `text-xl font-bold` with yen symbol, more prominent than current CardPricing
|
||
|
|
5. **Plan name**: Keep as subtitle in `text-xs text-muted-foreground`
|
||
|
|
6. **Hover effect**: Add `hover:-translate-y-0.5 hover:shadow-lg` for lift effect
|
||
|
|
7. **Button**: Change from `variant="outline"` to filled `variant="default"` for regular, `variant="success"` for family
|
||
|
|
8. **Background**: Add subtle gradient - `bg-gradient-to-br from-sky-50/50 to-transparent` for regular (dark mode: `dark:from-sky-950/20`), emerald variant for family
|
||
|
|
9. **Border**: Slightly thicker on hover with primary color
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
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**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add -A
|
||
|
|
git commit -m "style: polish internet and SIM service page redesign"
|
||
|
|
```
|