refactor: streamline component layouts and enhance navigation

- Updated the AppShell and Sidebar components for improved layout and spacing.
- Replaced font colors in the Logo component for better visibility.
- Adjusted the PageLayout component to utilize backLink props instead of breadcrumbs for navigation consistency.
- Removed unnecessary description props from multiple PageLayout instances across various views to simplify the codebase.
- Introduced SectionCard component in OrderDetail for better organization of billing information.
- Enhanced utility styles in CSS for improved typography and layout consistency.
This commit is contained in:
barsa 2026-03-05 18:17:53 +09:00
parent 57f2c543d1
commit cab58d1c5b
43 changed files with 1101 additions and 285 deletions

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -228,7 +228,6 @@
@theme {
/* Font Families */
--font-family-sans: var(--font-sans);
--font-family-heading: var(--font-display);
--font-family-mono: var(--font-mono);
/* Colors */

View File

@ -25,12 +25,18 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
aria-label="Assist Solutions Logo"
>
{/* Top section - Light blue curved arrows */}
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#60A5FA" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#60A5FA" />
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#ffffff" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#ffffff" />
{/* Bottom section - Dark blue curved arrows */}
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="#1E40AF" />
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />
<path
d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z"
fill="rgba(255,255,255,0.6)"
/>
<path
d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z"
fill="rgba(255,255,255,0.6)"
/>
</svg>
</div>
);

View File

@ -0,0 +1,63 @@
import type { ReactNode } from "react";
import { cn } from "@/shared/utils";
type SectionTone = "primary" | "success" | "info" | "warning" | "danger" | "neutral";
const toneStyles: Record<SectionTone, string> = {
primary: "bg-primary/10 text-primary",
success: "bg-success/10 text-success",
info: "bg-info/10 text-info",
warning: "bg-warning/10 text-warning",
danger: "bg-danger/10 text-danger",
neutral: "bg-neutral/10 text-neutral",
};
interface SectionCardProps {
icon: ReactNode;
title: string;
subtitle?: string | undefined;
tone?: SectionTone;
actions?: ReactNode | undefined;
children: ReactNode;
className?: string | undefined;
}
export function SectionCard({
icon,
title,
subtitle,
tone = "primary",
actions,
children,
className,
}: SectionCardProps) {
return (
<div
className={cn(
"bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden",
className
)}
>
<div className="bg-muted/40 px-6 py-4 border-b border-border/40">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div
className={cn(
"h-9 w-9 rounded-lg flex items-center justify-center flex-shrink-0",
toneStyles[tone]
)}
>
{icon}
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
{subtitle && <p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>}
</div>
</div>
{actions && <div className="flex items-center gap-2 flex-shrink-0">{actions}</div>}
</div>
</div>
<div className="px-6 py-5">{children}</div>
</div>
);
}

View File

@ -0,0 +1 @@
export { SectionCard } from "./SectionCard";

View File

@ -27,6 +27,7 @@ export * from "./SummaryStats";
export * from "./FilterDropdown";
export * from "./ClearFiltersButton";
export * from "./DetailStatsGrid";
export { SectionCard } from "./SectionCard";
// Metric display
export { MetricCard, MetricCardSkeleton, type MetricCardProps } from "./MetricCard";

View File

