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:
parent
57f2c543d1
commit
cab58d1c5b
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -228,7 +228,6 @@
|
|||||||
@theme {
|
@theme {
|
||||||
/* Font Families */
|
/* Font Families */
|
||||||
--font-family-sans: var(--font-sans);
|
--font-family-sans: var(--font-sans);
|
||||||
--font-family-heading: var(--font-display);
|
|
||||||
--font-family-mono: var(--font-mono);
|
--font-family-mono: var(--font-mono);
|
||||||
|
|
||||||
/* Colors */
|
/* Colors */
|
||||||
|
|||||||
@ -25,12 +25,18 @@ export function Logo({ className = "", size = 32 }: LogoProps) {
|
|||||||
aria-label="Assist Solutions Logo"
|
aria-label="Assist Solutions Logo"
|
||||||
>
|
>
|
||||||
{/* Top section - Light blue curved arrows */}
|
{/* 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="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="#60A5FA" />
|
<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 */}
|
{/* 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
|
||||||
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { SectionCard } from "./SectionCard";
|
||||||
@ -27,6 +27,7 @@ export * from "./SummaryStats";
|
|||||||
export * from "./FilterDropdown";
|
export * from "./FilterDropdown";
|
||||||
export * from "./ClearFiltersButton";
|
export * from "./ClearFiltersButton";
|
||||||
export * from "./DetailStatsGrid";
|
export * from "./DetailStatsGrid";
|
||||||
|
export { SectionCard } from "./SectionCard";
|
||||||
|
|
||||||
// Metric display
|
// Metric display
|
||||||
export { MetricCard, MetricCardSkeleton, type MetricCardProps } from "./MetricCard";
|
export { MetricCard, MetricCardSkeleton, type MetricCardProps } from "./MetricCard";
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useAuthStore, useAuthSession } from "@/features/auth/stores/auth.store";
|
import { useAuthStore, useAuthSession } from "@/features/auth/stores/auth.store";
|
||||||
import { useActiveSubscriptions } from "@/features/subscriptions/hooks";
|
|
||||||
import { accountService } from "@/features/account/api/account.api";
|
import { accountService } from "@/features/account/api/account.api";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
import { computeNavigation, type NavigationItem } from "./navigation";
|
import { baseNavigation, type NavigationItem } from "./navigation";
|
||||||
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
|
||||||
|
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -152,12 +150,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, isAuthReady, isCheckingAuth } = useAppShellAuth(pathname);
|
const { user, isAuthReady, isCheckingAuth } = useAppShellAuth(pathname);
|
||||||
const { expandedItems, toggleExpanded } = useSidebarExpansion(pathname);
|
const { expandedItems, toggleExpanded } = useSidebarExpansion(pathname);
|
||||||
const activeSubscriptionsQuery = useActiveSubscriptions();
|
const navigation = baseNavigation;
|
||||||
const activeSubscriptions: Subscription[] = useMemo(
|
|
||||||
() => activeSubscriptionsQuery.data ?? [],
|
|
||||||
[activeSubscriptionsQuery.data]
|
|
||||||
);
|
|
||||||
const navigation = useMemo(() => computeNavigation(activeSubscriptions), [activeSubscriptions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -67,9 +67,7 @@ export const Sidebar = memo(function Sidebar({
|
|||||||
<div className="flex flex-col h-0 flex-1 bg-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 flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
|
||||||
<div className="flex items-center space-x-3">
|
<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={28} />
|
||||||
<Logo size={22} />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
|
<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>
|
<p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
@ -40,8 +39,8 @@ export const baseNavigation: NavigationItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Subscriptions",
|
name: "Subscriptions",
|
||||||
|
href: "/account/subscriptions",
|
||||||
icon: ServerIcon,
|
icon: ServerIcon,
|
||||||
children: [{ name: "All Subscriptions", href: "/account/subscriptions" }],
|
|
||||||
},
|
},
|
||||||
{ name: "Services", href: "/account/services", icon: Squares2X2Icon },
|
{ name: "Services", href: "/account/services", icon: Squares2X2Icon },
|
||||||
{
|
{
|
||||||
@ -59,36 +58,3 @@ export const baseNavigation: NavigationItem[] = [
|
|||||||
},
|
},
|
||||||
{ name: "Log out", href: "#", icon: ArrowRightStartOnRectangleIcon, isLogout: true },
|
{ 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)) + "…";
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import Link from "next/link";
|
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 { Skeleton } from "@/components/atoms/loading-skeleton";
|
||||||
import { ErrorState } from "@/components/atoms/error-state";
|
import { ErrorState } from "@/components/atoms/error-state";
|
||||||
|
|
||||||
export interface BreadcrumbItem {
|
|
||||||
label: string;
|
|
||||||
href?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageLayoutProps {
|
interface PageLayoutProps {
|
||||||
icon?: ReactNode | undefined;
|
icon?: ReactNode | undefined;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
actions?: ReactNode | undefined;
|
actions?: ReactNode | undefined;
|
||||||
breadcrumbs?: BreadcrumbItem[] | undefined;
|
backLink?: { label: string; href: string } | undefined;
|
||||||
|
statusPill?: ReactNode | undefined;
|
||||||
loading?: boolean | undefined;
|
loading?: boolean | undefined;
|
||||||
loadingFallback?: ReactNode | undefined;
|
loadingFallback?: ReactNode | undefined;
|
||||||
error?: Error | string | null | undefined;
|
error?: Error | string | null | undefined;
|
||||||
@ -27,7 +23,8 @@ export function PageLayout({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
actions,
|
actions,
|
||||||
breadcrumbs,
|
backLink,
|
||||||
|
statusPill,
|
||||||
loading = false,
|
loading = false,
|
||||||
loadingFallback,
|
loadingFallback,
|
||||||
error = null,
|
error = null,
|
||||||
@ -35,42 +32,24 @@ export function PageLayout({
|
|||||||
children,
|
children,
|
||||||
}: PageLayoutProps) {
|
}: PageLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)] md:py-[var(--cp-space-2xl)]">
|
<div>
|
||||||
<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">
|
{/* Header band with subtle background */}
|
||||||
{/* Breadcrumbs - scrollable on mobile */}
|
<div className="bg-muted/40 border-b border-border/40">
|
||||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
<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)]">
|
||||||
<nav
|
{/* Back link */}
|
||||||
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"
|
{backLink && (
|
||||||
aria-label="Breadcrumb"
|
<div className="mb-3">
|
||||||
>
|
|
||||||
<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 ? (
|
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={backLink.href}
|
||||||
className="hover:text-foreground transition-colors duration-200 py-1 px-0.5 -mx-0.5 rounded"
|
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>
|
</Link>
|
||||||
) : (
|
</div>
|
||||||
<span className="text-foreground font-medium py-1" aria-current="page">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Header */}
|
<div className="flex items-start justify-between gap-4 min-w-0">
|
||||||
<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 min-w-0 flex-1">
|
<div className="flex items-start min-w-0 flex-1">
|
||||||
{icon && (
|
{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">
|
<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>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<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">
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
{statusPill}
|
||||||
|
</div>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm text-muted-foreground mt-1 leading-relaxed line-clamp-2 sm:line-clamp-none">
|
<p className="text-sm text-muted-foreground mt-1 leading-relaxed line-clamp-2 sm:line-clamp-none">
|
||||||
{description}
|
{description}
|
||||||
@ -88,17 +70,18 @@ export function PageLayout({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions - full width on mobile, stacks buttons */}
|
|
||||||
{actions && (
|
{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}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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)]">
|
<div className="space-y-[var(--cp-space-xl)] sm:space-y-[var(--cp-space-2xl)]">
|
||||||
{renderPageContent({
|
{renderPageContent({
|
||||||
loading,
|
loading,
|
||||||
@ -110,6 +93,7 @@ export function PageLayout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export { PageLayout } from "./PageLayout";
|
export { PageLayout } from "./PageLayout";
|
||||||
export type { BreadcrumbItem } from "./PageLayout";
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export { AuthLayout } from "./AuthLayout/AuthLayout";
|
|||||||
export type { AuthLayoutProps } from "./AuthLayout/AuthLayout";
|
export type { AuthLayoutProps } from "./AuthLayout/AuthLayout";
|
||||||
|
|
||||||
export { PageLayout } from "./PageLayout/PageLayout";
|
export { PageLayout } from "./PageLayout/PageLayout";
|
||||||
export type { BreadcrumbItem } from "./PageLayout/PageLayout";
|
|
||||||
|
|
||||||
export { PublicShell } from "./PublicShell/PublicShell";
|
export { PublicShell } from "./PublicShell/PublicShell";
|
||||||
export type { PublicShellProps } from "./PublicShell/PublicShell";
|
export type { PublicShellProps } from "./PublicShell/PublicShell";
|
||||||
|
|||||||
@ -124,25 +124,14 @@ export default function ProfileContainer() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<UserIcon />} title="Profile" loading>
|
||||||
icon={<UserIcon />}
|
|
||||||
title="Profile"
|
|
||||||
description="Manage your account information"
|
|
||||||
loading
|
|
||||||
>
|
|
||||||
<ProfileLoadingSkeleton />
|
<ProfileLoadingSkeleton />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<UserIcon />} title="Profile" error={error} onRetry={reload}>
|
||||||
icon={<UserIcon />}
|
|
||||||
title="Profile"
|
|
||||||
description="Manage your account information"
|
|
||||||
error={error}
|
|
||||||
onRetry={reload}
|
|
||||||
>
|
|
||||||
{error && (
|
{error && (
|
||||||
<AlertBanner variant="error" title="Unable to load profile" className="mb-6" elevated>
|
<AlertBanner variant="error" title="Unable to load profile" className="mb-6" elevated>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
|
||||||
import { ErrorState } from "@/components/atoms/error-state";
|
import { ErrorState } from "@/components/atoms/error-state";
|
||||||
@ -18,11 +17,7 @@ import {
|
|||||||
|
|
||||||
function InvoiceDetailSkeleton() {
|
function InvoiceDetailSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<DocumentTextIcon />} title="Invoice">
|
||||||
icon={<DocumentTextIcon />}
|
|
||||||
title="Invoice"
|
|
||||||
description="Invoice details and actions"
|
|
||||||
>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<LoadingCard />
|
<LoadingCard />
|
||||||
<div className="bg-card text-card-foreground rounded-xl border border-border p-6 space-y-4 shadow-[var(--cp-shadow-1)]">
|
<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
|
<PageLayout
|
||||||
icon={<DocumentTextIcon />}
|
icon={<DocumentTextIcon />}
|
||||||
title="Invoice"
|
title="Invoice"
|
||||||
description="Invoice details and actions"
|
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
|
||||||
>
|
>
|
||||||
<ErrorState
|
<ErrorState
|
||||||
title="Error loading invoice"
|
title="Error loading invoice"
|
||||||
message={error instanceof Error ? error.message : "Invoice not found"}
|
message={error instanceof Error ? error.message : "Invoice not found"}
|
||||||
variant="page"
|
variant="page"
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
|
||||||
<Link href="/account/billing/invoices" className="text-primary font-medium">
|
|
||||||
← Back to invoices
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,12 +96,7 @@ export function InvoiceDetailContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<DocumentTextIcon />}
|
icon={<DocumentTextIcon />}
|
||||||
title={`Invoice #${invoice.id}`}
|
title={`Invoice #${invoice.id}`}
|
||||||
description="Invoice details and actions"
|
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
|
||||||
breadcrumbs={[
|
|
||||||
{ label: "Billing", href: "/account/billing/invoices" },
|
|
||||||
{ label: "Invoices", href: "/account/billing/invoices" },
|
|
||||||
{ label: `#${invoice.id}` },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-card text-card-foreground rounded-xl shadow-[var(--cp-shadow-1)] border border-border overflow-hidden">
|
<div className="bg-card text-card-foreground rounded-xl shadow-[var(--cp-shadow-1)] border border-border overflow-hidden">
|
||||||
|
|||||||
@ -6,11 +6,7 @@ import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceL
|
|||||||
|
|
||||||
export function InvoicesListContainer() {
|
export function InvoicesListContainer() {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<CreditCardIcon />} title="Invoices">
|
||||||
icon={<CreditCardIcon />}
|
|
||||||
title="Invoices"
|
|
||||||
description="Manage and view your billing invoices"
|
|
||||||
>
|
|
||||||
<InvoicesList />
|
<InvoicesList />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -235,11 +235,7 @@ export function PaymentMethodsContainer() {
|
|||||||
|
|
||||||
if (combinedError) {
|
if (combinedError) {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<CreditCardIcon />} title="Payment Methods">
|
||||||
icon={<CreditCardIcon />}
|
|
||||||
title="Payment Methods"
|
|
||||||
description="Manage your saved payment methods and billing information"
|
|
||||||
>
|
|
||||||
<AsyncBlock error={combinedError} variant="page">
|
<AsyncBlock error={combinedError} variant="page">
|
||||||
<></>
|
<></>
|
||||||
</AsyncBlock>
|
</AsyncBlock>
|
||||||
@ -254,11 +250,7 @@ export function PaymentMethodsContainer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<CreditCardIcon />} title="Payment Methods">
|
||||||
icon={<CreditCardIcon />}
|
|
||||||
title="Payment Methods"
|
|
||||||
description="Manage your saved payment methods and billing information"
|
|
||||||
>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<InlineToast
|
<InlineToast
|
||||||
visible={paymentRefresh.toast.visible}
|
visible={paymentRefresh.toast.visible}
|
||||||
|
|||||||
@ -193,11 +193,7 @@ export function AccountCheckoutContainer() {
|
|||||||
if (!cartItem || !orderType) return <CheckoutErrorFallback shopHref={getShopHref(pathname)} />;
|
if (!cartItem || !orderType) return <CheckoutErrorFallback shopHref={getShopHref(pathname)} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout title="Checkout" icon={<ShieldCheck className="h-6 w-6" />}>
|
||||||
title="Checkout"
|
|
||||||
description="Verify your address, review totals, and submit your order"
|
|
||||||
icon={<ShieldCheck className="h-6 w-6" />}
|
|
||||||
>
|
|
||||||
<div className="max-w-2xl mx-auto space-y-8">
|
<div className="max-w-2xl mx-auto space-y-8">
|
||||||
<InlineToast
|
<InlineToast
|
||||||
visible={payment.paymentRefresh.toast.visible}
|
visible={payment.paymentRefresh.toast.visible}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { useInternetEligibility } from "@/features/services/hooks";
|
|||||||
|
|
||||||
function DashboardSkeleton() {
|
function DashboardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Dashboard" description="Overview of your account" loading>
|
<PageLayout title="Dashboard" loading>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="h-4 cp-skeleton-shimmer rounded w-24" />
|
<div className="h-4 cp-skeleton-shimmer rounded w-24" />
|
||||||
@ -167,7 +167,7 @@ function DashboardContent({
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Dashboard" description="Overview of your account">
|
<PageLayout title="Dashboard">
|
||||||
<InlineToast
|
<InlineToast
|
||||||
visible={eligibilityToast.visible}
|
visible={eligibilityToast.visible}
|
||||||
text={eligibilityToast.text}
|
text={eligibilityToast.text}
|
||||||
@ -222,7 +222,7 @@ export function DashboardView() {
|
|||||||
if (authLoading || summaryLoading) return <DashboardSkeleton />;
|
if (authLoading || summaryLoading) return <DashboardSkeleton />;
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Dashboard" description="Overview of your account">
|
<PageLayout title="Dashboard">
|
||||||
<ErrorState
|
<ErrorState
|
||||||
title="Error loading dashboard"
|
title="Error loading dashboard"
|
||||||
message={getErrorMessageText(error)}
|
message={getErrorMessageText(error)}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { StatusPill } from "@/components/atoms/status-pill";
|
import { StatusPill } from "@/components/atoms/status-pill";
|
||||||
import { DetailStatsGrid, type StatGridItem } from "@/components/molecules";
|
import { DetailStatsGrid, type StatGridItem } from "@/components/molecules";
|
||||||
|
import { SectionCard } from "@/components/molecules/SectionCard";
|
||||||
import { ordersService } from "@/features/orders/api/orders.api";
|
import { ordersService } from "@/features/orders/api/orders.api";
|
||||||
import { useOrderUpdates } from "@/features/orders/hooks/useOrderUpdates";
|
import { useOrderUpdates } from "@/features/orders/hooks/useOrderUpdates";
|
||||||
import {
|
import {
|
||||||
@ -486,15 +487,14 @@ function OrderDetailLoadedContent({
|
|||||||
items={buildStatsGridItems(statusDescriptor, statusPillVariant, totals, placedDateShort)}
|
items={buildStatsGridItems(statusDescriptor, statusPillVariant, totals, placedDateShort)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]">
|
<SectionCard icon={<ClockIcon className="h-5 w-5" />} title="Order Progress" tone="info">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground mb-4">Order Progress</h3>
|
|
||||||
{statusDescriptor && (
|
{statusDescriptor && (
|
||||||
<OrderProgressTimeline
|
<OrderProgressTimeline
|
||||||
serviceCategory={serviceCategory}
|
serviceCategory={serviceCategory}
|
||||||
currentState={statusDescriptor.state}
|
currentState={statusDescriptor.state}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SectionCard>
|
||||||
|
|
||||||
<div className="rounded-xl border border-border bg-card text-card-foreground shadow-[var(--cp-shadow-1)]">
|
<div className="rounded-xl border border-border bg-card text-card-foreground shadow-[var(--cp-shadow-1)]">
|
||||||
<OrderDetailHeader
|
<OrderDetailHeader
|
||||||
@ -551,15 +551,12 @@ export function OrderDetailContainer() {
|
|||||||
useOrderUpdates(params.id, handleOrderUpdate);
|
useOrderUpdates(params.id, handleOrderUpdate);
|
||||||
|
|
||||||
const pageTitle = data ? `${data.orderType} Service Order` : "Order Details";
|
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 (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ClipboardDocumentCheckIcon />}
|
icon={<ClipboardDocumentCheckIcon />}
|
||||||
title={pageTitle}
|
title={pageTitle}
|
||||||
description={pageDescription}
|
backLink={{ label: "Back to Orders", href: "/account/orders" }}
|
||||||
breadcrumbs={[{ label: "Orders", href: "/account/orders" }, { label: breadcrumbLabel }]}
|
|
||||||
>
|
>
|
||||||
{error && <div className="mb-4 text-sm text-danger">{error}</div>}
|
{error && <div className="mb-4 text-sm text-danger">{error}</div>}
|
||||||
{isNewOrder && <NewOrderBanner />}
|
{isNewOrder && <NewOrderBanner />}
|
||||||
|
|||||||
@ -224,11 +224,7 @@ export function OrdersListContainer() {
|
|||||||
const summaryStatsItems = useMemo(() => buildOrderSummaryStats(stats), [stats]);
|
const summaryStatsItems = useMemo(() => buildOrderSummaryStats(stats), [stats]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<ClipboardDocumentListIcon />} title="My Orders">
|
||||||
icon={<ClipboardDocumentListIcon />}
|
|
||||||
title="My Orders"
|
|
||||||
description="View and track all your orders"
|
|
||||||
>
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<OrdersSuccessBanner />
|
<OrdersSuccessBanner />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@ -198,7 +198,7 @@ export function InternetConfigureContainer({
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ServerIcon />}
|
icon={<ServerIcon />}
|
||||||
title="Configure Internet Service"
|
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">
|
<div className="text-center py-12">
|
||||||
<p className="text-muted-foreground">Plan not found</p>
|
<p className="text-muted-foreground">Plan not found</p>
|
||||||
@ -228,7 +228,7 @@ export function InternetConfigureContainer({
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ServerIcon />}
|
icon={<ServerIcon />}
|
||||||
title="Configure Internet Service"
|
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="min-h-[70vh]">
|
||||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|||||||
@ -5,11 +5,7 @@ import { ServerIcon } from "@heroicons/react/24/outline";
|
|||||||
|
|
||||||
export function ConfigureLoadingSkeleton() {
|
export function ConfigureLoadingSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<ServerIcon />} title="Configure Internet Service">
|
||||||
icon={<ServerIcon />}
|
|
||||||
title="Configure Internet Service"
|
|
||||||
description="Set up your internet service options"
|
|
||||||
>
|
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
{/* Back to plans */}
|
{/* Back to plans */}
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
|
|||||||
@ -154,7 +154,7 @@ export function SimConfigureView(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title={`Configure ${plan.name}`}
|
title={`Configure ${plan.name}`}
|
||||||
description="Customize your mobile service"
|
backLink={{ label: "Back to Services", href: "/account/services" }}
|
||||||
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
|
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
|
||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
|
|||||||
@ -5,11 +5,7 @@ import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
|
|||||||
|
|
||||||
export function LoadingSkeleton() {
|
export function LoadingSkeleton() {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout title="Configure SIM" icon={<DevicePhoneMobileIcon className="h-6 w-6" />}>
|
||||||
title="Configure SIM"
|
|
||||||
description="Customize your mobile service"
|
|
||||||
icon={<DevicePhoneMobileIcon className="h-6 w-6" />}
|
|
||||||
>
|
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
{/* Header card skeleton */}
|
{/* Header card skeleton */}
|
||||||
<div className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]">
|
<div className="bg-card rounded-xl border border-border p-6 shadow-[var(--cp-shadow-1)]">
|
||||||
|
|||||||
@ -8,11 +8,7 @@ export function PlanNotFound() {
|
|||||||
const servicesBasePath = useServicesBasePath();
|
const servicesBasePath = useServicesBasePath();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout title="Plan Not Found" icon={<ExclamationTriangleIcon className="h-6 w-6" />}>
|
||||||
title="Plan Not Found"
|
|
||||||
description="The selected plan could not be found"
|
|
||||||
icon={<ExclamationTriangleIcon className="h-6 w-6" />}
|
|
||||||
>
|
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<ExclamationTriangleIcon className="h-12 w-12 mx-auto text-danger mb-4" />
|
<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>
|
<h2 className="text-xl font-semibold text-foreground mb-2">Plan Not Found</h2>
|
||||||
|
|||||||
@ -11,11 +11,7 @@ import { ServicesOverviewContent } from "@/features/services/components/common/S
|
|||||||
*/
|
*/
|
||||||
export function AccountServicesOverview() {
|
export function AccountServicesOverview() {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<Squares2X2Icon />} title="Services">
|
||||||
icon={<Squares2X2Icon />}
|
|
||||||
title="Services"
|
|
||||||
description="Browse and order connectivity services"
|
|
||||||
>
|
|
||||||
<ServicesOverviewContent basePath="/account/services" showHero={false} />
|
<ServicesOverviewContent basePath="/account/services" showHero={false} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState, type ReactNode } from "react";
|
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 { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
import { ArrowLeftIcon, CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@ -23,10 +22,6 @@ export interface CancellationFlowProps {
|
|||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
/** Page title */
|
/** Page title */
|
||||||
title: string;
|
title: string;
|
||||||
/** Page description / subtitle */
|
|
||||||
description: string;
|
|
||||||
/** Breadcrumb items */
|
|
||||||
breadcrumbs: BreadcrumbItem[];
|
|
||||||
/** Back link URL */
|
/** Back link URL */
|
||||||
backHref: string;
|
backHref: string;
|
||||||
/** Back link label */
|
/** Back link label */
|
||||||
@ -511,8 +506,6 @@ function StepRouter(props: StepRouterProps) {
|
|||||||
export function CancellationFlow({
|
export function CancellationFlow({
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
description,
|
|
||||||
breadcrumbs,
|
|
||||||
backHref,
|
backHref,
|
||||||
backLabel,
|
backLabel,
|
||||||
availableMonths,
|
availableMonths,
|
||||||
@ -577,20 +570,12 @@ export function CancellationFlow({
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
|
||||||
actions={headerActions}
|
actions={headerActions}
|
||||||
breadcrumbs={breadcrumbs}
|
backLink={{ label: backLabel, href: backHref }}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
>
|
>
|
||||||
<div className="max-w-2xl mx-auto">
|
<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}
|
{warningBanner}
|
||||||
<CancellationAlerts formError={formError} successMessage={successMessage} />
|
<CancellationAlerts formError={formError} successMessage={successMessage} />
|
||||||
<StepIndicator currentStep={step} />
|
<StepIndicator currentStep={step} />
|
||||||
|
|||||||
@ -34,12 +34,14 @@ export function SubscriptionGridCard({ subscription, className }: SubscriptionGr
|
|||||||
const { formatCurrency } = useFormatCurrency();
|
const { formatCurrency } = useFormatCurrency();
|
||||||
const statusIndicator = mapSubscriptionStatus(subscription.status);
|
const statusIndicator = mapSubscriptionStatus(subscription.status);
|
||||||
const cycleLabel = getBillingCycleLabel(subscription.cycle);
|
const cycleLabel = getBillingCycleLabel(subscription.cycle);
|
||||||
|
const isInactive = ["Completed", "Cancelled", "Terminated"].includes(subscription.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/account/subscriptions/${subscription.serviceId}`}
|
href={`/account/subscriptions/${subscription.serviceId}`}
|
||||||
className={cn(
|
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",
|
"transition-all duration-200",
|
||||||
"hover:border-border hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5",
|
"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",
|
"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() {
|
export function SubscriptionGridCardSkeleton() {
|
||||||
return (
|
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="flex items-start justify-between gap-2 mb-3">
|
||||||
<div className="space-y-1.5 flex-1">
|
<div className="space-y-1.5 flex-1">
|
||||||
<div className="h-4 cp-skeleton-shimmer rounded w-3/4" />
|
<div className="h-4 cp-skeleton-shimmer rounded w-3/4" />
|
||||||
|
|||||||
@ -43,12 +43,7 @@ function CancellationPendingView({
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={title}
|
title={title}
|
||||||
description={preview.serviceName}
|
backLink={{ label: "Back to Subscription", href: `/account/subscriptions/${subscriptionId}` }}
|
||||||
breadcrumbs={[
|
|
||||||
{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF },
|
|
||||||
{ label: preview.serviceName, href: `/account/subscriptions/${subscriptionId}` },
|
|
||||||
{ label: "Cancellation Status" },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<div className="bg-card border border-border rounded-xl p-6 sm:p-8">
|
<div className="bg-card border border-border rounded-xl p-6 sm:p-8">
|
||||||
@ -236,12 +231,6 @@ function CancellationFlowView({
|
|||||||
<CancellationFlow
|
<CancellationFlow
|
||||||
icon={getServiceIcon(preview.serviceType)}
|
icon={getServiceIcon(preview.serviceType)}
|
||||||
title={getServiceTitle(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}`}
|
backHref={`/account/subscriptions/${subscriptionId}`}
|
||||||
backLabel="Back to Subscription"
|
backLabel="Back to Subscription"
|
||||||
availableMonths={preview.availableMonths}
|
availableMonths={preview.availableMonths}
|
||||||
@ -316,8 +305,7 @@ export function CancelSubscriptionContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<GlobeAltIcon />}
|
icon={<GlobeAltIcon />}
|
||||||
title="Cancel Subscription"
|
title="Cancel Subscription"
|
||||||
description="Loading cancellation information..."
|
backLink={{ label: "Back to Subscriptions", href: SUBSCRIPTIONS_HREF }}
|
||||||
breadcrumbs={[{ label: "Subscriptions", href: SUBSCRIPTIONS_HREF }, { label: "Cancel" }]}
|
|
||||||
loading={state.loading}
|
loading={state.loading}
|
||||||
error={state.error}
|
error={state.error}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export function SimCallHistoryContainer(): React.ReactElement {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<PhoneIcon />}
|
icon={<PhoneIcon />}
|
||||||
title="Call & SMS History"
|
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="max-w-5xl mx-auto">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@ -238,7 +238,7 @@ export function SimChangePlanContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<DevicePhoneMobileIcon />}
|
icon={<DevicePhoneMobileIcon />}
|
||||||
title="Change Plan"
|
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="max-w-4xl mx-auto">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@ -331,7 +331,7 @@ export function SimReissueContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<DevicePhoneMobileIcon />}
|
icon={<DevicePhoneMobileIcon />}
|
||||||
title="Reissue SIM"
|
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="max-w-3xl mx-auto">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export function SimTopUpContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<DevicePhoneMobileIcon />}
|
icon={<DevicePhoneMobileIcon />}
|
||||||
title="Top Up Data"
|
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="max-w-3xl mx-auto">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { useSubscription } from "@/features/subscriptions/hooks";
|
import { useSubscription } from "@/features/subscriptions/hooks";
|
||||||
import { Formatting } from "@customer-portal/domain/toolkit";
|
import { Formatting } from "@customer-portal/domain/toolkit";
|
||||||
import { PageLayout } from "@/components/templates/PageLayout";
|
import { PageLayout } from "@/components/templates/PageLayout";
|
||||||
|
import { SectionCard } from "@/components/molecules/SectionCard";
|
||||||
import { StatusPill } from "@/components/atoms/status-pill";
|
import { StatusPill } from "@/components/atoms/status-pill";
|
||||||
import { SubscriptionDetailStatsSkeleton } from "@/components/atoms/loading-skeleton";
|
import { SubscriptionDetailStatsSkeleton } from "@/components/atoms/loading-skeleton";
|
||||||
import { formatIsoDate, cn } from "@/shared/utils";
|
import { formatIsoDate, cn } from "@/shared/utils";
|
||||||
@ -197,23 +198,24 @@ function SubscriptionDetailContent({
|
|||||||
{isSim && <SimTabNavigation subscriptionId={subscriptionId} activeTab={activeTab} />}
|
{isSim && <SimTabNavigation subscriptionId={subscriptionId} activeTab={activeTab} />}
|
||||||
{activeTab === "sim" && isSim && <SimManagementSection subscriptionId={subscriptionId} />}
|
{activeTab === "sim" && isSim && <SimManagementSection subscriptionId={subscriptionId} />}
|
||||||
{activeTab === "overview" && (
|
{activeTab === "overview" && (
|
||||||
<div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)] p-6">
|
<SectionCard
|
||||||
<div className="flex items-center justify-between">
|
icon={<CreditCardIcon className="h-5 w-5" />}
|
||||||
<div className="flex items-center gap-3">
|
title="Billing Information"
|
||||||
<CreditCardIcon className="h-5 w-5 text-muted-foreground" />
|
subtitle="Payment and invoices"
|
||||||
<h3 className="text-lg font-semibold text-foreground">Billing</h3>
|
tone="primary"
|
||||||
</div>
|
actions={
|
||||||
<Link
|
<Link
|
||||||
href="/account/billing/invoices"
|
href="/account/billing/invoices"
|
||||||
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
|
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
|
||||||
>
|
>
|
||||||
View all invoices →
|
View Invoices
|
||||||
</Link>
|
</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.
|
Invoices and payment history are available on the billing page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</SectionCard>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -231,11 +233,7 @@ export function SubscriptionDetailContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ServerIcon className="h-6 w-6" />}
|
icon={<ServerIcon className="h-6 w-6" />}
|
||||||
title="Subscription"
|
title="Subscription"
|
||||||
description="Loading subscription details..."
|
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
|
||||||
breadcrumbs={[
|
|
||||||
{ label: "Subscriptions", href: "/account/subscriptions" },
|
|
||||||
{ label: "Subscription" },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<SubscriptionDetailStatsSkeleton />
|
<SubscriptionDetailStatsSkeleton />
|
||||||
@ -255,10 +253,7 @@ export function SubscriptionDetailContainer() {
|
|||||||
icon={<ServerIcon className="h-6 w-6" />}
|
icon={<ServerIcon className="h-6 w-6" />}
|
||||||
title={subscription?.productName ?? "Subscription"}
|
title={subscription?.productName ?? "Subscription"}
|
||||||
actions={headerActions}
|
actions={headerActions}
|
||||||
breadcrumbs={[
|
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}
|
||||||
{ label: "Subscriptions", href: "/account/subscriptions" },
|
|
||||||
{ label: subscription?.productName ?? "Subscription" },
|
|
||||||
]}
|
|
||||||
error={getPageError(error)}
|
error={getPageError(error)}
|
||||||
>
|
>
|
||||||
{subscription ? (
|
{subscription ? (
|
||||||
|
|||||||
@ -146,11 +146,7 @@ export function SubscriptionsListContainer() {
|
|||||||
|
|
||||||
if (showLoading || error) {
|
if (showLoading || error) {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout icon={<Server />} title="Subscriptions">
|
||||||
icon={<Server />}
|
|
||||||
title="Subscriptions"
|
|
||||||
description="Manage your active subscriptions"
|
|
||||||
>
|
|
||||||
<AsyncBlock isLoading={false} error={error}>
|
<AsyncBlock isLoading={false} error={error}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<SubscriptionMetricsSkeleton />
|
<SubscriptionMetricsSkeleton />
|
||||||
@ -165,7 +161,6 @@ export function SubscriptionsListContainer() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<Server />}
|
icon={<Server />}
|
||||||
title="Subscriptions"
|
title="Subscriptions"
|
||||||
description="Manage your active subscriptions"
|
|
||||||
actions={
|
actions={
|
||||||
<Button as="a" href="/account/services" size="sm">
|
<Button as="a" href="/account/services" size="sm">
|
||||||
Browse Services
|
Browse Services
|
||||||
@ -175,7 +170,7 @@ export function SubscriptionsListContainer() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{stats && <SubscriptionMetrics stats={stats} />}
|
{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">
|
<div className="px-4 sm:px-5 py-3.5 border-b border-border/40">
|
||||||
<SearchFilterBar
|
<SearchFilterBar
|
||||||
searchValue={searchTerm}
|
searchValue={searchTerm}
|
||||||
|
|||||||
@ -242,8 +242,7 @@ export function NewSupportCaseView() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<TicketIconSolid />}
|
icon={<TicketIconSolid />}
|
||||||
title="Create Support Case"
|
title="Create Support Case"
|
||||||
description="Get help from our support team"
|
backLink={{ label: "Back to Cases", href: "/account/support" }}
|
||||||
breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Create Case" }]}
|
|
||||||
>
|
>
|
||||||
<AiChatSuggestion />
|
<AiChatSuggestion />
|
||||||
|
|
||||||
|
|||||||
@ -291,15 +291,10 @@ export function SupportCaseDetailView({ caseId }: SupportCaseDetailViewProps): R
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<TicketIconSolid />}
|
icon={<TicketIconSolid />}
|
||||||
title={state.supportCase ? `Case #${state.supportCase.caseNumber}` : "Loading..."}
|
title={state.supportCase ? `Case #${state.supportCase.caseNumber}` : "Loading..."}
|
||||||
description={state.supportCase?.subject}
|
|
||||||
loading={state.showLoading}
|
loading={state.showLoading}
|
||||||
error={state.pageError}
|
error={state.pageError}
|
||||||
onRetry={() => void state.refetch()}
|
onRetry={() => void state.refetch()}
|
||||||
breadcrumbs={[
|
backLink={{ label: "Back to Cases", href: SUPPORT_HREF }}
|
||||||
{ label: "Support", href: SUPPORT_HREF },
|
|
||||||
{ label: "Cases", href: SUPPORT_HREF },
|
|
||||||
{ label: state.supportCase ? `#${state.supportCase.caseNumber}` : "..." },
|
|
||||||
]}
|
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
@ -344,11 +339,7 @@ function CaseNotFoundView(): React.ReactElement {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<TicketIconSolid />}
|
icon={<TicketIconSolid />}
|
||||||
title="Case Not Found"
|
title="Case Not Found"
|
||||||
breadcrumbs={[
|
backLink={{ label: "Back to Cases", href: SUPPORT_HREF }}
|
||||||
{ label: "Support", href: SUPPORT_HREF },
|
|
||||||
{ label: "Cases", href: SUPPORT_HREF },
|
|
||||||
{ label: "Not Found" },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<AlertBanner variant="error" title="Case not found">
|
<AlertBanner variant="error" title="Case not found">
|
||||||
The support case you're looking for could not be found or you don't have
|
The support case you're looking for could not be found or you don't have
|
||||||
|
|||||||
@ -233,11 +233,9 @@ export function SupportCasesView() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ChatBubbleLeftRightIconSolid />}
|
icon={<ChatBubbleLeftRightIconSolid />}
|
||||||
title="Support Cases"
|
title="Support Cases"
|
||||||
description="Track and manage your support requests"
|
|
||||||
loading={showLoading}
|
loading={showLoading}
|
||||||
error={error}
|
error={error}
|
||||||
onRetry={() => void refetch()}
|
onRetry={() => void refetch()}
|
||||||
breadcrumbs={[{ label: "Support", href: "/account/support" }, { label: "Cases" }]}
|
|
||||||
actions={
|
actions={
|
||||||
<Button as="a" href="/account/support/new" leftIcon={<TicketIcon className="h-4 w-4" />}>
|
<Button as="a" href="/account/support/new" leftIcon={<TicketIcon className="h-4 w-4" />}>
|
||||||
New Case
|
New Case
|
||||||
|
|||||||
@ -109,7 +109,6 @@ export function SupportHomeView() {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
icon={<ChatBubbleLeftRightIconSolid />}
|
icon={<ChatBubbleLeftRightIconSolid />}
|
||||||
title="Support Center"
|
title="Support Center"
|
||||||
description="Get help with your account and services"
|
|
||||||
loading={showLoading}
|
loading={showLoading}
|
||||||
error={error}
|
error={error}
|
||||||
onRetry={() => void refetch()}
|
onRetry={() => void refetch()}
|
||||||
|
|||||||
@ -373,9 +373,8 @@ export function ResidenceCardVerificationSettingsView(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title="ID Verification"
|
title="ID Verification"
|
||||||
description="Upload your residence card for SIM activation"
|
|
||||||
icon={<ShieldCheckIcon className="h-6 w-6" />}
|
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">
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
<SubCard
|
<SubCard
|
||||||
|
|||||||
@ -172,6 +172,10 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility font-heading {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
}
|
||||||
|
|
||||||
@utility text-display-xl {
|
@utility text-display-xl {
|
||||||
font-size: var(--cp-text-display-xl);
|
font-size: var(--cp-text-display-xl);
|
||||||
letter-spacing: var(--cp-tracking-tight);
|
letter-spacing: var(--cp-tracking-tight);
|
||||||
|
|||||||
143
docs/plans/2026-03-05-portal-ui-cleanup-design.md
Normal file
143
docs/plans/2026-03-05-portal-ui-cleanup-design.md
Normal 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
|
||||||
774
docs/plans/2026-03-05-portal-ui-cleanup-plan.md
Normal file
774
docs/plans/2026-03-05-portal-ui-cleanup-plan.md
Normal 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
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user