@ -1,14 +1,12 @@
"use client";
import { useState, useEffect, useMemo, useRef } from "react";
import { useState, useEffect, useRef } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuthStore, useAuthSession } from "@/features/auth/stores/auth.store";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks";
import { accountService } from "@/features/account/api/account.api";
import { Sidebar } from "./Sidebar";
import { Header } from "./Header";
import { computeNavigation, type NavigationItem } from "./navigation";
import type { Subscription } from "@customer-portal/domain/subscriptions";
import { baseNavigation, type NavigationItem } from "./navigation";
interface AppShellProps {
children: React.ReactNode;
@ -152,12 +150,7 @@ export function AppShell({ children }: AppShellProps) {
const router = useRouter();
const { user, isAuthReady, isCheckingAuth } = useAppShellAuth(pathname);
const { expandedItems, toggleExpanded } = useSidebarExpansion(pathname);
const activeSubscriptionsQuery = useActiveSubscriptions();
const activeSubscriptions: Subscription[] = useMemo(
() => activeSubscriptionsQuery.data ?? [],
[activeSubscriptionsQuery.data]
);
const navigation = useMemo(() => computeNavigation(activeSubscriptions), [activeSubscriptions]);
const navigation = baseNavigation;
useEffect(() => {
try {

View File

@ -67,9 +67,7 @@ export const Sidebar = memo(function Sidebar({
<div className="flex flex-col h-0 flex-1 bg-sidebar">
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
<div className="flex items-center space-x-3">
<div className="h-9 w-9 bg-white/10 backdrop-blur-sm rounded-lg border border-white/10 flex items-center justify-center">
<Logo size={22} />
</div>
<Logo size={28} />
<div>
<span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
<p className="text-[11px] text-white/50 font-medium">Customer Portal</p>

View File

@ -1,4 +1,3 @@
import type { Subscription } from "@customer-portal/domain/subscriptions";
import {
HomeIcon,
CreditCardIcon,
@ -40,8 +39,8 @@ export const baseNavigation: NavigationItem[] = [
},
{
name: "Subscriptions",
href: "/account/subscriptions",
icon: ServerIcon,
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }],
},
{ name: "Services", href: "/account/services", icon: Squares2X2Icon },
{
@ -59,36 +58,3 @@ export const baseNavigation: NavigationItem[] = [
},
{ name: "Log out", href: "#", icon: ArrowRightStartOnRectangleIcon, isLogout: true },
];
export function computeNavigation(activeSubscriptions?: Subscription[]): NavigationItem[] {
const nav: NavigationItem[] = baseNavigation.map(item => ({
...item,
children: item.children ? [...item.children] : undefined,
}));
const subIdx = nav.findIndex(n => n.name === "Subscriptions");
const currentItem = nav[subIdx];
if (subIdx >= 0 && currentItem) {
const dynamicChildren = (activeSubscriptions || []).map(sub => ({
name: truncate(sub.productName || `Subscription ${sub.id}`, 28),
href: `/account/subscriptions/${sub.id}`,
tooltip: sub.productName || `Subscription ${sub.id}`,
}));
nav[subIdx] = {
name: currentItem.name,
icon: currentItem.icon,
href: currentItem.href,
isLogout: currentItem.isLogout,
section: currentItem.section,
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }, ...dynamicChildren],
};
}
return nav;
}
export function truncate(text: string, max: number): string {
if (text.length <= max) return text;
return text.slice(0, Math.max(0, max - 1)) + "…";
}

View File

@ -1,20 +1,16 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
export interface BreadcrumbItem {
label: string;
href?: string | undefined;
}
interface PageLayoutProps {
icon?: ReactNode | undefined;
title: string;
description?: string | undefined;
actions?: ReactNode | undefined;
breadcrumbs?: BreadcrumbItem[] | undefined;
backLink?: { label: string; href: string } | undefined;
statusPill?: ReactNode | undefined;
loading?: boolean | undefined;
loadingFallback?: ReactNode | undefined;
error?: Error | string | null | undefined;
@ -27,7 +23,8 @@ export function PageLayout({
title,
description,
actions,
breadcrumbs,
backLink,
statusPill,
loading = false,
loadingFallback,
error = null,
@ -35,42 +32,24 @@ export function PageLayout({
children,
}: PageLayoutProps) {
return (
<div className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)] md:py-[var(--cp-space-2xl)]">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8">
{/* Breadcrumbs - scrollable on mobile */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav
className="mb-[var(--cp-space-md)] sm:mb-[var(--cp-space-lg)] -mx-[var(--cp-space-md)] px-[var(--cp-space-md)] sm:mx-0 sm:px-0 overflow-x-auto scrollbar-none"
aria-label="Breadcrumb"
>
<ol className="flex items-center space-x-1 sm:space-x-2 text-sm text-muted-foreground whitespace-nowrap">
{breadcrumbs.map((item, index) => (
<li key={index} className="flex items-center flex-shrink-0">
{index > 0 && (
<ChevronRightIcon className="h-4 w-4 mx-1 sm:mx-2 text-muted-foreground/50 flex-shrink-0" />
)}
{item.href ? (
<div>
{/* Header band with subtle background */}
<div className="bg-muted/40 border-b border-border/40">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-[var(--cp-space-md)] sm:py-[var(--cp-space-lg)]">
{/* Back link */}
{backLink && (
<div className="mb-3">
<Link
href={item.href}
className="hover:text-foreground transition-colors duration-200 py-1 px-0.5 -mx-0.5 rounded"
href={backLink.href}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200"
>
{item.label}
<ArrowLeftIcon className="h-4 w-4" />
{backLink.label}
</Link>
) : (
<span className="text-foreground font-medium py-1" aria-current="page">
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>
</div>
)}
{/* Header */}
<div className="mb-[var(--cp-space-lg)] sm:mb-[var(--cp-space-xl)] md:mb-[var(--cp-space-2xl)] pb-[var(--cp-space-lg)] sm:pb-[var(--cp-space-xl)] md:pb-[var(--cp-space-2xl)] border-b border-border">
<div className="flex flex-col gap-[var(--cp-space-md)] sm:gap-[var(--cp-space-lg)]">
{/* Title row */}
<div className="flex items-start justify-between gap-4 min-w-0">
<div className="flex items-start min-w-0 flex-1">
{icon && (
<div className="h-7 w-7 sm:h-8 sm:w-8 text-primary mr-[var(--cp-space-md)] sm:mr-[var(--cp-space-lg)] flex-shrink-0 mt-0.5">
@ -78,9 +57,12 @@ export function PageLayout({
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight">
{title}
</h1>
{statusPill}
</div>
{description && (
<p className="text-sm text-muted-foreground mt-1 leading-relaxed line-clamp-2 sm:line-clamp-none">
{description}
@ -88,17 +70,18 @@ export function PageLayout({
)}
</div>
</div>
{/* Actions - full width on mobile, stacks buttons */}
{actions && (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 w-full sm:w-auto [&>*]:w-full [&>*]:sm:w-auto">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 flex-shrink-0">
{actions}
</div>
)}
</div>
</div>
</div>
{/* Content with loading and error states */}
{/* Content */}
<div className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)]">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8">
<div className="space-y-[var(--cp-space-xl)] sm:space-y-[var(--cp-space-2xl)]">
{renderPageContent({
loading,
@ -110,6 +93,7 @@ export function PageLayout({
</div>
</div>
</div>
</div>
);
}

View File

@ -1,2 +1 @@
export { PageLayout } from "./PageLayout";
export type { BreadcrumbItem } from "./PageLayout";

View File

@ -7,7 +7,6 @@ export { AuthLayout } from "./AuthLayout/AuthLayout";
export type { AuthLayoutProps } from "./AuthLayout/AuthLayout";
export { PageLayout } from "./PageLayout/PageLayout";
export type { BreadcrumbItem } from "./PageLayout/PageLayout";
export { PublicShell } from "./PublicShell/PublicShell";
export type { PublicShellProps } from "./PublicShell/PublicShell";

View File

@ -124,25 +124,14 @@ export default function ProfileContainer() {
if (isLoading) {
return (
<PageLayout
icon={<UserIcon />}
title="Profile"
description="Manage your account information"
loading
>
<PageLayout icon={<UserIcon />} title="Profile" loading>
<ProfileLoadingSkeleton />
</PageLayout>
);
}
return (
<PageLayout
icon={<UserIcon />}
title="Profile"
description="Manage your account information"
error={error}
onRetry={reload}
>
<PageLayout icon={<UserIcon />} title="Profile" error={error} onRetry={reload}>
{error && (
<AlertBanner variant="error" title="Unable to load profile" className="mb-6" elevated>
{error}

View File

@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
@ -18,11 +17,7 @@ import {
function InvoiceDetailSkeleton() {
return (
<PageLayout
icon={<DocumentTextIcon />}
title="Invoice"
description="Invoice details and actions"
>
<PageLayout icon={<DocumentTextIcon />} title="Invoice">
<div className="space-y-6">
<LoadingCard />
<div className="bg-card text-card-foreground rounded-xl border border-border p-6 space-y-4 shadow-[var(--cp-shadow-1)]">
@ -86,18 +81,13 @@ export function InvoiceDetailContainer() {
<PageLayout
icon={<DocumentTextIcon />}
title="Invoice"
description="Invoice details and actions"
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
>
<ErrorState
title="Error loading invoice"
message={error instanceof Error ? error.message : "Invoice not found"}
variant="page"
/>
<div className="mt-4">
<Link href="/account/billing/invoices" className="text-primary font-medium">
Back to invoices
</Link>
</div>
</PageLayout>
);
}
@ -106,12 +96,7 @@ export function InvoiceDetailContainer() {
<PageLayout
icon={<DocumentTextIcon />}
title={`Invoice #${invoice.id}`}
description="Invoice details and actions"
breadcrumbs={[
{ label: "Billing", href: "/account/billing/invoices" },
{ label: "Invoices", href: "/account/billing/invoices" },
{ label: `#${invoice.id}` },
]}
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
>
<div>
<div className="bg-card text-card-foreground rounded-xl shadow-[var(--cp-shadow-1)] border border-border overflow-hidden">

View File

@ -6,11 +6,7 @@ import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceL
export function InvoicesListContainer() {
return (
<PageLayout
icon={<CreditCardIcon />}
title="Invoices"
description="Manage and view your billing invoices"
>
<PageLayout icon={<CreditCardIcon />} title="Invoices">
<InvoicesList />
</PageLayout>
);

View File

@ -235,11 +235,7 @@ export function PaymentMethodsContainer() {
if (combinedError) {
return (
<PageLayout
icon={<CreditCardIcon />}
title="Payment Methods"
description="Manage your saved payment methods and billing information"
>
<PageLayout icon={<CreditCardIcon />} title="Payment Methods">
<AsyncBlock error={combinedError} variant="page">
<></>
</AsyncBlock>
@ -254,11 +250,7 @@ export function PaymentMethodsContainer() {
};
return (
<PageLayout
icon={<CreditCardIcon />}
title="Payment Methods"
description="Manage your saved payment methods and billing information"
>
<PageLayout icon={<CreditCardIcon />} title="Payment Methods">
<ErrorBoundary>
<InlineToast
visible={paymentRefresh.toast.visible}

View File

@ -193,11 +193,7 @@ export function AccountCheckoutContainer() {
if (!cartItem || !orderType) return <CheckoutErrorFallback shopHref={getShopHref(pathname)} />;
return (
<PageLayout
title="Checkout"
description="Verify your address, review totals, and submit your order"
icon={<ShieldCheck className="h-6 w-6" />}
>
<PageLayout title="Checkout" icon={<ShieldCheck className="h-6 w-6" />}>
<div className="max-w-2xl mx-auto space-y-8">
<InlineToast
visible={payment.paymentRefresh.toast.visible}

View File

@ -14,7 +14,7 @@ import { useInternetEligibility } from "@/features/services/hooks";
function DashboardSkeleton() {
return (
<PageLayout title="Dashboard" description="Overview of your account" loading>
<PageLayout title="Dashboard" loading>
<div className="space-y-8">
<div className="space-y-3">
<div className="h-4 cp-skeleton-shimmer rounded w-24" />
@ -167,7 +167,7 @@ function DashboardContent({
displayName: string;
}) {
return (
<PageLayout title="Dashboard" description="Overview of your account">
<PageLayout title="Dashboard">
<InlineToast
visible={eligibilityToast.visible}
text={eligibilityToast.text}
@ -222,7 +222,7 @@ export function DashboardView() {
if (authLoading || summaryLoading) return <DashboardSkeleton />;
if (error) {
return (
<PageLayout title="Dashboard" description="Overview of your account">
<PageLayout title="Dashboard">
<ErrorState
title="Error loading dashboard"
message={getErrorMessageText(error)}

View File

@ -20,6 +20,7 @@ import {
} from "@heroicons/react/24/outline";
import { StatusPill } from "@/components/atoms/status-pill";
import { DetailStatsGrid, type StatGridItem } from "@/components/molecules";
import { SectionCard } from "@/components/molecules/SectionCard";
import { ordersService } from "@/features/orders/api/orders.api";
import { useOrderUpdates } from "@/features/orders/hooks/useOrderUpdates";
import {
@ -486,15 +487,14 @@ function OrderDetailLoadedContent({
items={buildStatsGridItems(statusDescriptor, statusPillVariant, totals, placedDateShort)}
/>
<div className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]">
<h3 className="text-sm font-medium text-muted-foreground mb-4">Order Progress</h3>
<SectionCard icon={<ClockIcon className="h-5 w-5" />} title="Order Progress" tone="info">
{statusDescriptor && (
<OrderProgressTimeline
serviceCategory={serviceCategory}
currentState={statusDescriptor.state}
/>
)}
</div>
</SectionCard>
<div className="rounded-xl border border-border bg-card text-card-foreground shadow-[var(--cp-shadow-1)]">
<OrderDetailHeader
@ -551,15 +551,12 @@ export function OrderDetailContainer() {
useOrderUpdates(params.id, handleOrderUpdate);
const pageTitle = data ? `${data.orderType} Service Order` : "Order Details";
const pageDescription = data ? `Order #${derived.orderNumber}` : "Loading order details...";
const breadcrumbLabel = data ? `Order #${derived.orderNumber}` : "Order Details";
return (
<PageLayout
icon={<ClipboardDocumentCheckIcon />}
title={pageTitle}
description={pageDescription}
breadcrumbs={[{ label: "Orders", href: "/account/orders" }, { label: breadcrumbLabel }]}
backLink={{ label: "Back to Orders", href: "/account/orders" }}
>
{error && <div className="mb-4 text-sm text-danger">{error}</div>}
{isNewOrder && <NewOrderBanner />}

View File

@ -224,11 +224,7 @@ export function OrdersListContainer() {
const summaryStatsItems = useMemo(() => buildOrderSummaryStats(stats), [stats]);
return (
<PageLayout
icon={<ClipboardDocumentListIcon />}
title="My Orders"
description="View and track all your orders"
>
<PageLayout icon={<ClipboardDocumentListIcon />} title="My Orders">
<Suspense fallback={null}>
<OrdersSuccessBanner />
</Suspense>

View File

@ -198,7 +198,7 @@ export function InternetConfigureContainer({
<PageLayout
icon={<ServerIcon />}
title="Configure Internet Service"
description="Set up your internet service options"
backLink={{ label: "Back to Services", href: "/account/services" }}
>
<div className="text-center py-12">
<p className="text-muted-foreground">Plan not found</p>
@ -228,7 +228,7 @@ export function InternetConfigureContainer({
<PageLayout
icon={<ServerIcon />}
title="Configure Internet Service"
description="Set up your internet service options"
backLink={{ label: "Back to Services", href: "/account/services" }}
>
<div className="min-h-[70vh]">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">

View File

@ -5,11 +5,7 @@ import { ServerIcon } from "@heroicons/react/24/outline";
export function ConfigureLoadingSkeleton() {
return (
<PageLayout
icon={<ServerIcon />}
title="Configure Internet Service"
description="Set up your internet service options"
>
<PageLayout icon={<ServerIcon />} title="Configure Internet Service">
<div className="max-w-4xl mx-auto">
{/* Back to plans */}
<div className="text-center mb-12">

View File

@ -154,7 +154,7 @@ export function SimConfigureView(props: Props) {
return (
<PageLayout
title={`Configure ${plan.name}`}
description="Customize your mobile service"
backLink={{ label: "Back to Services", href: "/account/services" }}
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<div className="max-w-4xl mx-auto space-y-8">

View File

@ -5,11 +5,7 @@ import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
export function LoadingSkeleton() {
return (
<PageLayout
title="Configure SIM"
description="Customize your mobile service"
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
>
<PageLayout title="Configure SIM" icon={<DevicePhoneMobileIcon className="h-6 w-6" />}>
<div className="max-w-4xl mx-auto space-y-8">
{/* Header card skeleton */}
<div className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]">

View File

@ -8,11 +8,7 @@ export function PlanNotFound() {
const servicesBasePath = useServicesBasePath();
return (
<PageLayout
title="Plan Not Found"
description="The selected plan could not be found"
icon={<ExclamationTriangleIcon className="h-6 w-6" />}
>
<PageLayout title="Plan Not Found" icon={<ExclamationTriangleIcon className="h-6 w-6" />}>
<div className="text-center py-12">
<ExclamationTriangleIcon className="h-12 w-12 mx-auto text-danger mb-4" />
<h2 className="text-xl font-semibold text-foreground mb-2">Plan Not Found</h2>

View File

@ -11,11 +11,7 @@ import { ServicesOverviewContent } from "@/features/services/components/common/S
*/
export function AccountServicesOverview() {
return (
<PageLayout
icon={<Squares2X2Icon />}
title="Services"
description="Browse and order connectivity services"
>
<PageLayout icon={<Squares2X2Icon />} title="Services">
<ServicesOverviewContent basePath="/account/services" showHero={false} />
</PageLayout>
);

View File

@ -1,11 +1,10 @@
"use client";
import Link from "next/link";
import { useState, type ReactNode } from "react";
import { PageLayout, type BreadcrumbItem } from "@/components/templates/PageLayout";
import { PageLayout } from "@/components/templates/PageLayout";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { Button } from "@/components/atoms";
import { ArrowLeftIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ============================================================================
// Types
@ -23,10 +22,6 @@ export interface CancellationFlowProps {
icon: ReactNode;
/** Page title */
title: string;
/** Page description / subtitle */
description: string;
/** Breadcrumb items */
breadcrumbs: BreadcrumbItem[];
/** Back link URL */
backHref: string;
/** Back link label */
@ -511,8 +506,6 @@ function StepRouter(props: StepRouterProps) {
export function CancellationFlow({
icon,
title,
description,
breadcrumbs,
backHref,
backLabel,
availableMonths,
@ -577,20 +570,12 @@ export function CancellationFlow({
<PageLayout
icon={icon}
title={title}
description={description}
actions={headerActions}
breadcrumbs={breadcrumbs}
backLink={{ label: backLabel, href: backHref }}
loading={loading}
error={error}
>
<div className="max-w-2xl mx-auto">
<Link
href={backHref}
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-6"
>
<ArrowLeftIcon className="w-4 h-4" />
{backLabel}
</Link>
{warningBanner}
<CancellationAlerts formError={formError} successMessage={successMessage} />
<StepIndicator currentStep={step} />

View File

@ -34,12 +34,14 @@ export function SubscriptionGridCard({ subscription, className }: SubscriptionGr
const { formatCurrency } = useFormatCurrency();
const statusIndicator = mapSubscriptionStatus(subscription.status);
const cycleLabel = getBillingCycleLabel(subscription.cycle);
const isInactive = ["Completed", "Cancelled", "Terminated"].includes(subscription.status);
return (
<Link
href={`/account/subscriptions/${subscription.serviceId}`}
className={cn(
"group flex flex-col p-4 rounded-xl bg-card border border-border/60",
"group flex flex-col p-4 rounded-xl bg-card border border-border",
isInactive && "opacity-60",
"transition-all duration-200",
"hover:border-border hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
@ -91,7 +93,7 @@ export function SubscriptionGridCard({ subscription, className }: SubscriptionGr
export function SubscriptionGridCardSkeleton() {
return (
<div className="flex flex-col p-4 rounded-xl bg-card border border-border/60">
<div className="flex flex-col p-4 rounded-xl bg-card border border-border">
<div className="flex items-start justify-between gap-2 mb-3">
<div className="space-y-1.5 flex-1">
<div className="h-4 cp-skeleton-shimmer rounded w-3/4" />

View File

@ -43,12 +43,7 @@ function CancellationPendingView({
<PageLayout
icon={icon}
title={title}
description={preview.serviceName}
breadcrumbs={[
{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF },
{ label: preview.serviceName, href: `/account/subscriptions/${subscriptionId}` },
{ label: "Cancellation Status" },
]}
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
>
<div className="max-w-2xl mx-auto">
<div className="bg-card border border-border rounded-xl p-6 sm:p-8">
@ -236,12 +231,6 @@ function CancellationFlowView({
<CancellationFlow
icon={getServiceIcon(preview.serviceType)}
title={getServiceTitle(preview.serviceType)}
description={preview.serviceName}
breadcrumbs={[
{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF },
{ label: preview.serviceName, href: `/account/subscriptions/${subscriptionId}` },
{ label: "Cancel" },
]}
backHref={`/account/subscriptions/${subscriptionId}`}
backLabel="Back to Subscription"
availableMonths={preview.availableMonths}
@ -316,8 +305,7 @@ export function CancelSubscriptionContainer() {
<PageLayout
icon={<GlobeAltIcon />}
title="Cancel Subscription"
description="Loading cancellation information..."
breadcrumbs={[{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF }, { label: "Cancel" }]}
backLink={{ label: "Back to Subscriptions", href: SUBSCRIPTIONS_HREF }}
loading={state.loading}
error={state.error}
>

View File

@ -128,7 +128,7 @@ export function SimCallHistoryContainer(): React.ReactElement {
<PageLayout
icon={<PhoneIcon />}
title="Call & SMS History"
description="View your call and SMS records"
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
>
<div className="max-w-5xl mx-auto">
<div className="mb-4">

View File

@ -238,7 +238,7 @@ export function SimChangePlanContainer() {
<PageLayout
icon={<DevicePhoneMobileIcon />}
title="Change Plan"
description="Switch to a different data plan"
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
>
<div className="max-w-4xl mx-auto">
<div className="mb-4">

View File

@ -331,7 +331,7 @@ export function SimReissueContainer() {
<PageLayout
icon={<DevicePhoneMobileIcon />}
title="Reissue SIM"
description="Request a replacement SIM card"
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
>
<div className="max-w-3xl mx-auto">
<div className="mb-4">

View File

@ -100,7 +100,7 @@ export function SimTopUpContainer() {
<PageLayout
icon={<DevicePhoneMobileIcon />}
title="Top Up Data"
description="Add data to your SIM"
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
>
<div className="max-w-3xl mx-auto">
<div className="mb-4">

View File

@ -13,6 +13,7 @@ import {
import { useSubscription } from "@/features/subscriptions/hooks";
import { Formatting } from "@customer-portal/domain/toolkit";
import { PageLayout } from "@/components/templates/PageLayout";
import { SectionCard } from "@/components/molecules/SectionCard";
import { StatusPill } from "@/components/atoms/status-pill";
import { SubscriptionDetailStatsSkeleton } from "@/components/atoms/loading-skeleton";
import { formatIsoDate, cn } from "@/shared/utils";
@ -197,23 +198,24 @@ function SubscriptionDetailContent({
{isSim && <SimTabNavigation subscriptionId={subscriptionId} activeTab={activeTab} />}
{activeTab === "sim" && isSim && <SimManagementSection subscriptionId={subscriptionId} />}
{activeTab === "overview" && (
<div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)] p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CreditCardIcon className="h-5 w-5 text-muted-foreground" />
<h3 className="text-lg font-semibold text-foreground">Billing</h3>
</div>
<SectionCard
icon={<CreditCardIcon className="h-5 w-5" />}
title="Billing Information"
subtitle="Payment and invoices"
tone="primary"
actions={
<Link
href="/account/billing/invoices"
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
>
View all invoices &rarr;
View Invoices
</Link>
</div>
<p className="mt-2 text-sm text-muted-foreground">
}
>
<p className="text-sm text-muted-foreground">
Invoices and payment history are available on the billing page.
</p>
</div>
</SectionCard>
)}
</div>
);
@ -231,11 +233,7 @@ export function SubscriptionDetailContainer() {
<PageLayout
icon={<ServerIcon className="h-6 w-6" />}
title="Subscription"
description="Loading subscription details..."
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: "Subscription" },
]}
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
>
<div className="space-y-6">
<SubscriptionDetailStatsSkeleton />
@ -255,10 +253,7 @@ export function SubscriptionDetailContainer() {
icon={<ServerIcon className="h-6 w-6" />}
title={subscription?.productName ?? "Subscription"}
actions={headerActions}
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: subscription?.productName ?? "Subscription" },
]}
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
error={getPageError(error)}
>
{subscription ? (

View File

@ -146,11 +146,7 @@ export function SubscriptionsListContainer() {
if (showLoading || error) {
return (
<PageLayout
icon={<Server />}
title="Subscriptions"
description="Manage your active subscriptions"
>
<PageLayout icon={<Server />} title="Subscriptions">
<AsyncBlock isLoading={false} error={error}>
<div className="space-y-6">
<SubscriptionMetricsSkeleton />
@ -165,7 +161,6 @@ export function SubscriptionsListContainer() {
<PageLayout
icon={<Server />}
title="Subscriptions"
description="Manage your active subscriptions"
actions={
<Button as="a" href="/account/services" size="sm">
Browse Services
@ -175,7 +170,7 @@ export function SubscriptionsListContainer() {
<ErrorBoundary>
{stats && <SubscriptionMetrics stats={stats} />}
<div className="bg-card rounded-xl border border-border/60 shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="px-4 sm:px-5 py-3.5 border-b border-border/40">
<SearchFilterBar
searchValue={searchTerm}

View File

@ -242,8 +242,7 @@ export function NewSupportCaseView() {
<PageLayout
icon={<TicketIconSolid />}
title="Create Support Case"
description="Get help from our support team"
breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Create Case" }]}
backLink={{ label: "Back to Cases", href: "/account/support" }}
>
<AiChatSuggestion />

View File

@ -291,15 +291,10 @@ export function SupportCaseDetailView({ caseId }: SupportCaseDetailViewProps): R
<PageLayout
icon={<TicketIconSolid />}
title={state.supportCase ? `Case #${state.supportCase.caseNumber}` : "Loading..."}
description={state.supportCase?.subject}
loading={state.showLoading}
error={state.pageError}
onRetry={() => void state.refetch()}
breadcrumbs={[
{ label: "Support", href: SUPPORT_HREF },
{ label: "Cases", href: SUPPORT_HREF },
{ label: state.supportCase ? `#${state.supportCase.caseNumber}` : "..." },
]}
backLink={{ label: "Back to Cases", href: SUPPORT_HREF }}
actions={
<Button
as="a"
@ -344,11 +339,7 @@ function CaseNotFoundView(): React.ReactElement {
<PageLayout
icon={<TicketIconSolid />}
title="Case Not Found"
breadcrumbs={[
{ label: "Support", href: SUPPORT_HREF },
{ label: "Cases", href: SUPPORT_HREF },
{ label: "Not Found" },
]}
backLink={{ label: "Back to Cases", href: SUPPORT_HREF }}
>
<AlertBanner variant="error" title="Case not found">
The support case you&apos;re looking for could not be found or you don&apos;t have

View File

@ -233,11 +233,9 @@ export function SupportCasesView() {
<PageLayout
icon={<ChatBubbleLeftRightIconSolid />}
title="Support Cases"
description="Track and manage your support requests"
loading={showLoading}
error={error}
onRetry={() => void refetch()}
breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Cases" }]}
actions={
<Button as="a" href="/account/support/new" leftIcon={<TicketIcon className="h-4 w-4" />}>
New Case

View File

@ -109,7 +109,6 @@ export function SupportHomeView() {
<PageLayout
icon={<ChatBubbleLeftRightIconSolid />}
title="Support Center"
description="Get help with your account and services"
loading={showLoading}
error={error}
onRetry={() => void refetch()}

View File

@ -373,9 +373,8 @@ export function ResidenceCardVerificationSettingsView(): React.ReactElement {
return (
<PageLayout
title="ID Verification"
description="Upload your residence card for SIM activation"
icon={<ShieldCheckIcon className="h-6 w-6" />}
breadcrumbs={[{ label: "Settings", href: "/account/settings" }, { label: "ID Verification" }]}
backLink={{ label: "Back to Settings", href: "/account/settings" }}
>
<div className="max-w-2xl mx-auto space-y-6">
<SubCard

View File

@ -172,6 +172,10 @@
transform: translateX(-50%);
}
@utility font-heading {
font-family: var(--font-display);
}
@utility text-display-xl {
font-size: var(--cp-text-display-xl);
letter-spacing: var(--cp-tracking-tight);

View File

@ -0,0 +1,143 @@
# Portal UI Cleanup Design
## Problem
The portal account pages have accumulated visual inconsistencies: different card styles, spacing, button variants, badge implementations, and header structures across pages. The sidebar color feels disconnected from the landing page brand. Logo visibility is poor on the dark sidebar. Subscriptions navigation is over-complicated. Breadcrumbs conflict with page titles. Detail pages lack consistent structure.
## Decisions
### 1. Sidebar & Brand Consistency
**Sidebar background:** Shift from `oklch(0.18 0.03 250)` to `oklch(0.20 0.06 234.4)` — same hue as primary blue, slightly more saturated, still dark. Creates brand harmony with landing page.
**Sidebar border:** Adjust to match new tone.
**Logo:** White variant for dark sidebar. SVG fallback colors change from `#60A5FA`/`#1E40AF` to `#ffffff`/`rgba(255,255,255,0.6)`. Remove the `bg-white/10` container — let logo sit directly.
**Subscriptions nav:** Change from expandable with child links to a direct link to `/account/subscriptions` (like Dashboard or Services). Remove `computeNavigation` dynamic children.
### 2. Unified List Page Pattern
All list pages (Subscriptions, Orders, Invoices, Support) follow one pattern:
```
PageLayout (bg-muted/40 header built-in, icon + title + action buttons, NO description)
Metric cards row (grid-cols-2 lg:grid-cols-4 gap-3) — where applicable
Content card (bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)])
Search/filter bar (px-5 py-3.5 border-b border-border/40)
Content area (p-5)
Grid: grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3
or Table: DataTable
```
**List header:** Handled by PageLayout. Icon + title on left, action buttons right-aligned. No description text.
**Card borders:** `border border-border` everywhere — drop `/60` opacity variants on outer cards.
**List item hover:** `hover:border-border hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5` — consistent across all.
**Status badges:** Always use `StatusPill` atom — no inline badge classes.
**Affected pages:** SubscriptionsList, OrdersList, InvoicesList, SupportCasesView.
### 3. Unified Detail Page Pattern
```
PageLayout (bg-muted/40 header, backLink + title + status pill + right-aligned action buttons)
Stats/summary row (bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)])
Grid: grid-cols-2 md:grid-cols-4 gap-6, px-6 py-5
Stacked section cards (space-y-6)
```
**Back navigation:** PageLayout `backLink` prop renders `← Back to {parent}` link above the header. Detail pages only. No breadcrumbs.
**Action buttons:** Always in PageLayout `actions` prop, right-aligned. Use `<Button>` component variants (not inline-styled).
**Affected pages:** SubscriptionDetail, OrderDetail, InvoiceDetail, SupportCaseDetailView.
### 4. PageLayout Template Changes
PageLayout becomes the single source of truth for page header styling. Changes:
- **Header area:** Always renders with `bg-muted/40 border-b border-border/40` background. No more plain/transparent headers.
- **Remove description:** Drop the `description` prop usage — title is enough, description was redundant with sidebar context.
- **New `backLink` prop:** `{ label: string; href: string }` — renders `← Back to {parent}` above the header on detail pages. List pages don't pass this.
- **Remove breadcrumbs:** Drop breadcrumb rendering entirely — replaced by `backLink` on detail pages, sidebar on list pages.
- **Header layout:** `flex items-center justify-between` — icon + title on left, actions on right. Status pills sit next to the title.
- **Consistent spacing:** Header padding `px-6 py-4`, content area spacing `space-y-6`.
This means every account page automatically gets the same header treatment by using PageLayout. No per-page header styling needed.
### 5. Section Card Pattern
Every content section in detail pages uses this structure:
```
bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden
Header (bg-muted/40 px-6 py-4 border-b border-border/40)
flex items-center justify-between
Left: flex items-center gap-3
Icon circle (h-9 w-9 rounded-lg bg-{tone}/10 text-{tone})
Title: text-sm font-semibold text-foreground
Subtitle: text-xs text-muted-foreground
Right: action button(s) or link
Content (px-6 py-5)
```
Each section gets a semantic tone for its icon circle:
- Billing: `primary`
- SIM Management: `info`
- Order Items: `success`
- Support: `warning`
### 6. Subscriptions-Specific Fixes
- **Active/inactive dimming:** Active subscriptions render normally. Completed/Cancelled/Terminated get `opacity-60` on the grid card + muted status indicator.
- **Cancel button:** Move from inline-styled to `<Button variant="destructive" size="sm">` via PageLayout actions.
- **Domain imports:** Fix `@customer-portal/domain/subscriptions` module resolution (run `pnpm domain:build`).
- **Grid card borders:** Standardize from `border-border/60` to `border-border`.
### 7. Cross-Page Consistency
**Typography:**
- Page titles: PageLayout handles it (`text-xl sm:text-2xl md:text-3xl font-bold`). Remove custom title sizes in views.
- Section headings: `text-sm font-semibold text-foreground` inside section cards.
- Stat values: `font-heading` for numbers.
**Buttons — always use `<Button>` component:**
- Primary actions: `variant="default"` (Browse Services, New Case)
- Secondary: `variant="outline"` (Back, Retry, View All)
- Destructive: `variant="destructive"` (Cancel Service)
- Links: `variant="link"` or `ghost` for in-section navigation
**Status display:**
- Always `<StatusPill>` — no inline badge classes
- `size="sm"` in list views, `size="md"` in detail headers
**Borders:**
- Outer cards: `border border-border`
- Internal dividers only: `border-b border-border/40`
**Spacing:**
- Card padding: `p-4` for list item cards, `p-5` for container sections
- Gap between items: `gap-3`
- Section spacing: `space-y-6`
### 8. Navigation Changes
- **List pages:** No back link — sidebar handles navigation.
- **Detail pages:** `← Back to {parent}` link above title.
- **Breadcrumbs:** Removed — redundant with page title.
- **Sidebar subscriptions:** Direct link, no expandable children.
## Out of Scope
- Landing page color token migration (separate effort)
- New component creation beyond what's needed for the patterns above
- Mobile-specific layout changes

View File

@ -0,0 +1,774 @@
# Portal UI Cleanup Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Unify all portal account pages with consistent styling, fix sidebar brand alignment, simplify navigation, and standardize list/detail page patterns.
**Architecture:** Update PageLayout template to be the single source of truth for page headers (bg-muted/40 background, backLink support, no breadcrumbs). Create a reusable SectionCard molecule for detail pages. Update all views to use these patterns consistently.
**Tech Stack:** Next.js 15, React 19, Tailwind CSS v4, CSS custom properties (OKLCH), shadcn/ui patterns
---
### Task 1: Fix Sidebar Color & Logo
**Files:**
- Modify: `apps/portal/src/app/globals.css` (lines 83-88 for sidebar tokens, lines 197-199 for dark mode)
- Modify: `apps/portal/src/components/atoms/logo.tsx` (lines 27-33 for SVG colors)
- Modify: `apps/portal/src/components/organisms/AppShell/Sidebar.tsx` (lines 66-76 for logo container)
**Step 1: Update sidebar CSS tokens**
In `globals.css`, change the sidebar variables in `:root`:
```css
/* Before */
--sidebar: oklch(0.18 0.03 250);
--sidebar-border: oklch(0.25 0.04 250);
/* After — deeper primary blue hue (234.4), more saturated */
--sidebar: oklch(0.2 0.06 234.4);
--sidebar-border: oklch(0.27 0.05 234.4);
```
In `.dark`, update sidebar:
```css
/* Before */
--sidebar: oklch(0.13 0.025 250);
--sidebar-border: oklch(0.22 0.03 250);
/* After */
--sidebar: oklch(0.14 0.04 234.4);
--sidebar-border: oklch(0.22 0.04 234.4);
```
**Step 2: Update logo SVG fallback to white**
In `logo.tsx`, change the SVG path fills:
```tsx
// Before
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#60A5FA" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#60A5FA" />
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="#1E40AF" />
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />
// After
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#ffffff" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#ffffff" />
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="rgba(255,255,255,0.6)" />
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="rgba(255,255,255,0.6)" />
```
**Step 3: Simplify logo container in Sidebar**
In `Sidebar.tsx`, remove the `bg-white/10` container around the logo:
```tsx
// Before (lines 66-76)
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
<div className="flex items-center space-x-3">
<div className="h-9 w-9 bg-white/10 backdrop-blur-sm rounded-lg border border-white/10 flex items-center justify-center">
<Logo size={22} />
</div>
<div>
<span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
<p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
</div>
</div>
</div>
// After — logo sits directly, no glass container
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
<div className="flex items-center space-x-3">
<Logo size={28} />
<div>
<span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
<p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
</div>
</div>
</div>
```
**Step 4: Verify visually**
Run: `pnpm --filter @customer-portal/portal dev` (with user permission)
Check: Sidebar color is deeper blue matching brand, logo is clearly visible white, text remains readable.
**Step 5: Commit**
```
style: align sidebar color with brand blue and use white logo
```
---
### Task 2: Simplify Subscriptions Navigation
**Files:**
- Modify: `apps/portal/src/components/organisms/AppShell/navigation.ts` (lines 29-94)
- Modify: `apps/portal/src/components/organisms/AppShell/AppShell.tsx` (remove subscription data dependency)
**Step 1: Change Subscriptions to a direct link**
In `navigation.ts`, change the Subscriptions entry from expandable to direct:
```ts
// Before (lines 41-44)
{
name: "Subscriptions",
icon: ServerIcon,
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }],
},
// After
{
name: "Subscriptions",
href: "/account/subscriptions",
icon: ServerIcon,
},
```
**Step 2: Remove `computeNavigation` function**
Delete the `computeNavigation` function (lines 63-89) and the `truncate` helper (lines 91-94). Export only `baseNavigation`.
**Step 3: Remove the `Subscription` import**
Remove line 1: `import type { Subscription } from "@customer-portal/domain/subscriptions";`
This also fixes the TypeScript error `Cannot find module '@customer-portal/domain/subscriptions'`.
**Step 4: Update AppShell to use `baseNavigation` directly**
In `AppShell.tsx`, find where `computeNavigation` is called and replace with `baseNavigation`:
```ts
// Before
import { computeNavigation } from "./navigation";
// ... later
const navigation = computeNavigation(activeSubscriptions);
// After
import { baseNavigation } from "./navigation";
// ... later — use baseNavigation directly, remove activeSubscriptions fetch
```
Remove any hook/fetch for active subscriptions that was only used for sidebar navigation.
**Step 5: Verify**
Run: `pnpm type-check`
Expected: No errors related to navigation.ts or AppShell.tsx
**Step 6: Commit**
```
refactor: simplify subscriptions to direct sidebar link
```
---
### Task 3: Update PageLayout Template
**Files:**
- Modify: `apps/portal/src/components/templates/PageLayout/PageLayout.tsx`
**Step 1: Update PageLayout interface**
```tsx
// Remove BreadcrumbItem export and breadcrumbs prop
// Add backLink prop
interface PageLayoutProps {
icon?: ReactNode | undefined;
title: string;
description?: string | undefined; // keep prop but deprecate usage
actions?: ReactNode | undefined;
backLink?: { label: string; href: string } | undefined;
statusPill?: ReactNode | undefined; // new: renders next to title
loading?: boolean | undefined;
loadingFallback?: ReactNode | undefined;
error?: Error | string | null | undefined;
onRetry?: (() => void) | undefined;
children: ReactNode;
}
```
**Step 2: Rewrite the header section**
Replace the entire header block (lines 40-99) with:
```tsx
{
/* Back link — detail pages only */
}
{
backLink && (
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 pt-[var(--cp-space-md)]">
<Link
href={backLink.href}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200"
>
<ArrowLeftIcon className="h-4 w-4" />
{backLink.label}
</Link>
</div>
);
}
{
/* Header with muted background */
}
<div className="bg-muted/40 border-b border-border/40">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-4">
<div className="flex items-center justify-between gap-4">
{/* Left: icon + title + status */}
<div className="flex items-center gap-3 min-w-0">
{icon && <div className="h-7 w-7 sm:h-8 sm:w-8 text-primary flex-shrink-0">{icon}</div>}
<div className="min-w-0">
<div className="flex items-center gap-3">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight truncate">
{title}
</h1>
{statusPill}
</div>
{description && (
<p className="text-sm text-muted-foreground mt-0.5 leading-relaxed line-clamp-1">
{description}
</p>
)}
</div>
</div>
{/* Right: actions */}
{actions && <div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">{actions}</div>}
</div>
</div>
</div>;
```
**Step 3: Update outer wrapper structure**
The header now has its own bg, so it should sit outside the content container. Restructure:
```tsx
export function PageLayout({ ... }: PageLayoutProps) {
return (
<div>
{/* Back link */}
{backLink && ( ... )}
{/* Header with muted background */}
<div className="bg-muted/40 border-b border-border/40">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-4">
...header content...
</div>
</div>
{/* Content */}
<div className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)]">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8">
<div className="space-y-6">
{renderPageContent({ loading, error: error ?? undefined, children, onRetry, loadingFallback })}
</div>
</div>
</div>
</div>
);
}
```
**Step 4: Add ArrowLeftIcon import**
```tsx
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
```
**Step 5: Remove old breadcrumb code and BreadcrumbItem export**
Delete the `BreadcrumbItem` interface and all breadcrumb rendering code. Keep the `ChevronRightIcon` import removal too.
**Step 6: Verify**
Run: `pnpm type-check`
Expected: May show errors in files still passing `breadcrumbs` prop — those get fixed in later tasks.
**Step 7: Commit**
```
refactor: update PageLayout with muted header bg and backLink support
```
---
### Task 4: Create SectionCard Molecule
**Files:**
- Create: `apps/portal/src/components/molecules/SectionCard/SectionCard.tsx`
- Create: `apps/portal/src/components/molecules/SectionCard/index.ts`
- Modify: `apps/portal/src/components/molecules/index.ts` (add export)
**Step 1: Create SectionCard component**
```tsx
// apps/portal/src/components/molecules/SectionCard/SectionCard.tsx
import type { ReactNode } from "react";
import { cn } from "@/shared/utils";
type SectionTone = "primary" | "success" | "info" | "warning" | "danger" | "neutral";
const toneStyles: Record<SectionTone, string> = {
primary: "bg-primary/10 text-primary",
success: "bg-success/10 text-success",
info: "bg-info/10 text-info",
warning: "bg-warning/10 text-warning",
danger: "bg-danger/10 text-danger",
neutral: "bg-neutral/10 text-neutral",
};
interface SectionCardProps {
icon: ReactNode;
title: string;
subtitle?: string | undefined;
tone?: SectionTone;
actions?: ReactNode | undefined;
children: ReactNode;
className?: string | undefined;
}
export function SectionCard({
icon,
title,
subtitle,
tone = "primary",
actions,
children,
className,
}: SectionCardProps) {
return (
<div
className={cn(
"bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden",
className
)}
>
{/* Header */}
<div className="bg-muted/40 px-6 py-4 border-b border-border/40">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div
className={cn(
"h-9 w-9 rounded-lg flex items-center justify-center flex-shrink-0",
toneStyles[tone]
)}
>
{icon}
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
{subtitle && <p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>}
</div>
</div>
{actions && <div className="flex items-center gap-2 flex-shrink-0">{actions}</div>}
</div>
</div>
{/* Content */}
<div className="px-6 py-5">{children}</div>
</div>
);
}
```
**Step 2: Create barrel export**
```tsx
// apps/portal/src/components/molecules/SectionCard/index.ts
export { SectionCard } from "./SectionCard";
```
**Step 3: Add to molecules index**
In `apps/portal/src/components/molecules/index.ts`, add:
```tsx
export { SectionCard } from "./SectionCard";
```
**Step 4: Verify**
Run: `pnpm type-check`
Expected: No errors.
**Step 5: Commit**
```
feat: add SectionCard molecule for unified detail page sections
```
---
### Task 5: Update Subscriptions List View
**Files:**
- Modify: `apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx`
- Modify: `apps/portal/src/features/subscriptions/components/SubscriptionGridCard.tsx`
**Step 1: Remove description from PageLayout call**
```tsx
// Before
<PageLayout icon={<Server />} title="Subscriptions" description="Manage your active subscriptions">
// After
<PageLayout icon={<Server />} title="Subscriptions">
```
Do this for both the loading state and the main render.
**Step 2: Standardize content card borders**
```tsx
// Before
<div className="bg-card rounded-xl border border-border/60 shadow-[var(--cp-shadow-1)] overflow-hidden">
// After
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
```
**Step 3: Add opacity dimming for inactive subscriptions in SubscriptionGridCard**
In `SubscriptionGridCard.tsx`, wrap the card Link with conditional opacity:
```tsx
// Add to the Link className, conditionally:
const isInactive = ["Completed", "Cancelled", "Terminated"].includes(subscription.status);
// In the className of the Link:
className={cn(
"group flex flex-col p-4 rounded-xl bg-card border border-border transition-all duration-200 hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
isInactive && "opacity-60"
)}
```
Also standardize card border from `border-border/60` to `border-border`.
**Step 4: Verify**
Run: `pnpm type-check`
**Step 5: Commit**
```
style: clean up subscriptions list with unified patterns
```
---
### Task 6: Update Subscription Detail View
**Files:**
- Modify: `apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx`
**Step 1: Replace breadcrumbs with backLink**
```tsx
// Before
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: subscription.productName },
]}
// After
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
```
**Step 2: Move cancel button to PageLayout actions**
Replace any inline-styled cancel button with:
```tsx
actions={
subscription.status === "Active" && isInternetService ? (
<Button variant="destructive" size="sm" as="a" href={`/account/subscriptions/${subscription.id}/cancel`}>
Cancel Service
</Button>
) : undefined
}
```
Remove the old inline cancel button from the body.
**Step 3: Use SectionCard for billing section**
```tsx
import { SectionCard } from "@/components/molecules/SectionCard";
// Replace the billing card with:
<SectionCard
icon={<CreditCardIcon className="h-5 w-5" />}
title="Billing Information"
subtitle="Payment and invoices"
tone="primary"
actions={
<Link
href="/account/billing/invoices"
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
>
View Invoices
</Link>
}
>
{/* billing content */}
</SectionCard>;
```
**Step 4: Remove description from PageLayout**
Drop `description` prop from PageLayout usage.
**Step 5: Verify**
Run: `pnpm type-check`
**Step 6: Commit**
```
style: update subscription detail with backLink and SectionCard
```
---
### Task 7: Update Orders Views
**Files:**
- Modify: `apps/portal/src/features/orders/views/OrdersList.tsx`
- Modify: `apps/portal/src/features/orders/views/OrderDetail.tsx`
**Step 1: OrdersList — remove description, standardize card borders**
Remove `description` prop from PageLayout. Change any `border-border/60` to `border-border`. Replace inline status badge classes with `StatusPill` component.
**Step 2: OrderDetail — replace breadcrumbs with backLink**
```tsx
// Before
breadcrumbs={[{ label: "Orders", href: "/account/orders" }, { label: `Order #${id}` }]}
// After
backLink={{ label: "Back to Orders", href: "/account/orders" }}
```
Remove `description` prop. Use `SectionCard` for order item sections.
**Step 3: Remove any custom title sizes**
If OrderDetail has its own `text-2xl` title styling, remove it — let PageLayout handle the title.
**Step 4: Verify**
Run: `pnpm type-check`
**Step 5: Commit**
```
style: update order views with unified patterns
```
---
### Task 8: Update Billing Views
**Files:**
- Modify: `apps/portal/src/features/billing/views/InvoicesList.tsx`
- Modify: `apps/portal/src/features/billing/views/InvoiceDetail.tsx`
- Modify: `apps/portal/src/features/billing/views/PaymentMethods.tsx`
**Step 1: InvoicesList — remove description**
Drop `description` prop from PageLayout.
**Step 2: InvoiceDetail — replace breadcrumbs with backLink**
```tsx
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
```
Remove `description`. Use `SectionCard` for invoice detail sections where appropriate.
**Step 3: PaymentMethods — remove description**
Drop `description` prop.
**Step 4: Verify**
Run: `pnpm type-check`
**Step 5: Commit**
```
style: update billing views with unified patterns
```
---
### Task 9: Update Support Views
**Files:**
- Modify: `apps/portal/src/features/support/views/SupportCasesView.tsx`
- Modify: `apps/portal/src/features/support/views/SupportCaseDetailView.tsx`
- Modify: `apps/portal/src/features/support/views/NewSupportCaseView.tsx`
**Step 1: SupportCasesView — remove description, fix inline badges**
Remove `description` and `breadcrumbs` props from PageLayout. Replace inline badge classes (`inline-flex text-xs px-2 py-0.5 rounded font-medium`) with `<StatusPill>` component.
**Step 2: SupportCaseDetailView — replace breadcrumbs with backLink**
```tsx
backLink={{ label: "Back to Cases", href: "/account/support" }}
```
Remove `description`. Use `SectionCard` for conversation and meta sections.
**Step 3: NewSupportCaseView — replace breadcrumbs with backLink**
```tsx
backLink={{ label: "Back to Cases", href: "/account/support" }}
```
Remove `description`.
**Step 4: Verify**
Run: `pnpm type-check`
**Step 5: Commit**
```
style: update support views with unified patterns
```
---
### Task 10: Update Remaining Views
**Files:**
- Modify: `apps/portal/src/features/dashboard/views/DashboardView.tsx`
- Modify: `apps/portal/src/features/services/views/AccountServicesOverview.tsx`
- Modify: `apps/portal/src/features/account/views/ProfileContainer.tsx`
- Modify: `apps/portal/src/features/verification/views/ResidenceCardVerificationSettingsView.tsx`
- Modify: `apps/portal/src/features/subscriptions/views/SimReissue.tsx`
- Modify: `apps/portal/src/features/subscriptions/views/SimTopUp.tsx`
- Modify: `apps/portal/src/features/subscriptions/views/SimChangePlan.tsx`
- Modify: `apps/portal/src/features/subscriptions/views/SimCallHistory.tsx`
- Modify: `apps/portal/src/features/subscriptions/views/CancelSubscription.tsx`
- Modify: `apps/portal/src/features/subscriptions/components/CancellationFlow/CancellationFlow.tsx`
- Modify: `apps/portal/src/features/checkout/components/AccountCheckoutContainer.tsx`
- Modify: `apps/portal/src/features/services/components/sim/SimConfigureView.tsx`
- Modify: `apps/portal/src/features/services/components/internet/configure/InternetConfigureContainer.tsx`
**Step 1: Remove description from all remaining PageLayout usages**
For every file listed above, remove the `description` prop from `<PageLayout>`.
**Step 2: Replace breadcrumbs with backLink where applicable**
- `ResidenceCardVerificationSettingsView.tsx`: `backLink={{ label: "Back to Settings", href: "/account/settings" }}`
- `CancelSubscription.tsx` / `CancellationFlow.tsx`: `backLink={{ label: "Back to Subscription", href: "/account/subscriptions/{id}" }}`
- SIM views (Reissue, TopUp, ChangePlan, CallHistory): `backLink={{ label: "Back to Subscription", href: "/account/subscriptions/{id}" }}`
**Step 3: Verify**
Run: `pnpm type-check`
Expected: No errors. All `breadcrumbs` prop usages should be gone.
**Step 4: Commit**
```
style: remove descriptions and breadcrumbs from all remaining views
```
---
### Task 11: Fix Domain Build & Clean Up
**Files:**
- Modify: `apps/portal/src/features/subscriptions/components/SubscriptionGridCard.tsx` (if import still broken)
- Modify: `apps/portal/src/components/templates/PageLayout/PageLayout.tsx` (remove dead BreadcrumbItem type if not already)
**Step 1: Build domain package**
Run: `pnpm domain:build`
Expected: Successful build, resolves `@customer-portal/domain/subscriptions` import errors.
**Step 2: Remove BreadcrumbItem from PageLayout exports**
If `BreadcrumbItem` is still exported, remove it. Check if any file imports it:
Run: `grep -r "BreadcrumbItem" apps/portal/src/`
Remove any remaining imports.
**Step 3: Full type check**
Run: `pnpm type-check`
Expected: Clean — no errors.
**Step 4: Lint check**
Run: `pnpm lint`
Expected: Clean or only pre-existing warnings.
**Step 5: Commit**
```
chore: fix domain build and remove dead breadcrumb types
```
---
### Task 12: Final Visual Verification
**Step 1: Start dev server (with user permission)**
Run: `pnpm --filter @customer-portal/portal dev`
**Step 2: Verify each page visually**
Check these pages match the design:
- [ ] Sidebar: deeper blue, white logo, Subscriptions as direct link
- [ ] Dashboard (`/account`): muted header bg, no description
- [ ] Subscriptions list (`/account/subscriptions`): muted header, metrics row, unified content card, inactive subs dimmed
- [ ] Subscription detail (`/account/subscriptions/{id}`): back link, header with actions, SectionCard for billing
- [ ] Orders list (`/account/orders`): consistent with subscriptions list pattern
- [ ] Order detail (`/account/orders/{id}`): back link, SectionCards
- [ ] Invoices (`/account/billing/invoices`): consistent list pattern
- [ ] Invoice detail: back link, consistent
- [ ] Support cases (`/account/support`): StatusPill badges, no inline badges
- [ ] Support detail: back link, consistent
- [ ] Settings (`/account/settings`): muted header
**Step 3: Fix any visual issues found**
Address spacing, alignment, or color issues discovered during review.
**Step 4: Final commit**
```
style: portal UI cleanup — unified page patterns and brand alignment
```