Refactor UI components for improved styling and consistency

- Updated global styles to enhance color usage and contrast across the application.
- Refined input components for better accessibility and visual feedback.
- Enhanced layout components with consistent padding and margins for a cleaner look.
- Improved card and button styles to align with the new design tokens.
- Standardized text colors and hover effects for better user interaction.
This commit is contained in:
barsa 2025-12-16 16:08:17 +09:00
parent b5c917be33
commit 9d6c7dcde0
52 changed files with 1329 additions and 1540 deletions

View File

@ -4,37 +4,38 @@
@import "../styles/utilities.css"; @import "../styles/utilities.css";
@import "../styles/responsive.css"; @import "../styles/responsive.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
/* Core neutrals (light) */ /* Core neutrals (light) */
--background: oklch(0.98 0 0); /* Background and cards are both white for clean look */
--background: oklch(1 0 0); /* pure white for main content */
--foreground: oklch(0.16 0 0); --foreground: oklch(0.16 0 0);
--card: var(--background); --card: oklch(1 0 0); /* pure white for cards */
--card-foreground: var(--foreground); --card-foreground: var(--foreground);
--popover: var(--background); --card-muted: oklch(0.98 0.003 250); /* subtle gray for nested/muted cards */
--popover: oklch(1 0 0);
--popover-foreground: var(--foreground); --popover-foreground: var(--foreground);
/* Primary brand (azure) */ /* Primary brand - matches logo cyan blue */
--primary: oklch(0.62 0.17 255); --primary: oklch(0.68 0.14 220); /* bright cyan blue from logo */
--primary-foreground: oklch(0.99 0 0); --primary-foreground: oklch(0.99 0 0);
/* Interaction shades */ /* Interaction shades */
--primary-hover: oklch(0.58 0.17 255); --primary-hover: oklch(0.63 0.14 220);
--primary-active: oklch(0.54 0.17 255); --primary-active: oklch(0.58 0.14 220);
/* Subtle surfaces & text */ /* Subtle surfaces & text */
--secondary: oklch(0.93 0 0); --secondary: oklch(0.95 0.003 250); /* subtle cool gray */
--secondary-foreground: oklch(0.29 0 0); --secondary-foreground: oklch(0.29 0 0);
--muted: oklch(0.95 0 0); --muted: oklch(0.96 0.003 250); /* subtle cool gray for surfaces */
--muted-foreground: oklch(0.55 0 0); --muted-foreground: oklch(0.5 0 0);
/* Light accent (tinted, not a second brand) */ /* Light accent (tinted with brand) */
--accent: oklch(0.94 0.03 245); --accent: oklch(0.95 0.05 220); /* light cyan tint */
--accent-foreground: var(--foreground); --accent-foreground: var(--foreground);
/* Feedback (now with full semantic set) */ /* Feedback (now with full semantic set) */
@ -50,33 +51,33 @@
--warning-foreground: oklch(0.16 0 0); --warning-foreground: oklch(0.16 0 0);
--warning-soft: oklch(0.96 0.07 90); --warning-soft: oklch(0.96 0.07 90);
--info: oklch(0.64 0.16 255); --info: oklch(0.68 0.14 220); /* matches primary */
--info-foreground: oklch(0.99 0 0); --info-foreground: oklch(0.99 0 0);
--info-soft: oklch(0.95 0.05 255); --info-soft: oklch(0.95 0.06 220);
/* UI chrome */ /* UI chrome */
--border: oklch(0.90 0 0); --border: oklch(0.9 0 0);
--border-muted: oklch(0.88 0 0); --border-muted: oklch(0.92 0 0);
--input: var(--border); --input: oklch(0.88 0 0); /* slightly darker for inputs */
--ring: oklch(0.68 0.16 255); --ring: oklch(0.72 0.12 220);
--ring-subtle: color-mix(in oklch, var(--ring) 45%, transparent); --ring-subtle: color-mix(in oklch, var(--ring) 45%, transparent);
/* Charts */ /* Charts */
--chart-1: oklch(0.62 0.17 255); --chart-1: oklch(0.68 0.14 220); /* cyan from logo */
--chart-2: oklch(0.66 0.15 202); --chart-2: oklch(0.28 0.1 265); /* navy from logo */
--chart-3: oklch(0.64 0.19 147); --chart-3: oklch(0.64 0.19 147);
--chart-4: oklch(0.70 0.16 82); --chart-4: oklch(0.7 0.16 82);
--chart-5: oklch(0.68 0.16 28); --chart-5: oklch(0.68 0.16 28);
/* Sidebar */ /* Sidebar - Brand navy blue from logo */
--sidebar: var(--background); --sidebar: oklch(0.28 0.1 265); /* deep navy blue from logo */
--sidebar-foreground: var(--foreground); --sidebar-foreground: oklch(1 0 0); /* pure white text */
--sidebar-primary: var(--primary); --sidebar-primary: oklch(1 0 0); /* white for emphasis */
--sidebar-primary-foreground: var(--primary-foreground); --sidebar-primary-foreground: oklch(0.28 0.1 265);
--sidebar-accent: var(--secondary); --sidebar-accent: oklch(0.35 0.09 265); /* lighter navy for hover */
--sidebar-accent-foreground: var(--secondary-foreground); --sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: var(--border); --sidebar-border: oklch(0.35 0.08 265); /* lighter navy border */
--sidebar-ring: var(--ring); --sidebar-ring: oklch(0.72 0.12 220);
} }
.dark { .dark {
@ -86,28 +87,29 @@
--card: oklch(0.18 0 0); --card: oklch(0.18 0 0);
--card-foreground: var(--foreground); --card-foreground: var(--foreground);
--card-muted: oklch(0.22 0 0); /* subtle lighter gray for nested cards */
--popover: oklch(0.18 0 0); --popover: oklch(0.18 0 0);
--popover-foreground: var(--foreground); --popover-foreground: var(--foreground);
/* Primary brand (slightly lighter for dark) */ /* Primary brand - lighter cyan for dark mode */
--primary: oklch(0.74 0.16 255); --primary: oklch(0.75 0.12 220);
--primary-foreground: oklch(0.15 0 0); --primary-foreground: oklch(0.15 0 0);
--primary-hover: oklch(0.70 0.16 255); --primary-hover: oklch(0.7 0.12 220);
--primary-active: oklch(0.66 0.16 255); --primary-active: oklch(0.65 0.12 220);
/* Subtle surfaces & text */ /* Subtle surfaces & text */
--secondary: oklch(0.22 0 0); --secondary: oklch(0.22 0 0);
--secondary-foreground: oklch(0.90 0 0); --secondary-foreground: oklch(0.9 0 0);
--muted: oklch(0.25 0 0); --muted: oklch(0.25 0 0);
--muted-foreground: oklch(0.74 0 0); --muted-foreground: oklch(0.74 0 0);
/* Accent (tinted) */ /* Accent (tinted with brand) */
--accent: oklch(0.24 0.02 245); --accent: oklch(0.24 0.04 220);
--accent-foreground: oklch(0.92 0 0); --accent-foreground: oklch(0.92 0 0);
/* Feedback */ /* Feedback */
--destructive: oklch(0.70 0.21 27); --destructive: oklch(0.7 0.21 27);
--destructive-foreground: oklch(0.15 0 0); --destructive-foreground: oklch(0.15 0 0);
--destructive-soft: oklch(0.25 0.05 27); --destructive-soft: oklch(0.25 0.05 27);
@ -119,32 +121,32 @@
--warning-foreground: oklch(0.15 0 0); --warning-foreground: oklch(0.15 0 0);
--warning-soft: oklch(0.26 0.07 90); --warning-soft: oklch(0.26 0.07 90);
--info: oklch(0.78 0.15 255); --info: oklch(0.75 0.12 220);
--info-foreground: oklch(0.15 0 0); --info-foreground: oklch(0.15 0 0);
--info-soft: oklch(0.24 0.05 255); --info-soft: oklch(0.24 0.05 220);
/* UI chrome */ /* UI chrome */
--border: oklch(0.32 0 0); --border: oklch(0.32 0 0);
--border-muted: oklch(0.28 0 0); --border-muted: oklch(0.28 0 0);
--input: var(--border); --input: oklch(0.35 0 0); /* slightly lighter for inputs */
--ring: oklch(0.78 0.13 255); --ring: oklch(0.78 0.1 220);
--ring-subtle: color-mix(in oklch, var(--ring) 40%, transparent); --ring-subtle: color-mix(in oklch, var(--ring) 40%, transparent);
/* Charts */ /* Charts */
--chart-1: oklch(0.74 0.16 255); --chart-1: oklch(0.75 0.12 220); /* cyan from logo */
--chart-2: oklch(0.72 0.14 202); --chart-2: oklch(0.45 0.08 265); /* navy from logo */
--chart-3: oklch(0.70 0.18 147); --chart-3: oklch(0.7 0.18 147);
--chart-4: oklch(0.74 0.17 82); --chart-4: oklch(0.74 0.17 82);
--chart-5: oklch(0.72 0.17 28); --chart-5: oklch(0.72 0.17 28);
/* Sidebar */ /* Sidebar - Dark navy from logo */
--sidebar: var(--card); --sidebar: oklch(0.2 0.08 265); /* darker navy */
--sidebar-foreground: var(--foreground); --sidebar-foreground: oklch(1 0 0); /* pure white */
--sidebar-primary: var(--primary); --sidebar-primary: oklch(1 0 0);
--sidebar-primary-foreground: var(--primary-foreground); --sidebar-primary-foreground: oklch(0.2 0.08 265);
--sidebar-accent: var(--secondary); --sidebar-accent: oklch(0.28 0.07 265); /* lighter on hover */
--sidebar-accent-foreground: var(--secondary-foreground); --sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: var(--border); --sidebar-border: oklch(0.28 0.06 265);
--sidebar-ring: var(--ring); --sidebar-ring: var(--ring);
} }
@ -154,6 +156,7 @@
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-card-muted: var(--card-muted);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
@ -224,11 +227,15 @@
@theme inline { @theme inline {
--animate-aurora: aurora 60s linear infinite; --animate-aurora: aurora 60s linear infinite;
@keyframes aurora { @keyframes aurora {
from { from {
backgroundPosition: 50% 50%, 50% 50%; backgroundposition:
50% 50%,
50% 50%;
} }
to { to {
backgroundPosition: 350% 50%, 350% 50%; backgroundposition:
350% 50%,
350% 50%;
} }
} }
} }
@ -240,4 +247,4 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@ -14,9 +14,14 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex h-11 w-full rounded-lg border border-border bg-card text-foreground px-4 py-2.5 text-sm shadow-sm ring-offset-background transition-all duration-200",
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
"placeholder:text-muted-foreground",
"hover:border-muted-foreground/50",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-0 focus-visible:border-primary",
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-border",
isInvalid && isInvalid &&
"border-destructive focus-visible:ring-destructive focus-visible:ring-offset-2", "border-destructive hover:border-destructive focus-visible:ring-destructive/40 focus-visible:border-destructive",
className className
)} )}
aria-invalid={isInvalid || undefined} aria-invalid={isInvalid || undefined}

View File

@ -56,7 +56,7 @@ const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
> >
<span>{label}</span> <span>{label}</span>
{required ? ( {required ? (
<span aria-hidden="true" className="ml-1 text-red-600"> <span aria-hidden="true" className="ml-1 text-destructive">
* *
</span> </span>
) : null} ) : null}

View File

@ -28,50 +28,46 @@ export function SearchFilterBar({
children, children,
}: SearchFilterBarProps) { }: SearchFilterBarProps) {
return ( return (
<div className="bg-card text-card-foreground border border-border shadow-[var(--cp-shadow-1)] rounded-lg mb-6"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="p-6 border-b border-border"> {/* Search */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="relative flex-1 max-w-sm">
{/* Search */} <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<div className="relative max-w-xs"> <MagnifyingGlassIcon className="h-5 w-5 text-muted-foreground" />
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-muted-foreground/70" />
</div>
<input
type="text"
className="block w-full pl-10 pr-3 py-2 border border-input rounded-md leading-5 bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
placeholder={searchPlaceholder}
value={searchValue}
onChange={e => onSearchChange(e.target.value)}
/>
</div>
{/* Filters and Actions */}
<div className="flex items-center gap-4">
{/* Status Filter */}
{filterOptions.length > 0 && onFilterChange && filterValue !== undefined && (
<div className="relative">
<select
value={filterValue}
onChange={e => onFilterChange(e.target.value)}
className="block w-40 pl-3 pr-8 py-2 text-base border border-input focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring sm:text-sm rounded-md appearance-none bg-background text-foreground transition-colors"
aria-label={filterLabel}
>
{filterOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<FunnelIcon className="h-4 w-4 text-muted-foreground/70" />
</div>
</div>
)}
{/* Additional children (custom actions) */}
{children}
</div>
</div> </div>
<input
type="text"
className="block w-full pl-10 pr-3 py-2.5 border border-border rounded-lg leading-5 bg-card text-foreground shadow-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors"
placeholder={searchPlaceholder}
value={searchValue}
onChange={e => onSearchChange(e.target.value)}
/>
</div>
{/* Filters and Actions */}
<div className="flex items-center gap-3">
{/* Status Filter */}
{filterOptions.length > 0 && onFilterChange && filterValue !== undefined && (
<div className="relative">
<select
value={filterValue}
onChange={e => onFilterChange(e.target.value)}
className="block w-40 pl-3 pr-8 py-2.5 text-sm border border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary rounded-lg appearance-none bg-card text-foreground shadow-sm cursor-pointer transition-colors"
aria-label={filterLabel}
>
{filterOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center pr-2.5 pointer-events-none">
<FunnelIcon className="h-4 w-4 text-muted-foreground" />
</div>
</div>
)}
{/* Additional children (custom actions) */}
{children}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import { forwardRef, type ReactNode } from "react"; import { forwardRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface SubCardProps { export interface SubCardProps {
title?: string; title?: string;
@ -10,6 +11,8 @@ export interface SubCardProps {
className?: string; className?: string;
headerClassName?: string; headerClassName?: string;
bodyClassName?: string; bodyClassName?: string;
/** Enable hover effects for interactive cards */
interactive?: boolean;
} }
export const SubCard = forwardRef<HTMLDivElement, SubCardProps>( export const SubCard = forwardRef<HTMLDivElement, SubCardProps>(
@ -24,20 +27,24 @@ export const SubCard = forwardRef<HTMLDivElement, SubCardProps>(
className = "", className = "",
headerClassName = "", headerClassName = "",
bodyClassName = "", bodyClassName = "",
interactive = false,
}, },
ref ref
) => ( ) => (
<div <div
ref={ref} ref={ref}
className={`border-[var(--cp-card-border)] bg-card text-card-foreground shadow-[var(--cp-card-shadow)] rounded-[var(--cp-card-radius)] p-[var(--cp-card-padding)] transition-shadow duration-[var(--cp-duration-normal)] hover:shadow-[var(--cp-card-shadow-lg)] ${className}`} className={cn(
"border border-border bg-card text-card-foreground shadow-sm rounded-2xl p-5 sm:p-6",
interactive &&
"transition-all duration-200 hover:shadow-md hover:border-border/80 cursor-pointer",
className
)}
> >
{header ? ( {header ? (
<div className={`${headerClassName || "mb-[var(--cp-space-lg)]"}`}>{header}</div> <div className={`${headerClassName || "mb-5"}`}>{header}</div>
) : title ? ( ) : title ? (
<div <div className={`flex items-center justify-between mb-5 ${headerClassName}`}>
className={`flex items-center justify-between mb-[var(--cp-space-lg)] ${headerClassName}`} <div className="flex items-center gap-3">
>
<div className="flex items-center gap-[var(--cp-space-sm)]">
{icon && <div className="text-primary">{icon}</div>} {icon && <div className="text-primary">{icon}</div>}
<h3 className="text-lg font-semibold text-foreground">{title}</h3> <h3 className="text-lg font-semibold text-foreground">{title}</h3>
</div> </div>
@ -45,11 +52,7 @@ export const SubCard = forwardRef<HTMLDivElement, SubCardProps>(
</div> </div>
) : null} ) : null}
<div className={bodyClassName}>{children}</div> <div className={bodyClassName}>{children}</div>
{footer ? ( {footer ? <div className="mt-5 pt-5 border-t border-border/60">{footer}</div> : null}
<div className="mt-[var(--cp-space-lg)] pt-[var(--cp-space-lg)] border-t border-border">
{footer}
</div>
) : null}
</div> </div>
) )
); );

View File

@ -180,7 +180,7 @@ export function AppShell({ children }: AppShellProps) {
</div> </div>
{/* Main content */} {/* Main content */}
<div className="flex flex-col w-0 flex-1 overflow-hidden"> <div className="flex flex-col w-0 flex-1 overflow-hidden bg-background">
{/* Header */} {/* Header */}
<Header <Header
onMenuClick={() => setSidebarOpen(true)} onMenuClick={() => setSidebarOpen(true)}

View File

@ -17,12 +17,18 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
"Account" "Account"
: user?.email?.split("@")[0] || "Account"; : user?.email?.split("@")[0] || "Account";
// Get initials for avatar
const initials =
profileReady && user?.firstName && user?.lastName
? `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
: displayName.slice(0, 2).toUpperCase();
return ( return (
<div className="bg-[var(--cp-header-bg)] border-b border-[var(--cp-header-border)] backdrop-blur-sm"> <div className="bg-[var(--cp-header-bg)] border-b border-[var(--cp-header-border)]/50 backdrop-blur-xl">
<div className="flex items-center h-16 gap-3 px-4 sm:px-6"> <div className="flex items-center h-16 gap-3 px-4 sm:px-6">
<button <button
type="button" type="button"
className="md:hidden p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors duration-[var(--cp-duration-normal)] focus:outline-none focus:ring-2 focus:ring-primary/20" className="md:hidden p-2 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
onClick={onMenuClick} onClick={onMenuClick}
aria-label="Open navigation" aria-label="Open navigation"
> >
@ -31,12 +37,12 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
<div className="flex-1" /> <div className="flex-1" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<Link <Link
href="/support" href="/support"
prefetch prefetch
aria-label="Help" aria-label="Help"
className="hidden sm:inline-flex p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" className="hidden sm:inline-flex p-2.5 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-all duration-200"
title="Support Center" title="Support Center"
> >
<QuestionMarkCircleIcon className="h-5 w-5" /> <QuestionMarkCircleIcon className="h-5 w-5" />
@ -45,9 +51,12 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
<Link <Link
href="/account" href="/account"
prefetch prefetch
className="hidden sm:inline-flex items-center px-2.5 py-1.5 text-sm font-medium text-foreground/80 hover:text-foreground hover:bg-muted rounded-lg transition-colors duration-[var(--cp-duration-normal)]" className="group flex items-center gap-2.5 px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-xl transition-all duration-200"
> >
{displayName} <div className="h-7 w-7 rounded-lg bg-gradient-to-br from-primary to-primary-hover flex items-center justify-center text-xs font-bold text-primary-foreground shadow-sm">
{initials}
</div>
<span className="hidden sm:inline">{displayName}</span>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -23,16 +23,14 @@ export const Sidebar = memo(function Sidebar({
}: SidebarProps) { }: SidebarProps) {
return ( return (
<div className="flex flex-col h-0 flex-1 bg-[var(--cp-sidebar-bg)]"> <div className="flex flex-col h-0 flex-1 bg-[var(--cp-sidebar-bg)]">
<div className="flex items-center flex-shrink-0 h-16 px-6 border-b border-[var(--cp-sidebar-border)]"> <div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-[var(--cp-sidebar-border)]">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="p-2 bg-card rounded-xl border border-[var(--cp-sidebar-border)] shadow-[var(--cp-shadow-1)]"> <div className="h-10 w-10 bg-white rounded-xl shadow-lg shadow-black/10 flex items-center justify-center">
<Logo size={20} /> <Logo size={26} />
</div> </div>
<div> <div>
<span className="text-base font-bold text-[var(--cp-sidebar-text)]"> <span className="text-base font-bold text-white">Assist Solutions</span>
Assist Solutions <p className="text-xs text-white/70 font-medium">Customer Portal</p>
</span>
<p className="text-xs text-[var(--cp-sidebar-text)]/60">Customer Portal</p>
</div> </div>
</div> </div>
</div> </div>
@ -89,32 +87,30 @@ const NavigationItem = memo(function NavigationItem({
<button <button
onClick={() => toggleExpanded(item.name)} onClick={() => toggleExpanded(item.name)}
aria-expanded={isExpanded} aria-expanded={isExpanded}
className={`group w-full flex items-center px-3 py-2.5 text-left text-sm font-medium rounded-lg transition-all duration-200 relative ${ className={`group w-full flex items-center px-3 py-2.5 text-left text-sm font-semibold rounded-lg transition-all duration-200 relative ${
isActive isActive
? "text-[var(--cp-sidebar-active-text)] bg-[var(--cp-sidebar-active-bg)]" ? "text-white bg-white/20 shadow-sm"
: "text-[var(--cp-sidebar-text)] hover:text-[var(--cp-sidebar-text-hover)] hover:bg-[var(--cp-sidebar-hover-bg)]" : "text-white/90 hover:text-white hover:bg-white/10"
} focus:outline-none focus:ring-2 focus:ring-primary/20`} } focus:outline-none focus:ring-2 focus:ring-white/30`}
> >
{isActive && ( {isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r-full" /> <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white rounded-r-full" />
)} )}
<div <div
className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${ className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${
isActive isActive ? "bg-white/20 text-white" : "text-white/80 group-hover:text-white"
? "bg-primary/10 text-primary"
: "text-[var(--cp-sidebar-text)]/70 group-hover:text-[var(--cp-sidebar-text-hover)] group-hover:bg-[var(--cp-sidebar-hover-bg)]"
}`} }`}
> >
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
</div> </div>
<span className="flex-1 font-medium">{item.name}</span> <span className="flex-1">{item.name}</span>
<svg <svg
className={`h-4 w-4 transition-transform duration-200 ease-out ${ className={`h-4 w-4 transition-transform duration-200 ease-out ${
isExpanded ? "rotate-90" : "" isExpanded ? "rotate-90" : ""
} ${isActive ? "text-primary" : "text-[var(--cp-sidebar-text)]/50"}`} } ${isActive ? "text-white" : "text-white/60 group-hover:text-white/80"}`}
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
> >
@ -131,7 +127,7 @@ const NavigationItem = memo(function NavigationItem({
isExpanded ? "max-h-96 opacity-100" : "max-h-0 opacity-0" isExpanded ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
}`} }`}
> >
<div className="mt-1 ml-6 space-y-0.5 border-l border-[var(--cp-sidebar-border)] pl-4"> <div className="mt-1 ml-6 space-y-0.5 border-l border-white/30 pl-4">
{item.children?.map((child: NavigationChild) => { {item.children?.map((child: NavigationChild) => {
const isChildActive = pathname === (child.href || "").split(/[?#]/)[0]; const isChildActive = pathname === (child.href || "").split(/[?#]/)[0];
return ( return (
@ -146,21 +142,19 @@ const NavigationItem = memo(function NavigationItem({
}} }}
className={`group flex items-center px-3 py-2 text-sm rounded-md transition-all duration-200 relative ${ className={`group flex items-center px-3 py-2 text-sm rounded-md transition-all duration-200 relative ${
isChildActive isChildActive
? "text-[var(--cp-sidebar-active-text)] bg-[var(--cp-sidebar-active-bg)] font-medium" ? "text-white bg-white/20 font-semibold"
: "text-[var(--cp-sidebar-text)]/80 hover:text-[var(--cp-sidebar-text-hover)] hover:bg-[var(--cp-sidebar-hover-bg)]" : "text-white/80 hover:text-white hover:bg-white/10 font-medium"
}`} }`}
title={child.tooltip || child.name} title={child.tooltip || child.name}
aria-current={isChildActive ? "page" : undefined} aria-current={isChildActive ? "page" : undefined}
> >
{isChildActive && ( {isChildActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-primary rounded-full" /> <div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-white rounded-full" />
)} )}
<div <div
className={`w-1.5 h-1.5 rounded-full mr-3 transition-colors duration-200 ${ className={`w-1.5 h-1.5 rounded-full mr-3 transition-colors duration-200 ${
isChildActive isChildActive ? "bg-white" : "bg-white/40 group-hover:bg-white/70"
? "bg-primary"
: "bg-[var(--cp-sidebar-text)]/30 group-hover:bg-[var(--cp-sidebar-text)]/50"
}`} }`}
/> />
@ -178,9 +172,9 @@ const NavigationItem = memo(function NavigationItem({
return ( return (
<button <button
onClick={handleLogout} onClick={handleLogout}
className="group w-full flex items-center px-3 py-2.5 text-sm font-medium text-destructive hover:bg-destructive-soft rounded-lg transition-colors duration-[var(--cp-duration-normal)] focus:outline-none focus:ring-2 focus:ring-destructive/20" className="group w-full flex items-center px-3 py-2.5 text-sm font-semibold text-red-300 hover:text-red-100 hover:bg-red-500/25 rounded-lg transition-colors duration-[var(--cp-duration-normal)] focus:outline-none focus:ring-2 focus:ring-red-400/30"
> >
<div className="p-1.5 rounded-md mr-3 text-destructive group-hover:bg-destructive-soft transition-colors duration-[var(--cp-duration-normal)]"> <div className="p-1.5 rounded-md mr-3 text-red-300 group-hover:text-red-100 transition-colors duration-[var(--cp-duration-normal)]">
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
</div> </div>
<span>{item.name}</span> <span>{item.name}</span>
@ -197,22 +191,20 @@ const NavigationItem = memo(function NavigationItem({
void router.prefetch(item.href); void router.prefetch(item.href);
} }
}} }}
className={`group w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 relative ${ className={`group w-full flex items-center px-3 py-2.5 text-sm font-semibold rounded-lg transition-all duration-200 relative ${
isActive isActive
? "text-[var(--cp-sidebar-active-text)] bg-[var(--cp-sidebar-active-bg)]" ? "text-white bg-white/20 shadow-sm"
: "text-[var(--cp-sidebar-text)] hover:text-[var(--cp-sidebar-text-hover)] hover:bg-[var(--cp-sidebar-hover-bg)]" : "text-white/90 hover:text-white hover:bg-white/10"
} focus:outline-none focus:ring-2 focus:ring-primary/20`} } focus:outline-none focus:ring-2 focus:ring-white/30`}
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
> >
{isActive && ( {isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r-full" /> <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white rounded-r-full" />
)} )}
<div <div
className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${ className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${
isActive isActive ? "bg-white/20 text-white" : "text-white/80 group-hover:text-white"
? "bg-primary/10 text-primary"
: "text-[var(--cp-sidebar-text)]/70 group-hover:text-[var(--cp-sidebar-text-hover)] group-hover:bg-[var(--cp-sidebar-hover-bg)]"
}`} }`}
> >
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />

View File

@ -22,36 +22,55 @@ export function AuthLayout({
backLabel = "Back to Home", backLabel = "Back to Home",
}: AuthLayoutProps) { }: AuthLayoutProps) {
return ( return (
<div className="w-full flex flex-col items-center py-[var(--cp-space-3xl)]"> <div className="w-full flex flex-col items-center">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{showBackButton && ( {showBackButton && (
<div className="mb-6"> <div className="mb-6">
<Link <Link
href={backHref} href={backHref}
className="inline-flex items-center text-sm font-medium text-muted-foreground hover:text-foreground transition-colors" className="inline-flex items-center text-sm font-medium text-muted-foreground hover:text-foreground transition-colors group"
> >
<ArrowLeftIcon className="h-4 w-4 mr-2" /> <ArrowLeftIcon className="h-4 w-4 mr-2 transition-transform group-hover:-translate-x-0.5" />
{backLabel} {backLabel}
</Link> </Link>
</div> </div>
)} )}
<div className="text-center"> <div className="text-center">
<div className="flex justify-center mb-8"> <div className="flex justify-center mb-6">
<Logo size={72} className="text-primary" /> <div className="relative">
<div className="absolute inset-0 bg-[#28A6E0]/15 blur-2xl rounded-full scale-[2]" />
<div className="relative h-[4.5rem] w-[4.5rem] rounded-2xl bg-white border border-border/50 flex items-center justify-center shadow-xl shadow-[#28A6E0]/15">
<Logo size={44} />
</div>
</div>
</div> </div>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground mb-3"> <h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground mb-2">
{title} {title}
</h1> </h1>
{subtitle && ( {subtitle && (
<p className="text-base text-muted-foreground leading-relaxed">{subtitle}</p> <p className="text-sm sm:text-base text-muted-foreground leading-relaxed max-w-sm mx-auto">
{subtitle}
</p>
)} )}
</div> </div>
</div> </div>
<div className="mt-8 w-full max-w-md"> <div className="mt-8 w-full max-w-md">
<div className="bg-card text-card-foreground py-10 px-6 rounded-3xl border border-border shadow-[var(--cp-card-shadow-lg)] sm:px-12"> <div className="relative">
{children} {/* Subtle gradient glow behind card */}
<div className="absolute -inset-1 bg-gradient-to-r from-primary/10 via-transparent to-primary/10 rounded-[1.75rem] blur-xl opacity-50" />
<div className="relative bg-card text-card-foreground py-10 px-6 rounded-2xl border border-border/60 shadow-xl shadow-black/5 sm:px-10 backdrop-blur-sm">
{children}
</div>
</div>
{/* Trust indicator */}
<div className="mt-6 text-center">
<p className="text-xs text-muted-foreground/60">
Secure login protected by SSL encryption
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -63,7 +63,7 @@ export function PageLayout({
)} )}
{/* Header */} {/* Header */}
<div className="mb-[var(--cp-space-2xl)] sm:mb-[var(--cp-space-3xl)]"> <div className="mb-[var(--cp-space-xl)] sm:mb-[var(--cp-space-2xl)] pb-[var(--cp-space-xl)] sm:pb-[var(--cp-space-2xl)] border-b border-border">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-[var(--cp-space-lg)] sm:gap-[var(--cp-space-xl)]"> <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-[var(--cp-space-lg)] sm:gap-[var(--cp-space-xl)]">
<div className="flex items-start min-w-0 flex-1"> <div className="flex items-start min-w-0 flex-1">
{icon && ( {icon && (

View File

@ -9,14 +9,20 @@ export interface PublicShellProps {
export function PublicShell({ children }: PublicShellProps) { export function PublicShell({ children }: PublicShellProps) {
return ( return (
<div className="min-h-screen flex flex-col bg-background text-foreground"> <div className="min-h-screen flex flex-col bg-background text-foreground">
<header className="sticky top-0 z-40 border-b border-border bg-background/80 backdrop-blur-sm"> {/* Subtle background pattern */}
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
</div>
<header className="sticky top-0 z-40 border-b border-border/50 bg-background/80 backdrop-blur-xl">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-3 flex items-center justify-between gap-4"> <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-3 flex items-center justify-between gap-4">
<Link href="/" className="inline-flex items-center gap-3 min-w-0"> <Link href="/" className="inline-flex items-center gap-3 min-w-0 group">
<span className="inline-flex items-center justify-center h-9 w-9 rounded-xl border border-border bg-card shadow-[var(--cp-shadow-1)]"> <span className="inline-flex items-center justify-center h-11 w-11 rounded-xl bg-white border border-border/60 shadow-lg shadow-[#28A6E0]/10 transition-transform group-hover:scale-105">
<Logo size={20} /> <Logo size={28} />
</span> </span>
<span className="min-w-0"> <span className="min-w-0">
<span className="block text-sm font-semibold leading-tight truncate"> <span className="block text-base font-bold leading-tight truncate text-foreground">
Assist Solutions Assist Solutions
</span> </span>
<span className="block text-xs text-muted-foreground leading-tight truncate"> <span className="block text-xs text-muted-foreground leading-tight truncate">
@ -28,13 +34,13 @@ export function PublicShell({ children }: PublicShellProps) {
<nav className="flex items-center gap-2"> <nav className="flex items-center gap-2">
<Link <Link
href="/support" href="/support"
className="hidden sm:inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" className="hidden sm:inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
> >
Support Support
</Link> </Link>
<Link <Link
href="/auth/login" href="/auth/login"
className="inline-flex items-center rounded-lg px-3 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover transition-colors" className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover shadow-sm shadow-primary/20 transition-all hover:shadow-md hover:shadow-primary/30"
> >
Sign in Sign in
</Link> </Link>
@ -43,25 +49,34 @@ export function PublicShell({ children }: PublicShellProps) {
</header> </header>
<main className="flex-1"> <main className="flex-1">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-[var(--cp-space-3xl)]"> <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-12 sm:py-16">
{children} {children}
</div> </div>
</main> </main>
<footer className="border-t border-border bg-background"> <footer className="border-t border-border/50 bg-muted/30">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-8"> <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] py-8">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Assist Solutions © {new Date().getFullYear()} Assist Solutions. All rights reserved.
</div> </div>
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-6 text-sm">
<Link href="/support" className="text-muted-foreground hover:text-foreground"> <Link
href="/support"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Support Support
</Link> </Link>
<Link href="#" className="text-muted-foreground hover:text-foreground"> <Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Privacy Privacy
</Link> </Link>
<Link href="#" className="text-muted-foreground hover:text-foreground"> <Link
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Terms Terms
</Link> </Link>
</div> </div>

View File

@ -33,16 +33,18 @@ export function AddressCard({
return ( return (
<SubCard> <SubCard>
<div className="pb-5 border-b border-gray-200"> <div className="pb-5 border-b border-border/60">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<MapPinIcon className="h-6 w-6 text-blue-600" /> <div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center">
<h2 className="text-xl font-semibold text-gray-900">Address Information</h2> <MapPinIcon className="h-5 w-5 text-primary" />
</div>
<h2 className="text-lg font-semibold text-foreground">Address Information</h2>
</div> </div>
{!isEditing && ( {!isEditing && (
<button <button
onClick={onEdit} onClick={onEdit}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-border text-sm font-medium rounded-lg text-foreground bg-background hover:bg-muted transition-colors"
> >
<PencilIcon className="h-4 w-4 mr-2" /> <PencilIcon className="h-4 w-4 mr-2" />
Edit Edit
@ -55,12 +57,12 @@ export function AddressCard({
{isEditing ? ( {isEditing ? (
<div className="space-y-6"> <div className="space-y-6">
<AddressForm initialAddress={address} onChange={addr => onAddressChange(addr, true)} /> <AddressForm initialAddress={address} onChange={addr => onAddressChange(addr, true)} />
{error && <div className="text-sm text-red-600">{error}</div>} {error && <div className="text-sm text-destructive">{error}</div>}
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-gray-200"> <div className="flex items-center justify-end space-x-3 pt-4 border-t border-border/60">
<button <button
onClick={onCancel} onClick={onCancel}
disabled={isSaving} disabled={isSaving}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-border text-sm font-medium rounded-lg text-foreground bg-background hover:bg-muted disabled:opacity-50 transition-colors"
> >
<XMarkIcon className="h-4 w-4 mr-2" /> <XMarkIcon className="h-4 w-4 mr-2" />
Cancel Cancel
@ -68,11 +70,11 @@ export function AddressCard({
<button <button
onClick={onSave} onClick={onSave}
disabled={isSaving} disabled={isSaving}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 transition-colors" className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-primary-foreground bg-primary hover:bg-primary-hover disabled:opacity-50 transition-colors shadow-sm"
> >
{isSaving ? ( {isSaving ? (
<> <>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground mr-2"></div>
Saving... Saving...
</> </>
) : ( ) : (
@ -85,15 +87,15 @@ export function AddressCard({
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-1 text-gray-900"> <div className="space-y-1.5 text-foreground">
{address.address1 && <p className="font-semibold text-base">{address.address1}</p>} {address.address1 && <p className="font-semibold text-base">{address.address1}</p>}
{address.address2 && <p className="text-gray-700">{address.address2}</p>} {address.address2 && <p className="text-muted-foreground">{address.address2}</p>}
{(address.city || address.state || address.postcode) && ( {(address.city || address.state || address.postcode) && (
<p className="text-gray-700"> <p className="text-muted-foreground">
{[address.city, address.state, address.postcode].filter(Boolean).join(", ")} {[address.city, address.state, address.postcode].filter(Boolean).join(", ")}
</p> </p>
)} )}
{countryLabel && <p className="text-gray-600 font-medium">{countryLabel}</p>} {countryLabel && <p className="text-muted-foreground font-medium">{countryLabel}</p>}
</div> </div>
)} )}
</div> </div>

View File

@ -125,7 +125,7 @@ export default function ProfileContainer() {
))} ))}
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Skeleton className="h-4 w-28 mb-3" /> <Skeleton className="h-4 w-28 mb-3" />
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-5 w-48" /> <Skeleton className="h-5 w-48" />
<Skeleton className="h-5 w-24" /> <Skeleton className="h-5 w-24" />
@ -152,7 +152,7 @@ export default function ProfileContainer() {
</div> </div>
</div> </div>
<div className="p-6"> <div className="p-6">
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<div className="space-y-2"> <div className="space-y-2">
<Skeleton className="h-4 w-60" /> <Skeleton className="h-4 w-60" />
<Skeleton className="h-4 w-48" /> <Skeleton className="h-4 w-48" />
@ -211,7 +211,7 @@ export default function ProfileContainer() {
<label className="block text-sm font-medium text-muted-foreground mb-2"> <label className="block text-sm font-medium text-muted-foreground mb-2">
First Name First Name
</label> </label>
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<p className="text-base text-foreground font-medium"> <p className="text-base text-foreground font-medium">
{user?.firstname || ( {user?.firstname || (
<span className="text-muted-foreground italic">Not provided</span> <span className="text-muted-foreground italic">Not provided</span>
@ -226,7 +226,7 @@ export default function ProfileContainer() {
<label className="block text-sm font-medium text-muted-foreground mb-2"> <label className="block text-sm font-medium text-muted-foreground mb-2">
Last Name Last Name
</label> </label>
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<p className="text-base text-foreground font-medium"> <p className="text-base text-foreground font-medium">
{user?.lastname || ( {user?.lastname || (
<span className="text-muted-foreground italic">Not provided</span> <span className="text-muted-foreground italic">Not provided</span>
@ -249,7 +249,7 @@ export default function ProfileContainer() {
className="block w-full px-4 py-2.5 border border-input rounded-lg bg-background text-foreground shadow-[var(--cp-shadow-1)] focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors" className="block w-full px-4 py-2.5 border border-input rounded-lg bg-background text-foreground shadow-[var(--cp-shadow-1)] focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
/> />
) : ( ) : (
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-base text-foreground font-medium">{user?.email}</p> <p className="text-base text-foreground font-medium">{user?.email}</p>
</div> </div>
@ -264,7 +264,7 @@ export default function ProfileContainer() {
<label className="block text-sm font-medium text-muted-foreground mb-2"> <label className="block text-sm font-medium text-muted-foreground mb-2">
Customer Number Customer Number
</label> </label>
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<p className="text-base text-foreground font-medium"> <p className="text-base text-foreground font-medium">
{user?.sfNumber || ( {user?.sfNumber || (
<span className="text-muted-foreground italic">Not available</span> <span className="text-muted-foreground italic">Not available</span>
@ -278,7 +278,7 @@ export default function ProfileContainer() {
<label className="block text-sm font-medium text-muted-foreground mb-2"> <label className="block text-sm font-medium text-muted-foreground mb-2">
Date of Birth Date of Birth
</label> </label>
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<p className="text-base text-foreground font-medium"> <p className="text-base text-foreground font-medium">
{user?.dateOfBirth || ( {user?.dateOfBirth || (
<span className="text-muted-foreground italic">Not provided</span> <span className="text-muted-foreground italic">Not provided</span>
@ -312,7 +312,7 @@ export default function ProfileContainer() {
<div> <div>
<label className="block text-sm font-medium text-muted-foreground mb-2">Gender</label> <label className="block text-sm font-medium text-muted-foreground mb-2">Gender</label>
<div className="bg-muted rounded-lg p-4 border border-border"> <div className="bg-card rounded-lg p-4 border border-border shadow-sm">
<p className="text-base text-foreground font-medium"> <p className="text-base text-foreground font-medium">
{user?.gender || ( {user?.gender || (
<span className="text-muted-foreground italic">Not provided</span> <span className="text-muted-foreground italic">Not provided</span>
@ -443,7 +443,7 @@ export default function ProfileContainer() {
) : ( ) : (
<div> <div>
{address.values.address1 || address.values.city ? ( {address.values.address1 || address.values.city ? (
<div className="bg-muted rounded-lg p-5 border border-border"> <div className="bg-card rounded-lg p-5 border border-border shadow-sm">
<div className="text-foreground space-y-1.5"> <div className="text-foreground space-y-1.5">
{address.values.address1 && ( {address.values.address1 && (
<p className="font-medium text-base">{address.values.address1}</p> <p className="font-medium text-base">{address.values.address1}</p>

View File

@ -107,9 +107,9 @@ export function LoginForm({
checked={values.rememberMe} checked={values.rememberMe}
onChange={e => setValue("rememberMe", e.target.checked)} onChange={e => setValue("rememberMe", e.target.checked)}
disabled={isSubmitting || loading} disabled={isSubmitting || loading}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded transition-colors" className="h-4 w-4 text-primary focus:ring-primary border-border rounded transition-colors accent-primary"
/> />
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700"> <label htmlFor="remember-me" className="ml-2 block text-sm text-muted-foreground">
Remember me Remember me
</label> </label>
</div> </div>
@ -118,7 +118,7 @@ export function LoginForm({
<div className="text-sm"> <div className="text-sm">
<Link <Link
href="/auth/forgot-password" href="/auth/forgot-password"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors duration-200" className="font-medium text-primary hover:text-primary-hover transition-colors duration-200"
> >
Forgot your password? Forgot your password?
</Link> </Link>
@ -140,11 +140,11 @@ export function LoginForm({
{showSignupLink && ( {showSignupLink && (
<div className="text-center"> <div className="text-center">
<p className="text-sm text-gray-600"> <p className="text-sm text-muted-foreground">
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
<Link <Link
href="/auth/signup" href="/auth/signup"
className="font-medium text-blue-600 hover:text-blue-500 transition-colors duration-200" className="font-medium text-primary hover:text-primary-hover transition-colors duration-200"
> >
Sign up Sign up
</Link> </Link>

View File

@ -21,17 +21,15 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
const hasLinkedItems = items.some(item => hasServiceConnection(item)); const hasLinkedItems = items.some(item => hasServiceConnection(item));
const hasOneTimeItems = items.some(item => !hasServiceConnection(item)); const hasOneTimeItems = items.some(item => !hasServiceConnection(item));
const renderItemContent = (item: InvoiceItem, index: number) => { const renderItemContent = (item: InvoiceItem) => {
const isLinked = hasServiceConnection(item); const isLinked = hasServiceConnection(item);
const itemContent = ( const itemContent = (
<div <div
className={`flex justify-between items-start py-4 rounded-lg transition-all duration-200 ${ className={`flex justify-between items-start py-4 px-4 rounded-xl transition-all duration-200 ${
index !== items.length - 1 ? "border-b border-slate-100" : ""
} ${
isLinked isLinked
? "hover:bg-blue-50 hover:border-blue-200 cursor-pointer group" ? "hover:bg-primary/5 cursor-pointer group border border-transparent hover:border-primary/20"
: "bg-slate-50/50" : "bg-muted/30"
}`} }`}
> >
<div className="flex-1 pr-4"> <div className="flex-1 pr-4">
@ -39,13 +37,13 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
<div className="flex-1"> <div className="flex-1">
<div <div
className={`font-semibold mb-1 ${ className={`font-semibold mb-1 ${
isLinked ? "text-blue-900 group-hover:text-blue-700" : "text-slate-900" isLinked ? "text-primary group-hover:text-primary/80" : "text-foreground"
}`} }`}
> >
{item.description} {item.description}
{isLinked && ( {isLinked && (
<svg <svg
className="inline-block w-4 h-4 ml-1 text-blue-500 group-hover:text-blue-600" className="inline-block w-4 h-4 ml-1 text-primary/70 group-hover:text-primary"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -61,12 +59,12 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
</div> </div>
<div className="flex flex-wrap gap-3 text-sm"> <div className="flex flex-wrap gap-3 text-sm">
{item.quantity && item.quantity > 1 && ( {item.quantity && item.quantity > 1 && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary">
Qty: {item.quantity} Qty: {item.quantity}
</span> </span>
)} )}
{isLinked ? ( {isLinked ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -77,7 +75,7 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
Service #{item.serviceId} Service #{item.serviceId}
</span> </span>
) : ( ) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground">
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -92,10 +90,10 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
</div> </div>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right flex-shrink-0">
<div <div
className={`text-xl font-bold ${ className={`text-xl font-bold ${
isLinked ? "text-blue-900 group-hover:text-blue-700" : "text-slate-900" isLinked ? "text-primary group-hover:text-primary/80" : "text-foreground"
}`} }`}
> >
{formatCurrency(item.amount || 0, currency)} {formatCurrency(item.amount || 0, currency)}
@ -116,20 +114,20 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
}; };
return ( return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> <div className="bg-card rounded-2xl border border-border overflow-hidden">
<div className="px-6 py-4 bg-slate-50 border-b border-slate-200"> <div className="px-6 py-4 bg-muted/50 border-b border-border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-900">Items & Services</h3> <h3 className="text-lg font-semibold text-foreground">Items & Services</h3>
<div className="flex items-center gap-4 text-xs text-slate-500"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
{hasLinkedItems && ( {hasLinkedItems && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<div className="w-2 h-2 bg-green-500 rounded-full"></div> <div className="w-2 h-2 bg-success rounded-full"></div>
<span>Linked to service</span> <span>Linked to service</span>
</div> </div>
)} )}
{hasOneTimeItems && ( {hasOneTimeItems && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1.5">
<div className="w-2 h-2 bg-slate-400 rounded-full"></div> <div className="w-2 h-2 bg-muted-foreground/50 rounded-full"></div>
<span>One-time item</span> <span>One-time item</span>
</div> </div>
)} )}
@ -138,12 +136,10 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
</div> </div>
<div className="p-6"> <div className="p-6">
{items.length > 0 ? ( {items.length > 0 ? (
<div className="space-y-2"> <div className="space-y-3">{items.map(item => renderItemContent(item))}</div>
{items.map((item, index) => renderItemContent(item, index))}
</div>
) : ( ) : (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-slate-400 mb-2"> <div className="text-muted-foreground/50 mb-2">
<svg <svg
className="w-12 h-12 mx-auto" className="w-12 h-12 mx-auto"
fill="none" fill="none"
@ -158,7 +154,7 @@ export function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {
/> />
</svg> </svg>
</div> </div>
<p className="text-slate-500">No items found on this invoice.</p> <p className="text-muted-foreground">No items found on this invoice.</p>
</div> </div>
)} )}
</div> </div>

View File

@ -91,92 +91,81 @@ export function InvoiceSummaryBar({
const statusLabel = statusLabelMap[invoice.status] ?? invoice.status; const statusLabel = statusLabelMap[invoice.status] ?? invoice.status;
return ( return (
<div <div className="px-6 py-8 sm:px-8">
className={cn( {/* Header layout with proper alignment */}
"sticky top-0 z-10 bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/80", <div className="flex flex-col gap-6">
"border-b border-slate-200 shadow-sm", {/* Top row: Amount and Actions */}
"px-4 sm:px-6 lg:px-8 py-6" <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-6">
)}
>
<div className="max-w-4xl mx-auto">
{/* Header layout with proper alignment */}
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6">
{/* Left section: Amount, currency, and status */} {/* Left section: Amount, currency, and status */}
<div className="flex-1"> <div className="flex items-center gap-4 flex-wrap">
<div className="flex items-baseline gap-4 mb-3"> <div className="text-4xl sm:text-5xl font-bold text-foreground leading-none">
<div className="text-4xl lg:text-5xl font-bold text-slate-900 leading-none"> {formattedTotal}
{formattedTotal}
</div>
<div className="text-lg font-medium text-slate-500 uppercase tracking-wide">
{invoice.currency?.toUpperCase()}
</div>
<StatusPill
size="md"
variant={statusVariant}
label={statusLabel}
className="font-semibold"
/>
</div> </div>
<div className="text-base font-medium text-muted-foreground uppercase tracking-wide">
{invoice.currency?.toUpperCase()}
</div>
<StatusPill
size="md"
variant={statusVariant}
label={statusLabel}
className="font-semibold"
/>
</div>
{/* Due date information */} {/* Right section: Actions */}
{(dueDisplay || relativeDue) && ( <div className="flex flex-row gap-3 flex-shrink-0">
<div className="flex items-center gap-2 text-sm text-slate-600"> <Button
{dueDisplay && <span>Due {dueDisplay}</span>} variant="outline"
{relativeDue && ( onClick={onDownload}
<> disabled={!onDownload}
{dueDisplay && <span className="text-slate-400"></span>} loading={loadingDownload}
<span leftIcon={<ArrowDownTrayIcon className="h-4 w-4" />}
className={cn( >
"font-medium", Download PDF
invoice.status === "Overdue" ? "text-red-600" : "text-amber-600" </Button>
)} {(invoice.status === "Unpaid" || invoice.status === "Overdue") && (
> <Button
{relativeDue} onClick={onPay}
</span> disabled={!onPay}
</> loading={loadingPayment}
)} rightIcon={<ArrowTopRightOnSquareIcon className="h-4 w-4" />}
</div> variant="default"
>
Pay Now
</Button>
)}
</div>
</div>
{/* Bottom row: Due date and Invoice metadata */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-sm text-muted-foreground border-t border-border pt-4">
{/* Due date information */}
<div className="flex items-center gap-2">
{dueDisplay && <span>Due {dueDisplay}</span>}
{relativeDue && (
<>
{dueDisplay && <span className="text-muted-foreground/50"></span>}
<span
className={cn(
"font-medium",
invoice.status === "Overdue" ? "text-destructive" : "text-warning"
)}
>
{relativeDue}
</span>
</>
)} )}
</div> </div>
{/* Right section: Actions and invoice info */} {/* Invoice metadata */}
<div className="flex flex-col lg:items-end gap-4"> <div className="flex items-center gap-2">
{/* Action buttons */} <span className="font-semibold text-foreground">Invoice #{invoice.number}</span>
<div className="flex flex-col sm:flex-row gap-3 lg:flex-row-reverse"> {issuedDisplay && (
<Button <>
variant="outline" <span className="text-muted-foreground/50"></span>
onClick={onDownload} <span>Issued {issuedDisplay}</span>
disabled={!onDownload} </>
loading={loadingDownload} )}
leftIcon={<ArrowDownTrayIcon className="h-4 w-4" />}
className="order-2 sm:order-1 lg:order-2"
>
Download PDF
</Button>
{(invoice.status === "Unpaid" || invoice.status === "Overdue") && (
<Button
onClick={onPay}
disabled={!onPay}
loading={loadingPayment}
rightIcon={<ArrowTopRightOnSquareIcon className="h-4 w-4" />}
variant="default"
className="order-1 sm:order-2 lg:order-1"
>
Pay Now
</Button>
)}
</div>
{/* Invoice metadata - inline layout */}
<div className="flex flex-col sm:flex-row lg:flex-col xl:flex-row gap-2 lg:items-end text-sm text-slate-600">
<div className="font-semibold text-slate-900">Invoice #{invoice.number}</div>
{issuedDisplay && (
<>
<span className="hidden sm:inline lg:hidden xl:inline text-slate-400"></span>
<div>Issued {issuedDisplay}</div>
</>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,30 +13,30 @@ export function InvoiceTotals({ subtotal, tax, total }: InvoiceTotalsProps) {
const { formatCurrency } = useFormatCurrency(); const { formatCurrency } = useFormatCurrency();
return ( return (
<div className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> <div className="bg-card rounded-2xl border border-border overflow-hidden">
<div className="px-8 py-6"> <div className="px-6 py-5 bg-muted/50 border-b border-border">
<h3 className="text-lg font-semibold text-slate-900 mb-6">Invoice Summary</h3> <h3 className="text-base font-semibold text-foreground">Invoice Summary</h3>
</div>
<div className="px-6 py-5">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center text-slate-600"> <div className="flex justify-between items-center">
<span className="font-medium">Subtotal</span> <span className="text-sm text-muted-foreground">Subtotal</span>
<span className="font-semibold text-slate-900">{formatCurrency(subtotal)}</span> <span className="text-sm font-semibold text-foreground">
{formatCurrency(subtotal)}
</span>
</div> </div>
{tax > 0 && ( {tax > 0 && (
<div className="flex justify-between items-center text-slate-600"> <div className="flex justify-between items-center">
<span className="font-medium">Tax</span> <span className="text-sm text-muted-foreground">Tax</span>
<span className="font-semibold text-slate-900">{formatCurrency(tax)}</span> <span className="text-sm font-semibold text-foreground">{formatCurrency(tax)}</span>
</div> </div>
)} )}
<div className="border-t border-slate-300 pt-4 mt-6"> <div className="border-t border-border pt-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xl font-bold text-slate-900">Total Amount</span> <span className="text-base font-semibold text-foreground">Total</span>
<div className="flex items-baseline gap-2"> <span className="text-xl font-bold text-foreground">{formatCurrency(total)}</span>
<div className="text-3xl font-bold text-slate-900">{formatCurrency(total)}</div>
<div className="text-lg font-medium text-slate-500">JPY</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -94,36 +94,38 @@ export function InvoicesList({
} }
return ( return (
<div className={cn("space-y-4", className)}> <div
{/* Clean Header */} className={cn(
"bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden",
className
)}
>
{/* Search/Filter Header */}
{showFilters && ( {showFilters && (
<div className="bg-white/80 backdrop-blur-sm rounded-xl border border-gray-200/60 px-5 py-4 shadow-sm"> <div className="px-6 py-4 border-b border-border">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
{/* Title Section */} {/* Search Input */}
<div className="flex items-center gap-3"> <div className="relative flex-1 max-w-sm">
<h2 className="text-lg font-semibold text-gray-900">Invoices</h2> <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{pagination?.totalItems && ( <MagnifyingGlassIcon className="h-5 w-5 text-muted-foreground" />
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700"> </div>
{pagination.totalItems} total <input
</span> type="text"
)} className="block w-full pl-10 pr-3 py-2.5 text-sm border border-border rounded-lg bg-card shadow-sm placeholder-muted-foreground text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors"
placeholder="Search invoices..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div> </div>
{/* Controls */} {/* Controls */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Search Input */} {/* Total Count */}
<div className="relative"> {pagination?.totalItems && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <span className="text-sm text-muted-foreground">
<MagnifyingGlassIcon className="h-4 w-4 text-gray-400" /> {pagination.totalItems} invoice{pagination.totalItems !== 1 ? "s" : ""}
</div> </span>
<input )}
type="text"
className="block w-64 pl-9 pr-4 py-2.5 text-sm border border-gray-200 rounded-lg bg-white/50 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all duration-200"
placeholder="Search invoices..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
{/* Filter Dropdown */} {/* Filter Dropdown */}
{!isSubscriptionMode && ( {!isSubscriptionMode && (
@ -135,7 +137,7 @@ export function InvoicesList({
setStatusFilter(nextValue); setStatusFilter(nextValue);
setCurrentPage(1); setCurrentPage(1);
}} }}
className="block w-36 pl-3 pr-8 py-2.5 text-sm border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 rounded-lg appearance-none bg-white/50 cursor-pointer transition-all duration-200" className="block w-40 pl-3 pr-8 py-2.5 text-sm border border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary rounded-lg appearance-none bg-card shadow-sm text-foreground cursor-pointer transition-colors"
> >
{statusFilterOptions.map(option => ( {statusFilterOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@ -143,8 +145,8 @@ export function InvoicesList({
</option> </option>
))} ))}
</select> </select>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> <div className="absolute inset-y-0 right-0 flex items-center pr-2.5 pointer-events-none">
<ChevronDownIcon className="h-4 w-4 text-gray-400" /> <ChevronDownIcon className="h-4 w-4 text-muted-foreground" />
</div> </div>
</div> </div>
)} )}
@ -154,24 +156,24 @@ export function InvoicesList({
)} )}
{/* Invoice Table */} {/* Invoice Table */}
<div className="bg-white rounded-xl border border-gray-200/60 shadow-sm overflow-hidden"> <InvoiceTable
<InvoiceTable invoices={filtered}
invoices={filtered} loading={isLoading}
loading={isLoading} compact={compact}
compact={compact} className="border-0 rounded-none shadow-none"
className="border-0 rounded-none shadow-none" />
/>
{pagination && filtered.length > 0 && ( {/* Pagination */}
<div className="border-t border-gray-100 bg-gray-50/30 px-6 py-4"> {pagination && filtered.length > 0 && (
<PaginationBar <div className="border-t border-border px-6 py-4">
currentPage={currentPage} <PaginationBar
pageSize={pageSize} currentPage={currentPage}
totalItems={pagination?.totalItems || 0} pageSize={pageSize}
onPageChange={setCurrentPage} totalItems={pagination?.totalItems || 0}
/> onPageChange={setCurrentPage}
</div> />
)} </div>
</div> )}
</div> </div>
); );
} }

View File

@ -34,16 +34,16 @@ interface InvoiceTableProps {
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case "paid": case "paid":
return <CheckCircleIcon className="h-5 w-5 text-green-500" />; return <CheckCircleIcon className="h-5 w-5 text-success" />;
case "unpaid": case "unpaid":
return <ClockIcon className="h-5 w-5 text-yellow-500" />; return <ClockIcon className="h-5 w-5 text-warning" />;
case "overdue": case "overdue":
return <ExclamationTriangleIcon className="h-5 w-5 text-red-500" />; return <ExclamationTriangleIcon className="h-5 w-5 text-destructive" />;
case "cancelled": case "cancelled":
case "canceled": case "canceled":
return <ExclamationTriangleIcon className="h-5 w-5 text-gray-500" />; return <ExclamationTriangleIcon className="h-5 w-5 text-muted-foreground" />;
default: default:
return <ClockIcon className="h-5 w-5 text-gray-500" />; return <ClockIcon className="h-5 w-5 text-muted-foreground" />;
} }
}; };
@ -113,22 +113,22 @@ export function InvoiceTable({
const baseColumns = [ const baseColumns = [
{ {
key: "invoice", key: "invoice",
header: "Invoice Details", header: "Invoice",
className: "w-1/3", className: "w-1/3",
render: (invoice: Invoice) => { render: (invoice: Invoice) => {
const statusIcon = getStatusIcon(invoice.status); const statusIcon = getStatusIcon(invoice.status);
return ( return (
<div className="flex items-start space-x-3 py-3"> <div className="flex items-start space-x-3 py-5">
<div className="flex-shrink-0 mt-1">{statusIcon}</div> <div className="flex-shrink-0 mt-0.5">{statusIcon}</div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-semibold text-gray-900 text-sm">{invoice.number}</div> <div className="font-semibold text-foreground text-sm">{invoice.number}</div>
{!compact && invoice.description && ( {!compact && invoice.description && (
<div className="text-sm text-gray-600 mt-1.5 line-clamp-2 leading-relaxed"> <div className="text-sm text-muted-foreground mt-1 line-clamp-1">
{invoice.description} {invoice.description}
</div> </div>
)} )}
{!compact && invoice.issuedAt && ( {!compact && invoice.issuedAt && (
<div className="text-xs text-gray-500 mt-2 font-medium"> <div className="text-xs text-muted-foreground mt-1.5">
Issued {format(new Date(invoice.issuedAt), "MMM d, yyyy")} Issued {format(new Date(invoice.issuedAt), "MMM d, yyyy")}
</div> </div>
)} )}
@ -146,12 +146,12 @@ export function InvoiceTable({
switch (invoice.status) { switch (invoice.status) {
case "Paid": case "Paid":
return ( return (
<div className="space-y-2"> <div className="space-y-1.5">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-success-soft text-success border border-success/20">
Paid Paid
</span> </span>
{invoice.paidDate && ( {invoice.paidDate && (
<div className="text-xs text-green-700 font-medium"> <div className="text-xs text-muted-foreground">
{format(new Date(invoice.paidDate), "MMM d, yyyy")} {format(new Date(invoice.paidDate), "MMM d, yyyy")}
</div> </div>
)} )}
@ -160,12 +160,12 @@ export function InvoiceTable({
case "Overdue": case "Overdue":
return ( return (
<div className="space-y-2"> <div className="space-y-1.5">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-800 border border-red-200"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-destructive-soft text-destructive border border-destructive/20">
Overdue Overdue
</span> </span>
{invoice.daysOverdue && ( {invoice.daysOverdue && (
<div className="text-xs text-red-700 font-medium"> <div className="text-xs text-destructive">
{invoice.daysOverdue} day{invoice.daysOverdue !== 1 ? "s" : ""} overdue {invoice.daysOverdue} day{invoice.daysOverdue !== 1 ? "s" : ""} overdue
</div> </div>
)} )}
@ -174,12 +174,12 @@ export function InvoiceTable({
case "Unpaid": case "Unpaid":
return ( return (
<div className="space-y-2"> <div className="space-y-1.5">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 border border-yellow-200"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-warning-soft text-warning border border-warning/20">
Unpaid Unpaid
</span> </span>
{invoice.dueDate && ( {invoice.dueDate && (
<div className="text-xs text-yellow-700 font-medium"> <div className="text-xs text-muted-foreground">
Due {format(new Date(invoice.dueDate), "MMM d, yyyy")} Due {format(new Date(invoice.dueDate), "MMM d, yyyy")}
</div> </div>
)} )}
@ -192,7 +192,7 @@ export function InvoiceTable({
} }
}; };
return <div className="py-3">{renderStatusWithDate()}</div>; return <div className="py-5">{renderStatusWithDate()}</div>;
}, },
}, },
{ {
@ -200,8 +200,8 @@ export function InvoiceTable({
header: "Amount", header: "Amount",
className: "w-32 text-right", className: "w-32 text-right",
render: (invoice: Invoice) => ( render: (invoice: Invoice) => (
<div className="py-3 text-right"> <div className="py-5 text-right">
<div className="font-bold text-gray-900 text-base"> <div className="font-bold text-foreground text-sm tabular-nums">
{formatCurrency(invoice.total, invoice.currency)} {formatCurrency(invoice.total, invoice.currency)}
</div> </div>
</div> </div>
@ -213,15 +213,15 @@ export function InvoiceTable({
if (showActions) { if (showActions) {
baseColumns.push({ baseColumns.push({
key: "actions", key: "actions",
header: "Actions", header: "",
className: "w-48 text-right", className: "w-44 text-right",
render: (invoice: Invoice) => { render: (invoice: Invoice) => {
const canPay = invoice.status === "Unpaid" || invoice.status === "Overdue"; const canPay = invoice.status === "Unpaid" || invoice.status === "Overdue";
const isPaymentLoading = paymentLoading === invoice.id; const isPaymentLoading = paymentLoading === invoice.id;
const isDownloadLoading = downloadLoading === invoice.id; const isDownloadLoading = downloadLoading === invoice.id;
return ( return (
<div className="py-3 flex justify-end items-center space-x-2"> <div className="py-5 flex justify-end items-center space-x-2">
{/* Payment Button - Only for unpaid invoices - Always on the left */} {/* Payment Button - Only for unpaid invoices - Always on the left */}
{canPay && ( {canPay && (
<Button <Button
@ -231,7 +231,7 @@ export function InvoiceTable({
void handlePayment(invoice, event); void handlePayment(invoice, event);
}} }}
loading={isPaymentLoading} loading={isPaymentLoading}
className="text-xs font-medium shadow-sm" className="text-xs font-medium"
> >
Pay Now Pay Now
</Button> </Button>
@ -248,7 +248,7 @@ export function InvoiceTable({
leftIcon={ leftIcon={
!isDownloadLoading ? <ArrowDownTrayIcon className="h-4 w-4" /> : undefined !isDownloadLoading ? <ArrowDownTrayIcon className="h-4 w-4" /> : undefined
} }
className="text-xs font-medium border-gray-300 hover:border-gray-400 hover:bg-gray-50" className="text-xs font-medium"
title="Download PDF" title="Download PDF"
> >
PDF PDF
@ -270,38 +270,36 @@ export function InvoiceTable({
if (loading) { if (loading) {
return ( return (
<div className={cn("bg-white overflow-hidden", className)}> <div className={cn("bg-card overflow-hidden", className)}>
<div className="animate-pulse"> <div className="animate-pulse">
{/* Header skeleton */} {/* Header skeleton */}
<div className="bg-gradient-to-r from-gray-50 to-gray-50/80 px-6 py-4 border-b border-gray-200/80"> <div className="bg-muted/50 px-6 py-4 border-b border-border">
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<div className="h-3 bg-gray-200 rounded w-32"></div> <div className="h-3 bg-muted rounded w-32"></div>
<div className="h-3 bg-gray-200 rounded w-16"></div> <div className="h-3 bg-muted rounded w-16"></div>
<div className="h-3 bg-gray-200 rounded w-20"></div> <div className="h-3 bg-muted rounded w-20"></div>
<div className="h-3 bg-gray-200 rounded w-24"></div> <div className="h-3 bg-muted rounded w-24"></div>
</div> </div>
</div> </div>
{/* Row skeletons */} {/* Row skeletons */}
<div className="divide-y divide-gray-100/60"> <div className="divide-y divide-border">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="px-6 py-5"> <div key={i} className="px-6 py-5">
<div className="grid grid-cols-4 gap-4 items-center"> <div className="grid grid-cols-4 gap-4 items-center">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="h-5 w-5 bg-gray-200 rounded-full"></div> <div className="h-5 w-5 bg-muted rounded-full"></div>
<div className="space-y-2.5 flex-1"> <div className="space-y-2 flex-1">
<div className="h-4 bg-gray-200 rounded w-28"></div> <div className="h-4 bg-muted rounded w-28"></div>
<div className="h-3 bg-gray-200 rounded w-40"></div> <div className="h-3 bg-muted rounded w-40"></div>
<div className="h-3 bg-gray-200 rounded w-24"></div>
</div> </div>
</div> </div>
<div className="h-6 bg-gray-200 rounded-full w-20"></div> <div className="h-5 bg-muted rounded-full w-16"></div>
<div className="text-right space-y-2"> <div className="text-right">
<div className="h-4 bg-gray-200 rounded w-24 ml-auto"></div> <div className="h-4 bg-muted rounded w-20 ml-auto"></div>
<div className="h-3 bg-gray-200 rounded w-20 ml-auto"></div>
</div> </div>
<div className="text-right flex justify-end space-x-2"> <div className="text-right flex justify-end space-x-2">
<div className="h-8 bg-gray-200 rounded w-16"></div> <div className="h-8 bg-muted rounded w-16"></div>
<div className="h-8 bg-gray-200 rounded w-20"></div> <div className="h-8 bg-muted rounded w-14"></div>
</div> </div>
</div> </div>
</div> </div>
@ -313,7 +311,7 @@ export function InvoiceTable({
} }
return ( return (
<div className={cn("bg-white overflow-hidden", className)}> <div className={cn("bg-card overflow-hidden", className)}>
<DataTable <DataTable
data={invoices} data={invoices}
columns={columns} columns={columns}
@ -322,12 +320,14 @@ export function InvoiceTable({
className={cn( className={cn(
"invoice-table", "invoice-table",
// Header styling - cleaner and more modern // Header styling - cleaner and more modern
"[&_thead]:bg-gradient-to-r [&_thead]:from-gray-50 [&_thead]:to-gray-50/80", "[&_thead]:bg-muted/50",
"[&_thead_th]:px-6 [&_thead_th]:py-3.5 [&_thead_th]:text-xs [&_thead_th]:font-medium [&_thead_th]:text-gray-600 [&_thead_th]:uppercase [&_thead_th]:tracking-wide", "[&_thead_th]:px-6 [&_thead_th]:py-3.5 [&_thead_th]:text-xs [&_thead_th]:font-medium [&_thead_th]:text-muted-foreground [&_thead_th]:uppercase [&_thead_th]:tracking-wide",
"[&_thead_th]:border-b [&_thead_th]:border-gray-200/80", "[&_thead_th]:border-b [&_thead_th]:border-border",
// Right-align Amount column header (3rd column)
"[&_thead_th:nth-child(3)]:text-right",
// Row styling - enhanced hover and spacing // Row styling - enhanced hover and spacing
"[&_tbody_tr]:border-b [&_tbody_tr]:border-gray-100/60 [&_tbody_tr]:transition-all [&_tbody_tr]:duration-200", "[&_tbody_tr]:border-b [&_tbody_tr]:border-border [&_tbody_tr]:transition-all [&_tbody_tr]:duration-200",
"[&_tbody_tr:hover]:bg-gradient-to-r [&_tbody_tr:hover]:from-blue-50/30 [&_tbody_tr:hover]:to-indigo-50/20 [&_tbody_tr]:cursor-pointer", "[&_tbody_tr:hover]:bg-primary/5 [&_tbody_tr]:cursor-pointer",
"[&_tbody_tr:last-child]:border-b-0", "[&_tbody_tr:last-child]:border-b-0",
// Cell styling - better spacing // Cell styling - better spacing
"[&_tbody_td]:px-6 [&_tbody_td]:py-1 [&_tbody_td]:align-top", "[&_tbody_td]:px-6 [&_tbody_td]:py-1 [&_tbody_td]:align-top",

View File

@ -109,14 +109,9 @@ export function InvoiceDetailContainer() {
{ label: "Invoices", href: "/billing/invoices" }, { label: "Invoices", href: "/billing/invoices" },
{ label: `#${invoice.id}` }, { label: `#${invoice.id}` },
]} ]}
actions={
<Link href="/billing/invoices" className="text-sm font-medium text-primary hover:underline">
Back to invoices
</Link>
}
> >
<div className="max-w-4xl"> <div>
<div className="bg-card text-card-foreground rounded-3xl shadow-[var(--cp-card-shadow-lg)] border border-border overflow-hidden"> <div className="bg-card text-card-foreground rounded-2xl shadow-[var(--cp-shadow-1)] border border-border overflow-hidden">
<InvoiceSummaryBar <InvoiceSummaryBar
invoice={invoice} invoice={invoice}
loadingDownload={loadingDownload} loadingDownload={loadingDownload}

View File

@ -38,7 +38,7 @@ export function CatalogBackLink({
size="sm" size="sm"
variant="ghost" variant="ghost"
leftIcon={icon} leftIcon={icon}
className={cn("text-gray-600 hover:text-gray-900", buttonClassName)} className={cn("text-muted-foreground hover:text-foreground", buttonClassName)}
> >
{label} {label}
</Button> </Button>

View File

@ -36,9 +36,9 @@ export function CatalogHero({
align === "center" ? "mx-auto max-w-2xl" : "" align === "center" ? "mx-auto max-w-2xl" : ""
)} )}
> >
{eyebrow ? <div className="text-xs font-medium text-blue-700">{eyebrow}</div> : null} {eyebrow ? <div className="text-xs font-medium text-primary">{eyebrow}</div> : null}
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 leading-tight">{title}</h1> <h1 className="text-2xl md:text-3xl font-bold text-foreground leading-tight">{title}</h1>
<p className="text-sm text-gray-600 leading-relaxed">{description}</p> <p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
{children ? <div className="mt-1 w-full">{children}</div> : null} {children ? <div className="mt-1 w-full">{children}</div> : null}
</div> </div>
); );

View File

@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard"; import { AnimatedCard } from "@/components/molecules/AnimatedCard/AnimatedCard";
import { Button } from "@/components/atoms/button"; import { Button } from "@/components/atoms/button";
import { ArrowRightIcon } from "@heroicons/react/24/outline"; import { ArrowRightIcon, CheckIcon } from "@heroicons/react/24/outline";
export function ServiceHeroCard({ export function ServiceHeroCard({
title, title,
@ -20,24 +20,25 @@ export function ServiceHeroCard({
href: string; href: string;
color: "blue" | "green" | "purple"; color: "blue" | "green" | "purple";
}) { }) {
// Use design system tokens for colors
const colorClasses = { const colorClasses = {
blue: { blue: {
iconBg: "bg-blue-100", iconBg: "bg-info-soft",
iconText: "text-blue-600", iconText: "text-info",
border: "border-blue-100", border: "border-info/20",
hoverBorder: "hover:border-blue-200", hoverBorder: "hover:border-info/40",
}, },
green: { green: {
iconBg: "bg-green-100", iconBg: "bg-success-soft",
iconText: "text-green-600", iconText: "text-success",
border: "border-green-100", border: "border-success/20",
hoverBorder: "hover:border-green-200", hoverBorder: "hover:border-success/40",
}, },
purple: { purple: {
iconBg: "bg-purple-100", iconBg: "bg-primary/10",
iconText: "text-purple-600", iconText: "text-primary",
border: "border-purple-100", border: "border-primary/20",
hoverBorder: "hover:border-purple-200", hoverBorder: "hover:border-primary/40",
}, },
} as const; } as const;
@ -47,25 +48,25 @@ export function ServiceHeroCard({
<AnimatedCard <AnimatedCard
className={`relative group rounded-2xl overflow-hidden h-full border-2 ${colors.border} ${colors.hoverBorder} transition-all duration-300 hover:shadow-lg hover:-translate-y-1`} className={`relative group rounded-2xl overflow-hidden h-full border-2 ${colors.border} ${colors.hoverBorder} transition-all duration-300 hover:shadow-lg hover:-translate-y-1`}
> >
<div className="p-8 h-full flex flex-col bg-white"> <div className="p-8 h-full flex flex-col bg-card">
{/* Icon and Title */} {/* Icon and Title */}
<div className="flex items-start gap-4 mb-4"> <div className="flex items-start gap-4 mb-4">
<div className={`p-3 rounded-xl ${colors.iconBg} flex-shrink-0`}> <div className={`p-3 rounded-xl ${colors.iconBg} flex-shrink-0`}>
<div className={colors.iconText}>{icon}</div> <div className={colors.iconText}>{icon}</div>
</div> </div>
<div> <div>
<h3 className="text-2xl font-bold text-gray-900">{title}</h3> <h3 className="text-2xl font-bold text-foreground">{title}</h3>
</div> </div>
</div> </div>
{/* Description */} {/* Description */}
<p className="text-sm text-gray-600 mb-6 leading-relaxed">{description}</p> <p className="text-sm text-muted-foreground mb-6 leading-relaxed">{description}</p>
{/* Features List */} {/* Features List */}
<ul className="space-y-2.5 mb-8 flex-grow"> <ul className="space-y-2.5 mb-8 flex-grow">
{features.map((feature, index) => ( {features.map((feature, index) => (
<li key={index} className="flex items-start gap-2.5 text-sm text-gray-700"> <li key={index} className="flex items-start gap-2.5 text-sm text-foreground/80">
<span className="text-green-600 mt-0.5 flex-shrink-0"></span> <CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
<span>{feature}</span> <span>{feature}</span>
</li> </li>
))} ))}

View File

@ -2,7 +2,7 @@
import { AnimatedCard } from "@/components/molecules"; import { AnimatedCard } from "@/components/molecules";
import { Button } from "@/components/atoms/button"; import { Button } from "@/components/atoms/button";
import { ArrowRightIcon } from "@heroicons/react/24/outline"; import { ArrowRightIcon, CheckIcon } from "@heroicons/react/24/outline";
import type { import type {
InternetPlanCatalogItem, InternetPlanCatalogItem,
InternetInstallationCatalogItem, InternetInstallationCatalogItem,
@ -22,6 +22,26 @@ interface InternetPlanCardProps {
disabledReason?: string; disabledReason?: string;
} }
// Tier-based styling using design tokens
const tierStyles = {
gold: {
card: "border-2 border-warning/50 bg-gradient-to-br from-card to-warning-soft/30 shadow-xl hover:shadow-2xl ring-2 ring-warning/20",
border: "border-warning/30",
},
platinum: {
card: "border-2 border-primary/50 bg-gradient-to-br from-card to-info-soft/30 shadow-xl hover:shadow-2xl ring-2 ring-primary/20",
border: "border-primary/30",
},
silver: {
card: "border-2 border-muted-foreground/30 bg-gradient-to-br from-card to-muted/30 shadow-lg hover:shadow-xl ring-1 ring-border",
border: "border-muted-foreground/20",
},
default: {
card: "border border-border bg-card shadow-md hover:shadow-xl",
border: "border-border",
},
} as const;
export function InternetPlanCard({ export function InternetPlanCard({
plan, plan,
installations, installations,
@ -51,14 +71,11 @@ export function InternetPlanCard({
const minInstallationPrice = installationPrices.length ? Math.min(...installationPrices) : 0; const minInstallationPrice = installationPrices.length ? Math.min(...installationPrices) : 0;
const getBorderClass = () => { const getTierStyle = () => {
if (isGold) if (isGold) return tierStyles.gold;
return "border-2 border-yellow-300 bg-gradient-to-br from-white to-yellow-50/30 shadow-xl hover:shadow-2xl ring-2 ring-yellow-200/50"; if (isPlatinum) return tierStyles.platinum;
if (isPlatinum) if (isSilver) return tierStyles.silver;
return "border-2 border-indigo-300 bg-gradient-to-br from-white to-indigo-50/30 shadow-xl hover:shadow-2xl ring-2 ring-indigo-200/50"; return tierStyles.default;
if (isSilver)
return "border-2 border-gray-300 bg-gradient-to-br from-white to-gray-50/30 shadow-lg hover:shadow-xl ring-1 ring-gray-200/50";
return "border border-gray-200 bg-white shadow-md hover:shadow-xl";
}; };
const getTierBadgeVariant = (): BadgeVariant => { const getTierBadgeVariant = (): BadgeVariant => {
@ -68,16 +85,18 @@ export function InternetPlanCard({
return "default"; return "default";
}; };
const tierStyle = getTierStyle();
const renderFeature = (feature: string, index: number) => { const renderFeature = (feature: string, index: number) => {
const [label, detail] = feature.split(":"); const [label, detail] = feature.split(":");
if (detail) { if (detail) {
return ( return (
<li key={index} className="flex items-start gap-2"> <li key={index} className="flex items-start gap-2">
<span className="text-green-600 mt-0.5 flex-shrink-0"></span> <CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
<span> <span>
<span className="font-medium text-gray-900">{label.trim()}:</span>{" "} <span className="font-medium text-foreground">{label.trim()}:</span>{" "}
<span className="text-gray-700">{detail.trim()}</span> <span className="text-muted-foreground">{detail.trim()}</span>
</span> </span>
</li> </li>
); );
@ -85,8 +104,8 @@ export function InternetPlanCard({
return ( return (
<li key={index} className="flex items-start gap-2"> <li key={index} className="flex items-start gap-2">
<span className="text-green-600 mt-0.5 flex-shrink-0"></span> <CheckIcon className="h-4 w-4 text-success mt-0.5 flex-shrink-0" />
<span className="text-gray-700">{feature}</span> <span className="text-muted-foreground">{feature}</span>
</li> </li>
); );
}; };
@ -127,11 +146,11 @@ export function InternetPlanCard({
return ( return (
<AnimatedCard <AnimatedCard
variant="static" variant="static"
className={`overflow-hidden flex flex-col h-full transition-all duration-200 ease-out hover:-translate-y-1 rounded-xl ${getBorderClass()}`} className={`overflow-hidden flex flex-col h-full transition-all duration-200 ease-out hover:-translate-y-1 rounded-xl ${tierStyle.card}`}
> >
<div className="p-6 sm:p-7 flex flex-col flex-grow space-y-5"> <div className="p-6 sm:p-7 flex flex-col flex-grow space-y-5">
{/* Header with badges */} {/* Header with badges */}
<div className="flex flex-col gap-3 pb-4 border-b border-gray-100"> <div className={`flex flex-col gap-3 pb-4 border-b ${tierStyle.border}`}>
<div className="inline-flex flex-wrap items-center gap-2 text-sm"> <div className="inline-flex flex-wrap items-center gap-2 text-sm">
<CardBadge <CardBadge
text={plan.internetPlanTier ?? "Plan"} text={plan.internetPlanTier ?? "Plan"}
@ -144,11 +163,11 @@ export function InternetPlanCard({
{/* Plan name and description - Full width */} {/* Plan name and description - Full width */}
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<h3 className="text-xl sm:text-2xl font-bold text-gray-900 leading-tight"> <h3 className="text-xl sm:text-2xl font-bold text-foreground leading-tight">
{planBaseName} {planBaseName}
</h3> </h3>
{plan.catalogMetadata?.tierDescription || plan.description ? ( {plan.catalogMetadata?.tierDescription || plan.description ? (
<p className="text-sm text-gray-600 leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
{plan.catalogMetadata?.tierDescription || plan.description} {plan.catalogMetadata?.tierDescription || plan.description}
</p> </p>
) : null} ) : null}
@ -167,10 +186,10 @@ export function InternetPlanCard({
{/* Features */} {/* Features */}
<div className="flex-grow pt-1"> <div className="flex-grow pt-1">
<h4 className="font-semibold text-gray-900 mb-4 text-sm uppercase tracking-wide"> <h4 className="font-semibold text-foreground mb-4 text-sm uppercase tracking-wide">
Your Plan Includes: Your Plan Includes:
</h4> </h4>
<ul className="space-y-3 text-sm text-gray-700">{renderPlanFeatures()}</ul> <ul className="space-y-3 text-sm">{renderPlanFeatures()}</ul>
</div> </div>
{/* Action Button */} {/* Action Button */}

View File

@ -116,7 +116,7 @@ export function InternetConfigureContainer({
description="Set up your internet service options" description="Set up your internet service options"
> >
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-500">Plan not found</p> <p className="text-muted-foreground">Plan not found</p>
</div> </div>
</PageLayout> </PageLayout>
); );
@ -210,7 +210,7 @@ export function InternetConfigureContainer({
title="Configure Internet Service" title="Configure Internet Service"
description="Set up your internet service options" description="Set up your internet service options"
> >
<div className="min-h-[70vh] bg-gradient-to-br from-slate-50 via-blue-50/20 to-slate-50"> <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">
{/* Plan Header */} {/* Plan Header */}
<PlanHeader plan={plan} /> <PlanHeader plan={plan} />
@ -238,21 +238,21 @@ function PlanHeader({ plan }: { plan: InternetPlanCatalogItem }) {
<Button <Button
as="a" as="a"
href="/catalog/internet" href="/catalog/internet"
variant="outline" variant="ghost"
size="sm" size="sm"
leftIcon={<ArrowLeftIcon className="w-4 h-4" />} leftIcon={<ArrowLeftIcon className="w-4 h-4" />}
className="mb-6" className="mb-6 text-muted-foreground hover:text-foreground"
> >
Back to Internet Plans Back to Internet Plans
</Button> </Button>
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 mb-5">Configure your plan</h1> <h1 className="text-2xl md:text-3xl font-bold text-foreground mb-5">Configure your plan</h1>
<span className="sr-only"> <span className="sr-only">
{planBaseName} {planBaseName}
{planDetail ? ` (${planDetail})` : ""} {planDetail ? ` (${planDetail})` : ""}
</span> </span>
<div className="inline-flex flex-wrap items-center justify-center gap-3 bg-white px-6 py-3 rounded-full border border-blue-100 shadow-sm text-sm"> <div className="inline-flex flex-wrap items-center justify-center gap-3 bg-card px-6 py-3 rounded-full border border-border shadow-sm text-sm">
{plan.internetPlanTier ? ( {plan.internetPlanTier ? (
<CardBadge <CardBadge
text={plan.internetPlanTier} text={plan.internetPlanTier}
@ -262,7 +262,7 @@ function PlanHeader({ plan }: { plan: InternetPlanCatalogItem }) {
) : null} ) : null}
{planDetail ? <CardBadge text={planDetail} variant="family" size="sm" /> : null} {planDetail ? <CardBadge text={planDetail} variant="family" size="sm" /> : null}
{plan.monthlyPrice && plan.monthlyPrice > 0 ? ( {plan.monthlyPrice && plan.monthlyPrice > 0 ? (
<span className="inline-flex items-center rounded-full bg-blue-600/10 px-3 py-1 text-sm font-semibold text-blue-700"> <span className="inline-flex items-center rounded-full bg-primary/10 px-3 py-1 text-sm font-semibold text-primary">
¥{plan.monthlyPrice.toLocaleString()}/month ¥{plan.monthlyPrice.toLocaleString()}/month
</span> </span>
) : null} ) : null}

View File

@ -72,7 +72,7 @@ export function ReviewOrderStep({
/> />
</div> </div>
<div className="flex justify-between pt-6 border-t"> <div className="flex justify-between pt-6 border-t border-border">
<Button onClick={onBack} variant="outline" leftIcon={<ArrowLeftIcon className="w-4 h-4" />}> <Button onClick={onBack} variant="outline" leftIcon={<ArrowLeftIcon className="w-4 h-4" />}>
Back to Add-ons Back to Add-ons
</Button> </Button>
@ -100,26 +100,26 @@ function OrderSummary({
oneTimeTotal: number; oneTimeTotal: number;
}) { }) {
return ( return (
<div className="bg-gradient-to-b from-white to-gray-50 shadow-xl rounded-lg border border-gray-200 p-6"> <div className="bg-card shadow-xl rounded-lg border border-border p-6">
{/* Receipt Header */} {/* Receipt Header */}
<div className="text-center border-b-2 border-dashed border-gray-300 pb-4 mb-6"> <div className="text-center border-b-2 border-dashed border-border pb-4 mb-6">
<h3 className="text-xl font-bold text-gray-900 mb-1">Order Summary</h3> <h3 className="text-xl font-bold text-foreground mb-1">Order Summary</h3>
<p className="text-sm text-gray-500">Review your configuration</p> <p className="text-sm text-muted-foreground">Review your configuration</p>
</div> </div>
{/* Plan Details */} {/* Plan Details */}
<div className="space-y-3 mb-6"> <div className="space-y-3 mb-6">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<h4 className="font-semibold text-gray-900">{plan.name}</h4> <h4 className="font-semibold text-foreground">{plan.name}</h4>
<p className="text-sm text-gray-600">Internet Service</p> <p className="text-sm text-muted-foreground">Internet Service</p>
{mode && <p className="text-sm text-gray-600">Access Mode: {mode}</p>} {mode && <p className="text-sm text-muted-foreground">Access Mode: {mode}</p>}
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-gray-900"> <p className="font-semibold text-foreground">
¥{(plan.monthlyPrice ?? 0).toLocaleString()} ¥{(plan.monthlyPrice ?? 0).toLocaleString()}
</p> </p>
<p className="text-xs text-gray-500">per month</p> <p className="text-xs text-muted-foreground">per month</p>
</div> </div>
</div> </div>
</div> </div>
@ -127,21 +127,21 @@ function OrderSummary({
{/* Installation */} {/* Installation */}
{(selectedInstallation.monthlyPrice ?? 0) > 0 || {(selectedInstallation.monthlyPrice ?? 0) > 0 ||
(selectedInstallation.oneTimePrice ?? 0) > 0 ? ( (selectedInstallation.oneTimePrice ?? 0) > 0 ? (
<div className="border-t border-gray-200 pt-4 mb-6"> <div className="border-t border-border pt-4 mb-6">
<h4 className="font-medium text-gray-900 mb-3">Installation</h4> <h4 className="font-medium text-foreground mb-3">Installation</h4>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">{selectedInstallation.name}</span> <span className="text-muted-foreground">{selectedInstallation.name}</span>
<span className="text-gray-900"> <span className="text-foreground">
{selectedInstallation.monthlyPrice && selectedInstallation.monthlyPrice > 0 && ( {selectedInstallation.monthlyPrice && selectedInstallation.monthlyPrice > 0 && (
<> <>
¥{selectedInstallation.monthlyPrice.toLocaleString()} ¥{selectedInstallation.monthlyPrice.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">/mo</span> <span className="text-xs text-muted-foreground ml-1">/mo</span>
</> </>
)} )}
{selectedInstallation.oneTimePrice && selectedInstallation.oneTimePrice > 0 && ( {selectedInstallation.oneTimePrice && selectedInstallation.oneTimePrice > 0 && (
<> <>
¥{selectedInstallation.oneTimePrice.toLocaleString()} ¥{selectedInstallation.oneTimePrice.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">/once</span> <span className="text-xs text-muted-foreground ml-1">/once</span>
</> </>
)} )}
</span> </span>
@ -151,23 +151,23 @@ function OrderSummary({
{/* Add-ons */} {/* Add-ons */}
{selectedAddons.length > 0 && ( {selectedAddons.length > 0 && (
<div className="border-t border-gray-200 pt-4 mb-6"> <div className="border-t border-border pt-4 mb-6">
<h4 className="font-medium text-gray-900 mb-3">Add-ons</h4> <h4 className="font-medium text-foreground mb-3">Add-ons</h4>
<div className="space-y-2"> <div className="space-y-2">
{selectedAddons.map(addon => ( {selectedAddons.map(addon => (
<div key={addon.sku} className="flex justify-between text-sm"> <div key={addon.sku} className="flex justify-between text-sm">
<span className="text-gray-600">{addon.name}</span> <span className="text-muted-foreground">{addon.name}</span>
<span className="text-gray-900"> <span className="text-foreground">
{addon.monthlyPrice && addon.monthlyPrice > 0 && ( {addon.monthlyPrice && addon.monthlyPrice > 0 && (
<> <>
¥{addon.monthlyPrice.toLocaleString()} ¥{addon.monthlyPrice.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">/mo</span> <span className="text-xs text-muted-foreground ml-1">/mo</span>
</> </>
)} )}
{addon.oneTimePrice && addon.oneTimePrice > 0 && ( {addon.oneTimePrice && addon.oneTimePrice > 0 && (
<> <>
¥{addon.oneTimePrice.toLocaleString()} ¥{addon.oneTimePrice.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">/once</span> <span className="text-xs text-muted-foreground ml-1">/once</span>
</> </>
)} )}
</span> </span>
@ -178,26 +178,24 @@ function OrderSummary({
)} )}
{/* Totals */} {/* Totals */}
<div className="border-t-2 border-dashed border-gray-300 pt-4 bg-gray-50 -mx-6 px-6 py-4 rounded-b-lg"> <div className="border-t-2 border-dashed border-border pt-4 bg-muted/50 -mx-6 px-6 py-4 rounded-b-lg">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-xl font-bold"> <div className="flex justify-between text-xl font-bold">
<span className="text-gray-900">Monthly Total</span> <span className="text-foreground">Monthly Total</span>
<span className="text-blue-600">¥{monthlyTotal.toLocaleString()}</span> <span className="text-primary">¥{monthlyTotal.toLocaleString()}</span>
</div> </div>
{oneTimeTotal > 0 && ( {oneTimeTotal > 0 && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600">One-time Total</span> <span className="text-muted-foreground">One-time Total</span>
<span className="text-orange-600 font-semibold"> <span className="text-warning font-semibold">¥{oneTimeTotal.toLocaleString()}</span>
¥{oneTimeTotal.toLocaleString()}
</span>
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* Receipt Footer */} {/* Receipt Footer */}
<div className="text-center mt-6 pt-4 border-t border-gray-200"> <div className="text-center mt-6 pt-4 border-t border-border">
<p className="text-xs text-gray-500">High-speed internet service</p> <p className="text-xs text-muted-foreground">High-speed internet service</p>
</div> </div>
</div> </div>
); );

View File

@ -24,8 +24,8 @@ export function SimPlanCard({ plan, isFamily }: { plan: SimCatalogProduct; isFam
<div className="flex items-start justify-between gap-4 mb-3"> <div className="flex items-start justify-between gap-4 mb-3">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DevicePhoneMobileIcon className="h-5 w-5 text-blue-600" /> <DevicePhoneMobileIcon className="h-5 w-5 text-primary" />
<span className="font-bold text-base text-gray-900">{plan.simDataSize}</span> <span className="font-bold text-base text-foreground">{plan.simDataSize}</span>
</div> </div>
{isFamilyPlan && <CardBadge text="Family Discount" variant="family" size="sm" />} {isFamilyPlan && <CardBadge text="Family Discount" variant="family" size="sm" />}
</div> </div>
@ -35,13 +35,13 @@ export function SimPlanCard({ plan, isFamily }: { plan: SimCatalogProduct; isFam
<div className="mb-4"> <div className="mb-4">
<CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" /> <CardPricing monthlyPrice={displayPrice} size="sm" alignment="left" />
{isFamilyPlan && ( {isFamilyPlan && (
<div className="text-xs text-green-600 font-medium mt-1">Discounted pricing applied</div> <div className="text-xs text-success font-medium mt-1">Discounted pricing applied</div>
)} )}
</div> </div>
{/* Description */} {/* Description */}
<div className="mb-4 flex-grow"> <div className="mb-4 flex-grow">
<p className="text-sm text-gray-600 leading-relaxed line-clamp-2">{plan.name}</p> <p className="text-sm text-muted-foreground leading-relaxed line-clamp-2">{plan.name}</p>
</div> </div>
{/* Action Button */} {/* Action Button */}

View File

@ -12,14 +12,14 @@ interface VpnPlanCardProps {
export function VpnPlanCard({ plan }: VpnPlanCardProps) { export function VpnPlanCard({ plan }: VpnPlanCardProps) {
return ( return (
<AnimatedCard className="p-6 border border-blue-200 hover:border-blue-300 transition-all duration-300 hover:shadow-lg flex flex-col h-full"> <AnimatedCard className="p-6 border border-primary/20 hover:border-primary/40 transition-all duration-300 hover:shadow-lg flex flex-col h-full">
{/* Header with icon and name */} {/* Header with icon and name */}
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
<div className="p-2 bg-blue-50 rounded-lg"> <div className="p-2 bg-primary/10 rounded-lg">
<ShieldCheckIcon className="h-6 w-6 text-blue-600" /> <ShieldCheckIcon className="h-6 w-6 text-primary" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="text-xl font-semibold text-gray-900">{plan.name}</h3> <h3 className="text-xl font-semibold text-foreground">{plan.name}</h3>
</div> </div>
</div> </div>

View File

@ -213,8 +213,8 @@ export function CheckoutContainer() {
</div> </div>
</div> </div>
<div className="bg-muted border border-border rounded-2xl p-6 md:p-7 text-center shadow-[var(--cp-shadow-1)]"> <div className="bg-card border border-border rounded-2xl p-6 md:p-7 text-center shadow-[var(--cp-shadow-1)]">
<div className="w-16 h-16 bg-card rounded-full flex items-center justify-center mx-auto mb-4 shadow-[var(--cp-shadow-1)] border border-border"> <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4 shadow-sm border border-primary/20">
<ShieldCheckIcon className="w-8 h-8 text-primary" /> <ShieldCheckIcon className="w-8 h-8 text-primary" />
</div> </div>
<h2 className="text-2xl font-bold text-foreground mb-2">Review & Submit</h2> <h2 className="text-2xl font-bold text-foreground mb-2">Review & Submit</h2>
@ -222,7 +222,7 @@ export function CheckoutContainer() {
Youre almost done. Confirm your details above, then submit your order. Well review and Youre almost done. Confirm your details above, then submit your order. Well review and
notify you when everything is ready. notify you when everything is ready.
</p> </p>
<div className="bg-card rounded-lg p-4 border border-border text-left max-w-2xl mx-auto shadow-[var(--cp-shadow-1)]"> <div className="bg-muted/50 rounded-lg p-4 border border-border text-left max-w-2xl mx-auto">
<h3 className="font-semibold text-foreground mb-2">What to expect</h3> <h3 className="font-semibold text-foreground mb-2">What to expect</h3>
<div className="text-sm text-muted-foreground space-y-1"> <div className="text-sm text-muted-foreground space-y-1">
<p> Our team reviews your order and schedules setup if needed</p> <p> Our team reviews your order and schedules setup if needed</p>
@ -252,8 +252,8 @@ export function CheckoutContainer() {
<div className="flex gap-4"> <div className="flex gap-4">
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
className="flex-1 py-4" className="flex-1 py-4 text-muted-foreground hover:text-foreground"
onClick={navigateBackToConfigure} onClick={navigateBackToConfigure}
> >
Back to Configuration Back to Configuration

View File

@ -1,175 +0,0 @@
"use client";
import { useState } from "react";
import { ArrowTrendingUpIcon } from "@heroicons/react/24/outline";
import { cn } from "@/lib/utils";
import { DashboardActivityItem } from "./DashboardActivityItem";
import {
filterActivities,
ACTIVITY_FILTERS,
getActivityNavigationPath,
isActivityClickable,
} from "../utils/dashboard.utils";
import type { Activity, ActivityFilter } from "@customer-portal/domain/dashboard";
export interface ActivityFeedProps {
activities: Activity[];
onItemClick?: (activity: Activity) => void;
className?: string;
maxItems?: number;
showFilter?: boolean;
loading?: boolean;
error?: string | null;
}
export function ActivityFeed({
activities,
onItemClick,
className,
maxItems = 10,
showFilter = true,
loading = false,
error = null,
}: ActivityFeedProps) {
const [filter, setFilter] = useState<ActivityFilter>("all");
const filteredActivities = filterActivities(activities, filter);
const displayActivities = filteredActivities.slice(0, maxItems);
const handleActivityClick = (activity: Activity) => {
if (onItemClick) {
onItemClick(activity);
} else if (isActivityClickable(activity)) {
const path = getActivityNavigationPath(activity);
if (path) {
window.location.href = path;
}
}
};
if (loading) {
return (
<div
className={cn(
"bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden",
className
)}
>
<div className="px-6 py-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<div className="h-6 bg-gray-200 rounded animate-pulse w-32" />
{showFilter && (
<div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
{ACTIVITY_FILTERS.map((_, index) => (
<div key={index} className="h-6 w-12 bg-gray-200 rounded animate-pulse" />
))}
</div>
)}
</div>
</div>
<div className="p-6 space-y-4">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="flex items-start space-x-4">
<div className="w-10 h-10 rounded-full bg-gray-200 animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4" />
<div className="h-3 bg-gray-200 rounded animate-pulse w-1/2" />
<div className="h-3 bg-gray-200 rounded animate-pulse w-1/4" />
</div>
</div>
))}
</div>
</div>
);
}
if (error) {
return (
<div
className={cn(
"bg-white rounded-2xl shadow-lg border border-red-200 overflow-hidden",
className
)}
>
<div className="px-6 py-4 border-b border-red-100">
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
</div>
<div className="p-6">
<div className="text-center py-8">
<div className="text-red-600 text-sm">{error}</div>
</div>
</div>
</div>
);
}
return (
<div
className={cn(
"bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden",
className
)}
>
<div className="px-6 py-4 border-b border-gray-100">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Recent Activity</h3>
{showFilter && (
<div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
{ACTIVITY_FILTERS.map(filterOption => (
<button
key={filterOption.key}
onClick={() => setFilter(filterOption.key)}
className={cn(
"px-2.5 py-1 text-xs rounded-md font-medium transition-all duration-200",
filter === filterOption.key
? "bg-white text-gray-900 shadow"
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
)}
>
{filterOption.label}
</button>
))}
</div>
)}
</div>
</div>
<div className="p-6 max-h-[360px] overflow-y-auto">
{displayActivities.length > 0 ? (
<div className="space-y-4">
{displayActivities.map(activity => {
const clickable = isActivityClickable(activity);
return (
<DashboardActivityItem
key={activity.id}
activity={activity}
onClick={clickable ? () => handleActivityClick(activity) : undefined}
/>
);
})}
</div>
) : (
<div className="text-center py-12">
<ArrowTrendingUpIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
{filter === "all" ? "No recent activity" : `No ${filter} activity`}
</h3>
<p className="mt-1 text-sm text-gray-500">
{filter === "all"
? "Your account activity will appear here."
: `Your ${filter} activity will appear here.`}
</p>
</div>
)}
</div>
{filteredActivities.length > maxItems && (
<div className="px-6 py-3 border-t border-gray-100 bg-gray-50">
<p className="text-xs text-gray-500 text-center">
Showing {maxItems} of {filteredActivities.length} activities
</p>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,110 @@
"use client";
import { useMemo } from "react";
import { format, isToday, isYesterday, isSameDay } from "date-fns";
import { ArrowTrendingUpIcon } from "@heroicons/react/24/outline";
import type { Activity } from "@customer-portal/domain/dashboard";
import { DashboardActivityItem } from "./DashboardActivityItem";
interface ActivityTimelineProps {
activities: Activity[];
onItemClick?: (activity: Activity) => void;
maxItems?: number;
}
interface GroupedActivities {
label: string;
date: Date;
activities: Activity[];
}
function formatDateLabel(date: Date): string {
if (isToday(date)) {
return "Today";
}
if (isYesterday(date)) {
return "Yesterday";
}
return format(date, "MMM d");
}
function groupActivitiesByDate(activities: Activity[]): GroupedActivities[] {
const groups: GroupedActivities[] = [];
for (const activity of activities) {
const activityDate = new Date(activity.date);
const existingGroup = groups.find(g => isSameDay(g.date, activityDate));
if (existingGroup) {
existingGroup.activities.push(activity);
} else {
groups.push({
label: formatDateLabel(activityDate),
date: activityDate,
activities: [activity],
});
}
}
// Sort groups by date (newest first)
return groups.sort((a, b) => b.date.getTime() - a.date.getTime());
}
export function ActivityTimeline({
activities,
onItemClick,
maxItems = 10,
}: ActivityTimelineProps) {
const groupedActivities = useMemo(() => {
const limited = activities.slice(0, maxItems);
return groupActivitiesByDate(limited);
}, [activities, maxItems]);
if (activities.length === 0) {
return (
<div className="text-center py-12">
<div className="w-12 h-12 rounded-xl bg-muted/50 flex items-center justify-center mx-auto mb-3">
<ArrowTrendingUpIcon className="h-6 w-6 text-muted-foreground/40" />
</div>
<h3 className="text-sm font-medium text-foreground">No recent activity</h3>
<p className="mt-1 text-sm text-muted-foreground max-w-xs mx-auto">
Your account activity will appear here once you start using our services.
</p>
</div>
);
}
return (
<div className="space-y-6">
{groupedActivities.map(group => (
<div key={group.label}>
{/* Date header */}
<div className="flex items-center gap-3 mb-3">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{group.label}
</span>
<div className="flex-1 h-px bg-border" />
</div>
{/* Activities for this date */}
<div className="space-y-0">
{group.activities.map((activity, index) => {
const isClickable =
activity.type === "invoice_created" || activity.type === "invoice_paid";
const isLastInGroup = index === group.activities.length - 1;
return (
<DashboardActivityItem
key={activity.id}
activity={activity}
onClick={isClickable && onItemClick ? () => onItemClick(activity) : undefined}
showConnector={!isLastInGroup}
/>
);
})}
</div>
</div>
))}
</div>
);
}

View File

@ -9,15 +9,12 @@ import {
ExclamationTriangleIcon, ExclamationTriangleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import type { Activity } from "@customer-portal/domain/dashboard"; import type { Activity } from "@customer-portal/domain/dashboard";
import { import { formatActivityDescription } from "../utils/dashboard.utils";
formatActivityDate,
formatActivityDescription,
getActivityIconGradient,
} from "../utils/dashboard.utils";
interface DashboardActivityItemProps { interface DashboardActivityItemProps {
activity: Activity; activity: Activity;
onClick?: () => void; onClick?: () => void;
showConnector?: boolean;
} }
const ICON_COMPONENTS: Record<Activity["type"], ComponentType<SVGProps<SVGSVGElement>>> = { const ICON_COMPONENTS: Record<Activity["type"], ComponentType<SVGProps<SVGSVGElement>>> = {
@ -28,43 +25,58 @@ const ICON_COMPONENTS: Record<Activity["type"], ComponentType<SVGProps<SVGSVGEle
case_closed: CheckCircleIcon, case_closed: CheckCircleIcon,
}; };
const ICON_COLORS: Record<Activity["type"], string> = {
invoice_created: "text-blue-500 bg-blue-50",
invoice_paid: "text-green-500 bg-green-50",
service_activated: "text-purple-500 bg-purple-50",
case_created: "text-amber-500 bg-amber-50",
case_closed: "text-green-500 bg-green-50",
};
const FALLBACK_ICON = ExclamationTriangleIcon; const FALLBACK_ICON = ExclamationTriangleIcon;
export function DashboardActivityItem({ activity, onClick }: DashboardActivityItemProps) { export function DashboardActivityItem({
activity,
onClick,
showConnector = true,
}: DashboardActivityItemProps) {
const Icon = ICON_COMPONENTS[activity.type] ?? FALLBACK_ICON; const Icon = ICON_COMPONENTS[activity.type] ?? FALLBACK_ICON;
const gradient = getActivityIconGradient(activity.type); const colorClasses = ICON_COLORS[activity.type] ?? "text-muted-foreground bg-muted";
const description = formatActivityDescription(activity); const description = formatActivityDescription(activity);
const formattedDate = formatActivityDate(activity.date);
const content = ( const content = (
<> <div className="flex items-start gap-3 relative">
<div className="flex-shrink-0"> {/* Timeline connector */}
<div {showConnector && (
className={`w-10 h-10 rounded-full bg-gradient-to-r ${gradient} flex items-center justify-center shadow-[var(--cp-shadow-1)]`} <div className="absolute left-[15px] top-8 bottom-0 w-px bg-border -z-10" />
> )}
<Icon className="h-5 w-5 text-white" />
</div> {/* Icon */}
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${colorClasses}`}
>
<Icon className="h-4 w-4" />
</div> </div>
<div className="flex-1 min-w-0">
{/* Content */}
<div className="flex-1 min-w-0 pb-4">
<p <p
className={[ className={`text-sm font-medium leading-tight ${
"text-sm font-medium", onClick ? "text-foreground group-hover:text-primary" : "text-foreground"
onClick ? "text-foreground group-hover:text-primary" : "text-foreground", }`}
].join(" ")}
> >
{activity.title} {activity.title}
</p> </p>
<p className="text-sm text-muted-foreground mt-1">{description}</p> <p className="text-sm text-muted-foreground mt-0.5 leading-snug">{description}</p>
<p className="text-xs text-muted-foreground/70 mt-2">{formattedDate}</p>
</div> </div>
</> </div>
); );
if (onClick) { if (onClick) {
return ( return (
<button <button
type="button" type="button"
className="group flex items-start space-x-4 w-full text-left p-3 -m-3 rounded-lg hover:bg-muted transition-colors duration-[var(--cp-duration-normal)] cursor-pointer" className="group w-full text-left rounded-lg hover:bg-muted/50 transition-colors cursor-pointer -mx-2 px-2"
onClick={onClick} onClick={onClick}
> >
{content} {content}
@ -72,5 +84,5 @@ export function DashboardActivityItem({ activity, onClick }: DashboardActivityIt
); );
} }
return <div className="flex items-start space-x-4 w-full text-left">{content}</div>; return <div className="w-full text-left">{content}</div>;
} }

View File

@ -1,18 +0,0 @@
"use client";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
interface PaymentErrorBannerProps {
message: string;
onDismiss: () => void;
}
export function PaymentErrorBanner({ message, onDismiss }: PaymentErrorBannerProps) {
return (
<AlertBanner variant="error" title="Payment Error" onClose={onDismiss} role="alert">
{message}
</AlertBanner>
);
}
export type { PaymentErrorBannerProps };

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
export function QuickAction({ export function QuickAction({
href, href,
@ -20,28 +21,18 @@ export function QuickAction({
return ( return (
<Link <Link
href={href} href={href}
className="flex items-center p-4 rounded-xl hover:bg-muted transition-colors duration-[var(--cp-duration-normal)] group" className="flex items-center p-4 rounded-xl border border-transparent hover:border-border/60 hover:bg-muted/50 transition-all duration-200 group"
> >
<div className={`flex-shrink-0 p-2 rounded-lg ${bgColor}`}> <div className={`flex-shrink-0 p-2.5 rounded-xl ${bgColor} border border-primary/10`}>
<Icon className={`h-5 w-5 ${iconColor}`} /> <Icon className={`h-5 w-5 ${iconColor}`} />
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-4 flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground group-hover:text-primary transition-colors"> <p className="text-sm font-semibold text-foreground group-hover:text-primary transition-colors">
{title} {title}
</p> </p>
<p className="text-xs text-muted-foreground">{description}</p> <p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{description}</p>
</div> </div>
<svg <ArrowRightIcon className="h-4 w-4 text-muted-foreground/50 group-hover:text-primary group-hover:translate-x-0.5 transition-all" />
className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</Link> </Link>
); );
} }

View File

@ -1,54 +1,52 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { ArrowRightIcon } from "@heroicons/react/24/outline";
export function StatCard({ export function StatCard({
title, title,
value, value,
icon: Icon, icon: Icon,
gradient,
href, href,
zeroHint, tone = "neutral",
}: { }: {
title: string; title: string;
value: string | number; value: string | number;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
gradient: string;
href: string; href: string;
zeroHint?: { text: string; href: string }; tone?: "neutral" | "primary" | "info" | "success" | "warning";
}) { }) {
const router = useRouter(); const toneStyles: Record<
NonNullable<Parameters<typeof StatCard>[0]["tone"]>,
{ iconWrap: string; icon: string }
> = {
neutral: { iconWrap: "bg-muted/50", icon: "text-muted-foreground" },
primary: { iconWrap: "bg-primary/10", icon: "text-primary" },
info: { iconWrap: "bg-info/10", icon: "text-info" },
success: { iconWrap: "bg-success/10", icon: "text-success" },
warning: { iconWrap: "bg-warning/10", icon: "text-warning" },
};
const styles = toneStyles[tone];
return ( return (
<Link href={href} className="group"> <Link
<div className="bg-card text-card-foreground rounded-2xl shadow-[var(--cp-card-shadow)] border border-border overflow-hidden hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-[var(--cp-duration-normal)] min-h-[116px] flex"> href={href}
<div className="p-6 flex-1"> className="group flex items-center gap-4 p-4 rounded-xl hover:bg-muted/50 transition-colors"
<div className="flex items-center"> aria-label={`${title}: ${value}`}
<div className={`flex-shrink-0 p-3 rounded-xl bg-gradient-to-r ${gradient}`}> >
<Icon className="h-6 w-6 text-white" /> <div
</div> className={`flex-shrink-0 h-10 w-10 rounded-xl flex items-center justify-center ${styles.iconWrap}`}
<div className="ml-4 flex-1"> aria-hidden="true"
<p className="text-sm font-medium text-muted-foreground uppercase tracking-wide"> >
{title} <Icon className={`h-5 w-5 ${styles.icon}`} />
</p>
<p className="text-2xl font-bold text-foreground mt-1">{value}</p>
{Number(value) === 0 && zeroHint ? (
<span
onClick={e => {
// Prevent card navigation if clicking the hint
e.preventDefault();
e.stopPropagation();
router.push(zeroHint.href);
}}
className="mt-1 inline-flex items-center text-xs font-medium text-primary hover:underline cursor-pointer"
>
{zeroHint.text}
</span>
) : null}
</div>
</div>
</div>
</div> </div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{title}</p>
<p className="text-2xl font-bold text-foreground mt-0.5 tabular-nums">{value}</p>
</div>
<ArrowRightIcon className="h-4 w-4 text-muted-foreground/40 group-hover:text-foreground group-hover:translate-x-0.5 transition-all" />
</Link> </Link>
); );
} }

View File

@ -1,41 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { generateDashboardTasks } from "@/features/dashboard/utils/dashboard.utils";
interface TasksChipProps {
summaryLoading: boolean;
summary: {
nextInvoice?: { id: number } | null;
stats?: { unpaidInvoices?: number; openCases?: number };
};
}
export function TasksChip({ summaryLoading, summary }: TasksChipProps) {
const router = useRouter();
if (summaryLoading) return null;
const tasks = generateDashboardTasks(summary);
const count = tasks.length;
if (count === 0) return null;
return (
<button
onClick={() => {
const first = tasks[0];
if (first.href.startsWith("#")) {
const el = document.querySelector(first.href);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
router.push(first.href);
}
}}
className="inline-flex items-center rounded-full bg-blue-50 text-blue-700 px-2.5 py-1 text-xs font-medium hover:bg-blue-100 transition-colors duration-200"
title={tasks.map(t => t.label).join(" • ")}
>
{count} task{count === 1 ? "" : "s"}
</button>
);
}
export type { TasksChipProps };

View File

@ -1,67 +0,0 @@
"use client";
import Link from "next/link";
import { CalendarDaysIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { format, formatDistanceToNow } from "date-fns";
import { useFormatCurrency } from "@/lib/hooks/useFormatCurrency";
import type { NextInvoice } from "@customer-portal/domain/dashboard";
interface UpcomingPaymentBannerProps {
invoice: NextInvoice;
onPay?: (invoiceId: number) => void;
loading?: boolean;
}
export function UpcomingPaymentBanner({ invoice, onPay, loading }: UpcomingPaymentBannerProps) {
const { formatCurrency } = useFormatCurrency();
return (
<div id="attention" className="bg-white rounded-xl border border-orange-200 shadow-sm p-4">
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-md bg-gradient-to-r from-amber-500 to-orange-500 flex items-center justify-center">
<CalendarDaysIcon className="h-5 w-5 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
<span className="font-semibold text-gray-900">Upcoming Payment</span>
<span className="text-gray-400"></span>
<span>Invoice #{invoice.id}</span>
<span className="text-gray-400"></span>
<span title={format(new Date(invoice.dueDate), "MMMM d, yyyy")}>
Due {formatDistanceToNow(new Date(invoice.dueDate), { addSuffix: true })}
</span>
</div>
<div className="mt-1 text-2xl font-bold text-gray-900">
{formatCurrency(invoice.amount, { currency: invoice.currency })}
</div>
<div className="mt-1 text-xs text-gray-500">
Exact due date: {format(new Date(invoice.dueDate), "MMMM d, yyyy")}
</div>
</div>
<div className="flex flex-col items-end gap-2">
{onPay && (
<button
onClick={() => onPay(invoice.id)}
disabled={loading}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Opening Payment..." : "Pay Now"}
{!loading && <ChevronRightIcon className="ml-2 h-4 w-4" />}
</button>
)}
<Link
href={`/billing/invoices/${invoice.id}`}
className="text-blue-600 hover:text-blue-700 font-medium text-sm"
>
View invoice
</Link>
</div>
</div>
</div>
);
}
export type { UpcomingPaymentBannerProps };

View File

@ -2,3 +2,4 @@ export * from "./StatCard";
export * from "./QuickAction"; export * from "./QuickAction";
export * from "./DashboardActivityItem"; export * from "./DashboardActivityItem";
export * from "./AccountStatusCard"; export * from "./AccountStatusCard";
export * from "./ActivityTimeline";

View File

@ -78,21 +78,6 @@ export function formatActivityDate(date: string): string {
} }
} }
/**
* Get activity icon gradient class
*/
export function getActivityIconGradient(activityType: Activity["type"]): string {
const gradientMap: Record<Activity["type"], string> = {
invoice_created: "from-blue-500 to-cyan-500",
invoice_paid: "from-green-500 to-emerald-500",
service_activated: "from-purple-500 to-pink-500",
case_created: "from-yellow-500 to-orange-500",
case_closed: "from-green-500 to-emerald-500",
};
return gradientMap[activityType] || "from-gray-500 to-slate-500";
}
export function formatActivityDescription(activity: Activity): string { export function formatActivityDescription(activity: Activity): string {
switch (activity.type) { switch (activity.type) {
case "invoice_created": case "invoice_created":

View File

@ -4,25 +4,19 @@ import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Activity, DashboardSummary } from "@customer-portal/domain/dashboard"; import type { Activity, DashboardSummary } from "@customer-portal/domain/dashboard";
import { import { ChevronRightIcon } from "@heroicons/react/24/outline";
ServerIcon,
ChatBubbleLeftRightIcon,
ChevronRightIcon,
DocumentTextIcon,
ArrowTrendingUpIcon,
CalendarDaysIcon,
} from "@heroicons/react/24/outline";
import { import {
CreditCardIcon as CreditCardIconSolid, CreditCardIcon as CreditCardIconSolid,
ServerIcon as ServerIconSolid, ServerIcon as ServerIconSolid,
ChatBubbleLeftRightIcon as ChatBubbleLeftRightIconSolid, ChatBubbleLeftRightIcon as ChatBubbleLeftRightIconSolid,
ClipboardDocumentListIcon as ClipboardDocumentListIconSolid, ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,
Squares2X2Icon as Squares2X2IconSolid,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { format, formatDistanceToNow } from "date-fns"; import { format, formatDistanceToNow } from "date-fns";
import { useAuthStore } from "@/features/auth/services/auth.store"; import { useAuthStore } from "@/features/auth/services/auth.store";
import { useDashboardSummary } from "@/features/dashboard/hooks"; import { useDashboardSummary } from "@/features/dashboard/hooks";
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components"; import { StatCard, ActivityTimeline } from "@/features/dashboard/components";
import { LoadingStats, LoadingTable } from "@/components/atoms"; import { LoadingStats, LoadingTable } from "@/components/atoms";
import { ErrorState } from "@/components/atoms/error-state"; import { ErrorState } from "@/components/atoms/error-state";
import { PageLayout } from "@/components/templates"; import { PageLayout } from "@/components/templates";
@ -71,7 +65,6 @@ export function DashboardView() {
// Handle activity item clicks // Handle activity item clicks
const handleActivityClick = (activity: Activity) => { const handleActivityClick = (activity: Activity) => {
if (activity.type === "invoice_created" || activity.type === "invoice_paid") { if (activity.type === "invoice_created" || activity.type === "invoice_paid") {
// Use the related invoice ID for navigation
if (activity.relatedId) { if (activity.relatedId) {
router.push(`/billing/invoices/${activity.relatedId}`); router.push(`/billing/invoices/${activity.relatedId}`);
} }
@ -115,175 +108,147 @@ export function DashboardView() {
actions={<TasksChip summaryLoading={summaryLoading} summary={summary} />} actions={<TasksChip summaryLoading={summaryLoading} summary={summary} />}
> >
{/* Greeting */} {/* Greeting */}
<div className="flex items-start justify-between gap-4"> <div className="mb-6">
<div className="min-w-0"> <p className="text-sm font-medium text-muted-foreground">Welcome back</p>
<div className="text-sm text-muted-foreground">Welcome back</div> <h2 className="text-2xl sm:text-3xl font-bold text-foreground truncate mt-1">
<div className="text-xl sm:text-2xl font-semibold text-foreground truncate"> {user?.firstname || user?.email?.split("@")[0] || "User"}
{user?.firstname || user?.email?.split("@")[0] || "User"} </h2>
</div> </div>
{/* Overview Stats Card */}
<div className="cp-card rounded-2xl">
<div className="mb-2">
<h3 className="text-sm font-semibold text-foreground">Overview</h3>
<p className="text-xs text-muted-foreground mt-0.5">Quick snapshot of your account</p>
</div> </div>
</div>
{/* Stats */} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 divide-y sm:divide-y-0 sm:divide-x divide-border/60 -mx-4 sm:-mx-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--cp-space-2xl)]"> <StatCard
<StatCard title="Recent Orders"
title="Recent Orders" value={summary?.stats?.recentOrders ?? 0}
value={summary?.stats?.recentOrders ?? 0} icon={ClipboardDocumentListIconSolid}
icon={ClipboardDocumentListIconSolid} tone="primary"
gradient="from-primary to-primary-hover" href="/orders"
href="/orders" />
/> <StatCard
<StatCard title="Pending Invoices"
title="Pending Invoices" value={summary?.stats?.unpaidInvoices ?? 0}
value={summary?.stats?.unpaidInvoices || 0} icon={CreditCardIconSolid}
icon={CreditCardIconSolid} tone={(summary?.stats?.unpaidInvoices ?? 0) > 0 ? "warning" : "neutral"}
gradient={ href="/billing/invoices"
(summary?.stats?.unpaidInvoices ?? 0) > 0 />
? "from-warning to-warning" <StatCard
: "from-muted-foreground to-foreground" title="Active Services"
} value={summary?.stats?.activeSubscriptions ?? 0}
href="/billing/invoices" icon={ServerIconSolid}
zeroHint={{ text: "Set up auto-pay", href: "/billing/payments" }} tone="info"
/> href="/subscriptions"
<StatCard />
title="Active Services" <StatCard
value={summary?.stats?.activeSubscriptions || 0} title="Support Cases"
icon={ServerIconSolid} value={summary?.stats?.openCases ?? 0}
gradient="from-info to-primary" icon={ChatBubbleLeftRightIconSolid}
href="/subscriptions" tone={(summary?.stats?.openCases ?? 0) > 0 ? "info" : "neutral"}
/> href="/support/cases"
<StatCard />
title="Support Cases" <StatCard
value={summary?.stats?.openCases || 0} title="Browse Catalog"
icon={ChatBubbleLeftRightIconSolid} value="→"
gradient={ icon={Squares2X2IconSolid}
(summary?.stats?.openCases ?? 0) > 0 tone="primary"
? "from-info to-primary" href="/catalog"
: "from-muted-foreground to-foreground"
}
href="/support/cases"
zeroHint={{ text: "Open a ticket", href: "/support/new" }}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-[var(--cp-space-3xl)]">
{/* Main Content Area */}
<div className="lg:col-span-2 space-y-[var(--cp-space-3xl)]">
{/* Upcoming Payment */}
{upcomingInvoice && (
<div
id="attention"
className="bg-card border border-warning/35 rounded-xl p-4 shadow-[var(--cp-shadow-1)]"
>
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-md bg-gradient-to-r from-warning to-warning flex items-center justify-center">
<CalendarDaysIcon className="h-5 w-5 text-warning-foreground" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span className="font-semibold text-foreground">Upcoming Payment</span>
<span className="text-muted-foreground/60"></span>
<span>Invoice #{upcomingInvoice.id}</span>
<span className="text-muted-foreground/60"></span>
<span title={format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}>
Due{" "}
{formatDistanceToNow(new Date(upcomingInvoice.dueDate), {
addSuffix: true,
})}
</span>
</div>
<div className="mt-1 text-2xl font-bold text-foreground">
{formatCurrency(upcomingInvoice.amount, {
currency: upcomingInvoice.currency,
})}
</div>
<div className="mt-1 text-xs text-muted-foreground">
Exact due date: {format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Button
size="sm"
onClick={() => handlePayNow(upcomingInvoice.id)}
isLoading={paymentLoading}
loadingText="Opening…"
rightIcon={
!paymentLoading ? <ChevronRightIcon className="h-4 w-4" /> : undefined
}
>
Pay now
</Button>
<Link
href={`/billing/invoices/${upcomingInvoice.id}`}
className="text-primary hover:underline font-medium text-sm"
>
View invoice
</Link>
</div>
</div>
</div>
)}
{/* Payment Error Display */}
{paymentError && (
<ErrorState
title="Payment Error"
message={paymentError}
variant="inline"
onRetry={() => setPaymentError(null)}
retryLabel="Dismiss"
/>
)}
{/* Recent Activity */}
<RecentActivityCard
activities={summary?.recentActivity || []}
onItemClick={handleActivityClick}
/> />
</div> </div>
</div>
{/* Sidebar */} {/* Billing Card - only shown when there's an upcoming invoice */}
<div className="space-y-[var(--cp-space-2xl)]"> {upcomingInvoice && (
<div className="bg-card text-card-foreground rounded-2xl shadow-[var(--cp-card-shadow)] border border-border overflow-hidden"> <div className="cp-card rounded-2xl" id="attention">
<div className="px-6 py-4 border-b border-border"> <div className="flex items-center justify-between gap-4 mb-4">
<h3 className="text-lg font-semibold text-foreground">Quick Actions</h3> <div>
<h3 className="text-sm font-semibold text-foreground">Upcoming Payment</h3>
<p className="text-xs text-muted-foreground mt-0.5">Invoice due soon</p>
</div> </div>
<div className="p-[var(--cp-space-2xl)] space-y-[var(--cp-space-lg)]"> <Link
<QuickAction href="/billing/invoices"
href="/billing/invoices" className="text-sm font-medium text-primary hover:text-primary-hover transition-colors"
title="View invoices" >
description="Review and pay invoices" View all invoices
icon={DocumentTextIcon} </Link>
iconColor="text-primary" </div>
bgColor="bg-primary/10"
/> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/60">
<QuickAction <div className="flex-1 min-w-0">
href="/subscriptions" <div className="flex items-center gap-2">
title="Manage services" <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning">
description="View active subscriptions" Due soon
icon={ServerIcon} </span>
iconColor="text-primary" <span className="text-sm text-muted-foreground">Invoice #{upcomingInvoice.id}</span>
bgColor="bg-primary/10" </div>
/> <div className="mt-2 text-2xl font-bold text-foreground">
<QuickAction {formatCurrency(upcomingInvoice.amount, {
href="/support/new" currency: upcomingInvoice.currency,
title="Get support" })}
description="Open a support ticket" </div>
icon={ChatBubbleLeftRightIcon} <p className="mt-1 text-sm text-muted-foreground">
iconColor="text-primary" Due{" "}
bgColor="bg-primary/10" {formatDistanceToNow(new Date(upcomingInvoice.dueDate), {
/> addSuffix: true,
})}{" "}
· {format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}
</p>
</div>
<div className="flex items-center gap-3">
<Link
href={`/billing/invoices/${upcomingInvoice.id}`}
className="text-sm font-medium text-primary hover:text-primary-hover transition-colors"
>
View details
</Link>
<Button
onClick={() => handlePayNow(upcomingInvoice.id)}
isLoading={paymentLoading}
loadingText="Opening…"
rightIcon={!paymentLoading ? <ChevronRightIcon className="h-4 w-4" /> : undefined}
>
Pay now
</Button>
</div> </div>
</div> </div>
</div> </div>
)}
{/* Payment Error Display */}
{paymentError && (
<ErrorState
title="Payment Error"
message={paymentError}
variant="inline"
onRetry={() => setPaymentError(null)}
retryLabel="Dismiss"
/>
)}
{/* Recent Activity Card */}
<div className="cp-card rounded-2xl">
<div className="flex items-center justify-between gap-4 mb-5">
<div>
<h3 className="text-sm font-semibold text-foreground">Recent Activity</h3>
<p className="text-xs text-muted-foreground mt-0.5">Your latest account updates</p>
</div>
</div>
<ActivityTimeline
activities={summary?.recentActivity || []}
onItemClick={handleActivityClick}
maxItems={8}
/>
</div> </div>
</PageLayout> </PageLayout>
); );
} }
// Helpers and small components (local to dashboard) // Helpers
function TasksChip({ function TasksChip({
summaryLoading, summaryLoading,
summary, summary,
@ -315,76 +280,3 @@ function TasksChip({
</button> </button>
); );
} }
function RecentActivityCard({
activities,
onItemClick,
}: {
activities: Activity[];
onItemClick: (a: Activity) => void;
}) {
const [filter, setFilter] = useState<"all" | "billing" | "orders" | "support">("all");
const filtered = activities.filter(a => {
if (filter === "all") return true;
if (filter === "billing") return a.type === "invoice_created" || a.type === "invoice_paid";
if (filter === "orders") return a.type === "service_activated";
if (filter === "support") return a.type === "case_created" || a.type === "case_closed";
return true;
});
return (
<div className="bg-card text-card-foreground rounded-2xl shadow-[var(--cp-card-shadow)] border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Recent Activity</h3>
<div className="flex items-center space-x-1 bg-muted rounded-lg p-1">
{(
[
{ k: "all", label: "All" },
{ k: "billing", label: "Billing" },
{ k: "orders", label: "Orders" },
{ k: "support", label: "Support" },
] as const
).map(opt => (
<button
key={opt.k}
onClick={() => setFilter(opt.k)}
className={`px-2.5 py-1 text-xs rounded-md font-medium ${
filter === opt.k
? "bg-card text-foreground shadow-[var(--cp-shadow-1)]"
: "text-muted-foreground hover:text-foreground"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
</div>
<div className="p-6 max-h-[360px] overflow-y-auto">
{filtered.length > 0 ? (
<div className="space-y-4">
{filtered.slice(0, 10).map(activity => {
const isClickable =
activity.type === "invoice_created" || activity.type === "invoice_paid";
return (
<DashboardActivityItem
key={activity.id}
activity={activity}
onClick={isClickable ? () => onItemClick(activity) : undefined}
/>
);
})}
</div>
) : (
<div className="text-center py-12">
<ArrowTrendingUpIcon className="mx-auto h-12 w-12 text-muted-foreground/60" />
<h3 className="mt-2 text-sm font-medium text-foreground">No recent activity</h3>
<p className="mt-1 text-sm text-muted-foreground">
Your account activity will appear here.
</p>
</div>
)}
</div>
</div>
);
}

View File

@ -6,44 +6,55 @@ import {
CreditCardIcon, CreditCardIcon,
Cog6ToothIcon, Cog6ToothIcon,
PhoneIcon, PhoneIcon,
ArrowRightIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
export function PublicLandingView() { export function PublicLandingView() {
return ( return (
<div className="space-y-[var(--cp-space-3xl)]"> <div className="space-y-12">
{/* Hero */} {/* Hero */}
<section className="text-center space-y-4"> <section className="text-center space-y-8">
<div className="inline-flex items-center justify-center h-14 w-14 rounded-2xl border border-border bg-card shadow-[var(--cp-shadow-1)] mx-auto"> <div className="relative inline-block">
<Logo size={28} /> {/* Subtle glow behind logo */}
<div className="absolute inset-0 bg-primary/15 blur-3xl rounded-full scale-[2]" />
<div className="relative inline-flex items-center justify-center h-24 w-24 rounded-3xl bg-card border border-border/40 shadow-2xl shadow-primary/20 mx-auto">
<Logo size={56} />
</div>
</div>
<div className="space-y-4">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight text-foreground">
Customer Portal
</h1>
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Manage your services, billing, and support in one place.
</p>
</div> </div>
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Customer Portal</h1>
<p className="text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto">
Manage your services, billing, and support in one place.
</p>
</section> </section>
{/* Primary actions */} {/* Primary actions */}
<section className="grid grid-cols-1 md:grid-cols-2 gap-4"> <section className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<div className="cp-card"> <div className="group relative bg-card rounded-2xl border border-border/50 p-7 shadow-lg shadow-black/5 hover:shadow-xl hover:border-border/80 transition-all duration-300 hover:-translate-y-1">
<div className="flex items-start gap-4"> <div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="h-11 w-11 rounded-xl bg-muted flex items-center justify-center flex-shrink-0"> <div className="relative flex items-start gap-5">
<UserIcon className="h-6 w-6 text-foreground/70" /> <div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 border border-primary/20 flex items-center justify-center flex-shrink-0">
<UserIcon className="h-7 w-7 text-primary" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h2 className="text-lg font-semibold">Existing customers</h2> <h2 className="text-xl font-semibold text-foreground">Existing customers</h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-2 leading-relaxed">
Sign in or migrate your account from the old system. Sign in or migrate your account from the old system.
</p> </p>
<div className="mt-4 flex flex-col sm:flex-row gap-2"> <div className="mt-6 flex flex-col sm:flex-row gap-3">
<Link <Link
href="/auth/login" href="/auth/login"
className="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover transition-colors" className="inline-flex items-center justify-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold bg-primary text-primary-foreground hover:bg-primary-hover shadow-md shadow-primary/25 hover:shadow-lg hover:shadow-primary/30 transition-all"
> >
Sign in Sign in
<ArrowRightIcon className="h-4 w-4" />
</Link> </Link>
<Link <Link
href="/auth/link-whmcs" href="/auth/link-whmcs"
className="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium border border-border bg-card hover:bg-muted transition-colors" className="inline-flex items-center justify-center rounded-xl px-6 py-3 text-sm font-medium border border-border bg-card hover:bg-muted/50 transition-colors"
> >
Migrate account Migrate account
</Link> </Link>
@ -52,22 +63,24 @@ export function PublicLandingView() {
</div> </div>
</div> </div>
<div className="cp-card"> <div className="group relative bg-card rounded-2xl border border-border/50 p-7 shadow-lg shadow-black/5 hover:shadow-xl hover:border-border/80 transition-all duration-300 hover:-translate-y-1">
<div className="flex items-start gap-4"> <div className="absolute inset-0 bg-gradient-to-br from-success/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="h-11 w-11 rounded-xl bg-muted flex items-center justify-center flex-shrink-0"> <div className="relative flex items-start gap-5">
<SparklesIcon className="h-6 w-6 text-foreground/70" /> <div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-success/15 to-success/5 border border-success/20 flex items-center justify-center flex-shrink-0">
<SparklesIcon className="h-7 w-7 text-success" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h2 className="text-lg font-semibold">New customers</h2> <h2 className="text-xl font-semibold text-foreground">New customers</h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-2 leading-relaxed">
Create an account to get started. Create an account to get started with our services.
</p> </p>
<div className="mt-4"> <div className="mt-6">
<Link <Link
href="/auth/signup" href="/auth/signup"
className="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary-hover transition-colors" className="inline-flex items-center justify-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold bg-primary text-primary-foreground hover:bg-primary-hover shadow-md shadow-primary/25 hover:shadow-lg hover:shadow-primary/30 transition-all"
> >
Create account Create account
<ArrowRightIcon className="h-4 w-4" />
</Link> </Link>
</div> </div>
</div> </div>
@ -76,41 +89,50 @@ export function PublicLandingView() {
</section> </section>
{/* Feature highlights */} {/* Feature highlights */}
<section className="cp-card"> <section className="max-w-4xl mx-auto">
<div className="flex items-center justify-between gap-4 flex-wrap mb-6"> <div className="bg-card rounded-2xl border border-border/50 p-8 sm:p-10 shadow-lg shadow-black/5">
<h2 className="text-lg font-semibold">Everything you need</h2> <div className="flex items-center justify-between gap-4 flex-wrap mb-10">
<Link href="/support" className="text-sm font-medium text-primary hover:underline"> <div>
Need help? <h2 className="text-2xl font-bold text-foreground">Everything you need</h2>
</Link> <p className="text-muted-foreground mt-2">Powerful tools to manage your account</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="rounded-xl border border-border bg-card p-5">
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center mb-3">
<CreditCardIcon className="h-5 w-5 text-foreground/70" />
</div>
<div className="font-semibold">Billing</div>
<div className="text-sm text-muted-foreground mt-1">
View invoices, payments, and billing history.
</div> </div>
<Link
href="/support"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-primary-hover transition-colors"
>
Need help?
<ArrowRightIcon className="h-4 w-4" />
</Link>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-primary/30 hover:shadow-md transition-all duration-300">
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-primary/15 to-primary/5 border border-primary/15 flex items-center justify-center mb-5">
<CreditCardIcon className="h-6 w-6 text-primary" />
</div>
<div className="text-lg font-semibold text-foreground">Billing</div>
<div className="text-sm text-muted-foreground mt-2 leading-relaxed">
View invoices, payments, and billing history.
</div>
</div>
<div className="rounded-xl border border-border bg-card p-5"> <div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-info/30 hover:shadow-md transition-all duration-300">
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center mb-3"> <div className="h-12 w-12 rounded-xl bg-gradient-to-br from-info/15 to-info/5 border border-info/15 flex items-center justify-center mb-5">
<Cog6ToothIcon className="h-5 w-5 text-foreground/70" /> <Cog6ToothIcon className="h-6 w-6 text-info" />
</div>
<div className="text-lg font-semibold text-foreground">Services</div>
<div className="text-sm text-muted-foreground mt-2 leading-relaxed">
Manage subscriptions and service details.
</div>
</div> </div>
<div className="font-semibold">Services</div>
<div className="text-sm text-muted-foreground mt-1">
Manage subscriptions and service details.
</div>
</div>
<div className="rounded-xl border border-border bg-card p-5"> <div className="group rounded-2xl border border-border/50 bg-card p-6 hover:border-success/30 hover:shadow-md transition-all duration-300">
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center mb-3"> <div className="h-12 w-12 rounded-xl bg-gradient-to-br from-success/15 to-success/5 border border-success/15 flex items-center justify-center mb-5">
<PhoneIcon className="h-5 w-5 text-foreground/70" /> <PhoneIcon className="h-6 w-6 text-success" />
</div> </div>
<div className="font-semibold">Support</div> <div className="text-lg font-semibold text-foreground">Support</div>
<div className="text-sm text-muted-foreground mt-1"> <div className="text-sm text-muted-foreground mt-2 leading-relaxed">
Create cases and track responses in one place. Create cases and track responses in one place.
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,10 +34,10 @@ const STATUS_PILL_VARIANT = {
} as const; } as const;
const SERVICE_ICON_STYLES = { const SERVICE_ICON_STYLES = {
internet: "bg-blue-50 text-blue-600", internet: "bg-info/10 text-info border border-info/10",
sim: "bg-violet-50 text-violet-600", sim: "bg-primary/10 text-primary border border-primary/10",
vpn: "bg-teal-50 text-teal-600", vpn: "bg-success/10 text-success border border-success/10",
default: "bg-slate-50 text-slate-600", default: "bg-muted text-muted-foreground border border-border",
} as const; } as const;
const renderServiceIcon = (orderType?: string): ReactNode => { const renderServiceIcon = (orderType?: string): ReactNode => {
@ -100,9 +100,9 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
<article <article
key={String(order.id)} key={String(order.id)}
className={cn( className={cn(
"group overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition-all focus-visible:outline-none", "group overflow-hidden rounded-2xl border border-border bg-card shadow-[var(--cp-shadow-1)] transition-all duration-200 focus-visible:outline-none",
isInteractive && isInteractive &&
"cursor-pointer hover:border-blue-200 hover:shadow-md focus-within:border-blue-300 focus-within:ring-2 focus-within:ring-blue-100", "cursor-pointer hover:border-primary/30 hover:shadow-lg hover:-translate-y-0.5 focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-primary/10",
className className
)} )}
onClick={onClick} onClick={onClick}
@ -110,13 +110,13 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
role={isInteractive ? "button" : undefined} role={isInteractive ? "button" : undefined}
tabIndex={isInteractive ? 0 : undefined} tabIndex={isInteractive ? 0 : undefined}
> >
<div className="px-6 py-4"> <div className="px-5 sm:px-6 py-5">
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-4 sm:gap-6">
{/* Left section: Icon + Service info + Status */} {/* Left section: Icon + Service info + Status */}
<div className="flex items-start gap-3 flex-1 min-w-0"> <div className="flex items-start gap-3 sm:gap-4 flex-1 min-w-0">
<div <div
className={cn( className={cn(
"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg", "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl",
iconStyles iconStyles
)} )}
> >
@ -124,14 +124,14 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="font-semibold text-gray-900">{serviceName}</h3> <h3 className="font-semibold text-foreground">{serviceName}</h3>
<StatusPill label={statusDescriptor.label} variant={statusVariant} /> <StatusPill label={statusDescriptor.label} variant={statusVariant} />
</div> </div>
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500"> <div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium"> <span className="font-medium">
#{order.orderNumber || String(order.id).slice(-8)} #{order.orderNumber || String(order.id).slice(-8)}
</span> </span>
<span></span> <span className="text-muted-foreground/40"></span>
<span>{formattedCreatedDate || "—"}</span> <span>{formattedCreatedDate || "—"}</span>
</div> </div>
{displayItems.length > 0 && ( {displayItems.length > 0 && (
@ -139,7 +139,7 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
{displayItems.map(item => ( {displayItems.map(item => (
<span <span
key={item.id} key={item.id}
className="inline-flex items-center rounded-md bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700" className="inline-flex items-center rounded-lg bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground"
> >
{item.name} {item.name}
</span> </span>
@ -151,23 +151,23 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
{/* Right section: Pricing */} {/* Right section: Pricing */}
{showPricing && ( {showPricing && (
<div className="flex items-start gap-4 flex-shrink-0"> <div className="flex items-start gap-4 sm:gap-5 flex-shrink-0">
{totals.monthlyTotal > 0 && ( {totals.monthlyTotal > 0 && (
<div className="text-right"> <div className="text-right">
<p className="text-[10px] font-medium uppercase tracking-wider text-blue-600"> <p className="text-[10px] font-semibold uppercase tracking-wider text-primary">
Monthly Monthly
</p> </p>
<p className="text-xl font-bold text-gray-900"> <p className="text-xl font-bold text-foreground tabular-nums">
¥{totals.monthlyTotal.toLocaleString()} ¥{totals.monthlyTotal.toLocaleString()}
</p> </p>
</div> </div>
)} )}
{totals.oneTimeTotal > 0 && ( {totals.oneTimeTotal > 0 && (
<div className="text-right"> <div className="text-right">
<p className="text-[10px] font-medium uppercase tracking-wider text-blue-600"> <p className="text-[10px] font-semibold uppercase tracking-wider text-primary">
One-Time One-Time
</p> </p>
<p className="text-lg font-bold text-gray-900"> <p className="text-lg font-bold text-foreground tabular-nums">
¥{totals.oneTimeTotal.toLocaleString()} ¥{totals.oneTimeTotal.toLocaleString()}
</p> </p>
</div> </div>
@ -176,7 +176,9 @@ export function OrderCard({ order, onClick, footer, className }: OrderCardProps)
)} )}
</div> </div>
</div> </div>
{footer && <div className="border-t border-slate-100 bg-slate-50 px-6 py-3">{footer}</div>} {footer && (
<div className="border-t border-border bg-muted/30 px-5 sm:px-6 py-3">{footer}</div>
)}
</article> </article>
); );
} }

View File

@ -17,10 +17,8 @@ import {
ClockIcon, ClockIcon,
Squares2X2Icon, Squares2X2Icon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
ArrowLeftIcon,
} 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 { Button } from "@/components/atoms/button";
import { ordersService } from "@/features/orders/services/orders.service"; import { ordersService } from "@/features/orders/services/orders.service";
import { useOrderUpdates } from "@/features/orders/hooks/useOrderUpdates"; import { useOrderUpdates } from "@/features/orders/hooks/useOrderUpdates";
import { import {
@ -97,23 +95,23 @@ const ITEM_VISUAL_STYLES: Record<
} }
> = { > = {
service: { service: {
container: "border-info/30 bg-card", container: "bg-card",
icon: "bg-info-soft text-info", icon: "bg-info-soft text-info",
}, },
installation: { installation: {
container: "border-success/25 bg-card", container: "bg-card",
icon: "bg-success-soft text-success", icon: "bg-success-soft text-success",
}, },
addon: { addon: {
container: "border-border bg-card", container: "bg-card",
icon: "bg-muted text-foreground/70", icon: "bg-muted text-foreground/70",
}, },
activation: { activation: {
container: "border-success/25 bg-card", container: "bg-card",
icon: "bg-success-soft text-success", icon: "bg-success-soft text-success",
}, },
other: { other: {
container: "border-border bg-card", container: "bg-card",
icon: "bg-muted text-muted-foreground", icon: "bg-muted text-muted-foreground",
}, },
}; };
@ -269,29 +267,18 @@ export function OrderDetailContainer() {
useOrderUpdates(params.id, handleOrderUpdate); useOrderUpdates(params.id, handleOrderUpdate);
const orderNumber = data?.orderNumber || (data ? String(data.id).slice(-8) : "");
return ( return (
<PageLayout <PageLayout
icon={<ClipboardDocumentCheckIcon />} icon={<ClipboardDocumentCheckIcon />}
title={data ? `${data.orderType} Service Order` : "Order Details"} title={data ? `${data.orderType} Service Order` : "Order Details"}
description={ description={data ? `Order #${orderNumber}` : "Loading order details..."}
data breadcrumbs={[
? `Order #${data.orderNumber || String(data.id).slice(-8)}` { label: "Orders", href: "/orders" },
: "Loading order details..." { label: data ? `Order #${orderNumber}` : "Order Details" },
} ]}
> >
<div className="mb-6">
<Button
as="a"
href="/orders"
size="sm"
variant="ghost"
leftIcon={<ArrowLeftIcon className="h-4 w-4" />}
className="text-muted-foreground hover:text-foreground"
>
Back to orders
</Button>
</div>
{error && <div className="mb-4 text-sm text-destructive">{error}</div>} {error && <div className="mb-4 text-sm text-destructive">{error}</div>}
{isNewOrder && ( {isNewOrder && (
@ -382,26 +369,27 @@ export function OrderDetailContainer() {
No items found on this order. No items found on this order.
</div> </div>
) : ( ) : (
displayItems.map((item, itemIndex) => { <div className="rounded-xl border border-border overflow-hidden divide-y divide-border">
const categoryConfig = {displayItems.map(item => {
CATEGORY_CONFIG[item.primaryCategory] ?? CATEGORY_CONFIG.other; const categoryConfig =
const Icon = categoryConfig.icon; CATEGORY_CONFIG[item.primaryCategory] ?? CATEGORY_CONFIG.other;
const style = getItemVisualStyle(item); const Icon = categoryConfig.icon;
const style = getItemVisualStyle(item);
return ( return (
<div key={item.id}>
<div <div
key={item.id}
className={cn( className={cn(
"flex flex-col gap-3 rounded-xl border px-4 py-4 sm:flex-row sm:items-center sm:justify-between", "flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between",
style.container, style.container,
itemIndex > 0 && "border-t-0 rounded-t-none" "border-0 rounded-none"
)} )}
> >
{/* Icon + Title & Category | Price */} {/* Icon + Title & Category | Price */}
<div className="flex flex-1 items-start gap-3"> <div className="flex flex-1 items-start gap-3">
<div <div
className={cn( className={cn(
"flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg", "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
style.icon style.icon
)} )}
> >
@ -449,9 +437,9 @@ export function OrderDetailContainer() {
</div> </div>
</div> </div>
</div> </div>
</div> );
); })}
}) </div>
)} )}
</div> </div>
</div> </div>
@ -472,14 +460,14 @@ export function OrderDetailContainer() {
)} )}
{showFeeNotice && ( {showFeeNotice && (
<div className="border-l-4 border-amber-500 bg-amber-50 px-4 py-3"> <div className="border-l-4 border-warning bg-warning-soft px-4 py-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-amber-600" /> <ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-warning" />
<div> <div>
<p className="text-sm font-semibold text-amber-900"> <p className="text-sm font-semibold text-foreground">
Installation Fee Notice Installation Fee Notice
</p> </p>
<p className="mt-1 text-sm text-amber-800"> <p className="mt-1 text-sm text-muted-foreground">
Standard installation is included. Additional charges may apply for Standard installation is included. Additional charges may apply for
weekend scheduling, express service, or specialized equipment weekend scheduling, express service, or specialized equipment
installation. Any extra fees will be discussed and approved by you before installation. Any extra fees will be discussed and approved by you before

View File

@ -45,17 +45,25 @@ export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps
if (variant === "grid") { if (variant === "grid") {
return ( return (
<SubCard ref={ref} className={cn("hover:shadow-lg transition-all duration-200", className)}> <SubCard
ref={ref}
className={cn(
"hover:shadow-lg hover:-translate-y-0.5 transition-all duration-200",
className
)}
>
<div className="space-y-4"> <div className="space-y-4">
{/* Header */} {/* Header */}
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{getSubscriptionStatusIcon(subscription.status)} {getSubscriptionStatusIcon(subscription.status)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900 truncate"> <h3 className="text-lg font-semibold text-foreground truncate">
{subscription.productName} {subscription.productName}
</h3> </h3>
<p className="text-sm text-gray-500">Service ID: {subscription.serviceId}</p> <p className="text-sm text-muted-foreground">
Service ID: {subscription.serviceId}
</p>
</div> </div>
</div> </div>
<StatusPill <StatusPill
@ -68,23 +76,31 @@ export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps
{/* Details */} {/* Details */}
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<p className="text-gray-500">Price</p> <p className="text-muted-foreground text-xs font-medium uppercase tracking-wider">
<p className="font-semibold text-gray-900">{formatCurrency(subscription.amount)}</p> Price
<p className="text-xs text-gray-500">{getBillingCycleLabel(subscription.cycle)}</p> </p>
<p className="font-semibold text-foreground mt-1">
{formatCurrency(subscription.amount)}
</p>
<p className="text-xs text-muted-foreground">
{getBillingCycleLabel(subscription.cycle)}
</p>
</div> </div>
<div> <div>
<p className="text-gray-500">Next Due</p> <p className="text-muted-foreground text-xs font-medium uppercase tracking-wider">
<div className="flex items-center space-x-1"> Next Due
<CalendarIcon className="h-4 w-4 text-gray-400" /> </p>
<p className="font-medium text-gray-900">{formatDate(subscription.nextDue)}</p> <div className="flex items-center space-x-1 mt-1">
<CalendarIcon className="h-4 w-4 text-muted-foreground/60" />
<p className="font-medium text-foreground">{formatDate(subscription.nextDue)}</p>
</div> </div>
</div> </div>
</div> </div>
{/* Actions */} {/* Actions */}
{showActions && ( {showActions && (
<div className="flex items-center justify-between pt-2 border-t border-gray-100"> <div className="flex items-center justify-between pt-3 border-t border-border/60">
<p className="text-xs text-gray-500"> <p className="text-xs text-muted-foreground">
Created {formatDate(subscription.registrationDate)} Created {formatDate(subscription.registrationDate)}
</p> </p>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -106,13 +122,19 @@ export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps
// List variant (default) // List variant (default)
return ( return (
<SubCard ref={ref} className={cn("hover:shadow-md transition-all duration-200", className)}> <SubCard
ref={ref}
className={cn(
"hover:shadow-lg hover:-translate-y-0.5 transition-all duration-200",
className
)}
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4 min-w-0 flex-1"> <div className="flex items-center space-x-4 min-w-0 flex-1">
{getSubscriptionStatusIcon(subscription.status)} {getSubscriptionStatusIcon(subscription.status)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<h3 className="text-base font-semibold text-gray-900 truncate"> <h3 className="text-base font-semibold text-foreground truncate">
{subscription.productName} {subscription.productName}
</h3> </h3>
<StatusPill <StatusPill
@ -121,22 +143,28 @@ export const SubscriptionCard = forwardRef<HTMLDivElement, SubscriptionCardProps
size="sm" size="sm"
/> />
</div> </div>
<p className="text-sm text-gray-500 mt-1">Service ID: {subscription.serviceId}</p> <p className="text-sm text-muted-foreground mt-1">
Service ID: {subscription.serviceId}
</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-6 text-sm"> <div className="flex items-center space-x-6 text-sm">
<div className="text-right"> <div className="text-right">
<p className="font-semibold text-gray-900">{formatCurrency(subscription.amount)}</p> <p className="font-semibold text-foreground tabular-nums">
<p className="text-gray-500">{getBillingCycleLabel(subscription.cycle)}</p> {formatCurrency(subscription.amount)}
</p>
<p className="text-muted-foreground text-xs">
{getBillingCycleLabel(subscription.cycle)}
</p>
</div> </div>
<div className="text-right"> <div className="text-right hidden sm:block">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<CalendarIcon className="h-4 w-4 text-gray-400" /> <CalendarIcon className="h-4 w-4 text-muted-foreground/60" />
<p className="text-gray-900">{formatDate(subscription.nextDue)}</p> <p className="text-foreground">{formatDate(subscription.nextDue)}</p>
</div> </div>
<p className="text-gray-500">Next due</p> <p className="text-muted-foreground text-xs">Next due</p>
</div> </div>
{showActions && ( {showActions && (

View File

@ -32,13 +32,13 @@ interface SubscriptionTableProps {
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
case SUBSCRIPTION_STATUS.ACTIVE: case SUBSCRIPTION_STATUS.ACTIVE:
return <CheckCircleIcon className="h-5 w-5 text-green-500" />; return <CheckCircleIcon className="h-5 w-5 text-success" />;
case SUBSCRIPTION_STATUS.COMPLETED: case SUBSCRIPTION_STATUS.COMPLETED:
return <CheckCircleIcon className="h-5 w-5 text-blue-500" />; return <CheckCircleIcon className="h-5 w-5 text-primary" />;
case SUBSCRIPTION_STATUS.CANCELLED: case SUBSCRIPTION_STATUS.CANCELLED:
return <XCircleIcon className="h-5 w-5 text-gray-500" />; return <XCircleIcon className="h-5 w-5 text-muted-foreground" />;
default: default:
return <ClockIcon className="h-5 w-5 text-gray-500" />; return <ClockIcon className="h-5 w-5 text-muted-foreground" />;
} }
}; };
@ -116,11 +116,11 @@ export function SubscriptionTable({
render: (subscription: Subscription) => { render: (subscription: Subscription) => {
const statusIcon = getStatusIcon(subscription.status); const statusIcon = getStatusIcon(subscription.status);
return ( return (
<div className="flex items-center space-x-3 py-4"> <div className="flex items-center space-x-3 py-5">
<div className="flex-shrink-0">{statusIcon}</div> <div className="flex-shrink-0">{statusIcon}</div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="font-semibold text-gray-900 text-sm"> <div className="font-semibold text-foreground text-sm">
{subscription.productName} {subscription.productName}
</div> </div>
<StatusPill <StatusPill
@ -139,10 +139,10 @@ export function SubscriptionTable({
header: "Amount", header: "Amount",
className: "", className: "",
render: (subscription: Subscription) => ( render: (subscription: Subscription) => (
<div className="py-4 text-right"> <div className="py-5 text-right">
<div className="font-semibold text-gray-900 text-sm"> <div className="font-bold text-foreground text-sm tabular-nums">
{formatCurrency(subscription.amount, subscription.currency)}{" "} {formatCurrency(subscription.amount, subscription.currency)}{" "}
<span className="text-xs text-gray-500 font-normal"> <span className="text-xs text-muted-foreground font-normal">
{getBillingPeriodText(subscription.cycle)} {getBillingPeriodText(subscription.cycle)}
</span> </span>
</div> </div>
@ -154,10 +154,10 @@ export function SubscriptionTable({
header: "Next Due", header: "Next Due",
className: "", className: "",
render: (subscription: Subscription) => ( render: (subscription: Subscription) => (
<div className="py-4"> <div className="py-5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4 text-gray-400" /> <CalendarIcon className="h-4 w-4 text-muted-foreground" />
<div className="text-sm font-medium text-gray-700"> <div className="text-sm font-medium text-foreground">
{formatDate(subscription.nextDue)} {formatDate(subscription.nextDue)}
</div> </div>
</div> </div>
@ -175,33 +175,33 @@ export function SubscriptionTable({
if (loading) { if (loading) {
return ( return (
<div className={cn("bg-white overflow-hidden", className)}> <div className={cn("bg-card overflow-hidden", className)}>
<div className="animate-pulse"> <div className="animate-pulse">
{/* Header skeleton */} {/* Header skeleton */}
<div className="bg-gradient-to-r from-gray-50 to-gray-50/80 px-6 py-4 border-b border-gray-200/80"> <div className="bg-muted/50 px-6 py-4 border-b border-border">
<div className="grid grid-cols-3 gap-6"> <div className="grid grid-cols-3 gap-6">
<div className="h-3 bg-gray-200 rounded w-24"></div> <div className="h-3 bg-muted rounded w-24"></div>
<div className="h-3 bg-gray-200 rounded w-20 ml-auto"></div> <div className="h-3 bg-muted rounded w-20 ml-auto"></div>
<div className="h-3 bg-gray-200 rounded w-24"></div> <div className="h-3 bg-muted rounded w-24"></div>
</div> </div>
</div> </div>
{/* Row skeletons */} {/* Row skeletons */}
<div className="divide-y divide-gray-100/60"> <div className="divide-y divide-border">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="px-6 py-5"> <div key={i} className="px-6 py-5">
<div className="grid grid-cols-3 gap-6 items-center"> <div className="grid grid-cols-3 gap-6 items-center">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="h-5 w-5 bg-gray-200 rounded-full flex-shrink-0"></div> <div className="h-5 w-5 bg-muted rounded-full flex-shrink-0"></div>
<div className="space-y-2 flex-1"> <div className="space-y-2 flex-1">
<div className="h-4 bg-gray-200 rounded w-48"></div> <div className="h-4 bg-muted rounded w-48"></div>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="h-4 bg-gray-200 rounded w-32 ml-auto"></div> <div className="h-4 bg-muted rounded w-32 ml-auto"></div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-4 bg-gray-200 rounded"></div> <div className="h-4 w-4 bg-muted rounded"></div>
<div className="h-4 bg-gray-200 rounded w-28"></div> <div className="h-4 bg-muted rounded w-28"></div>
</div> </div>
</div> </div>
</div> </div>
@ -213,7 +213,7 @@ export function SubscriptionTable({
} }
return ( return (
<div className={cn("bg-white overflow-hidden", className)}> <div className={cn("bg-card overflow-hidden", className)}>
<DataTable <DataTable
data={subscriptions} data={subscriptions}
columns={columns} columns={columns}
@ -222,14 +222,14 @@ export function SubscriptionTable({
className={cn( className={cn(
"subscription-table", "subscription-table",
// Header styling - cleaner and more modern // Header styling - cleaner and more modern
"[&_thead]:bg-gradient-to-r [&_thead]:from-gray-50 [&_thead]:to-gray-50/80", "[&_thead]:bg-muted/50",
"[&_thead_th]:px-6 [&_thead_th]:py-3.5 [&_thead_th]:text-xs [&_thead_th]:font-medium [&_thead_th]:text-gray-600 [&_thead_th]:uppercase [&_thead_th]:tracking-wide", "[&_thead_th]:px-6 [&_thead_th]:py-3.5 [&_thead_th]:text-xs [&_thead_th]:font-medium [&_thead_th]:text-muted-foreground [&_thead_th]:uppercase [&_thead_th]:tracking-wide",
"[&_thead_th]:border-b [&_thead_th]:border-gray-200/80", "[&_thead_th]:border-b [&_thead_th]:border-border",
// Right-align Amount column header (2nd column) // Right-align Amount column header (2nd column)
"[&_thead_th:nth-child(2)]:text-right", "[&_thead_th:nth-child(2)]:text-right",
// Row styling - enhanced hover and spacing // Row styling - enhanced hover and spacing
"[&_tbody_tr]:border-b [&_tbody_tr]:border-gray-100/60 [&_tbody_tr]:transition-all [&_tbody_tr]:duration-200", "[&_tbody_tr]:border-b [&_tbody_tr]:border-border [&_tbody_tr]:transition-all [&_tbody_tr]:duration-200",
"[&_tbody_tr:hover]:bg-gradient-to-r [&_tbody_tr:hover]:from-blue-50/30 [&_tbody_tr:hover]:to-indigo-50/20 [&_tbody_tr]:cursor-pointer", "[&_tbody_tr:hover]:bg-primary/5 [&_tbody_tr]:cursor-pointer",
"[&_tbody_tr:last-child]:border-b-0", "[&_tbody_tr:last-child]:border-b-0",
// Cell styling - better spacing // Cell styling - better spacing
"[&_tbody_td]:px-6 [&_tbody_td]:py-1 [&_tbody_td]:align-top", "[&_tbody_td]:px-6 [&_tbody_td]:py-1 [&_tbody_td]:align-top",

View File

@ -16,13 +16,13 @@ import {
export type SubscriptionStatusVariant = "success" | "info" | "warning" | "neutral" | "error"; export type SubscriptionStatusVariant = "success" | "info" | "warning" | "neutral" | "error";
const STATUS_ICON_MAP: Record<SubscriptionStatus, ReactNode> = { const STATUS_ICON_MAP: Record<SubscriptionStatus, ReactNode> = {
[SUBSCRIPTION_STATUS.ACTIVE]: <CheckCircleIcon className="h-6 w-6 text-green-500" />, [SUBSCRIPTION_STATUS.ACTIVE]: <CheckCircleIcon className="h-6 w-6 text-success" />,
[SUBSCRIPTION_STATUS.INACTIVE]: <ServerIcon className="h-6 w-6 text-muted-foreground" />, [SUBSCRIPTION_STATUS.INACTIVE]: <ServerIcon className="h-6 w-6 text-muted-foreground" />,
[SUBSCRIPTION_STATUS.PENDING]: <ClockIcon className="h-6 w-6 text-blue-500" />, [SUBSCRIPTION_STATUS.PENDING]: <ClockIcon className="h-6 w-6 text-info" />,
[SUBSCRIPTION_STATUS.SUSPENDED]: <ExclamationTriangleIcon className="h-6 w-6 text-yellow-500" />, [SUBSCRIPTION_STATUS.SUSPENDED]: <ExclamationTriangleIcon className="h-6 w-6 text-warning" />,
[SUBSCRIPTION_STATUS.TERMINATED]: <XCircleIcon className="h-6 w-6 text-red-500" />, [SUBSCRIPTION_STATUS.TERMINATED]: <XCircleIcon className="h-6 w-6 text-destructive" />,
[SUBSCRIPTION_STATUS.CANCELLED]: <XCircleIcon className="h-6 w-6 text-muted-foreground" />, [SUBSCRIPTION_STATUS.CANCELLED]: <XCircleIcon className="h-6 w-6 text-muted-foreground" />,
[SUBSCRIPTION_STATUS.COMPLETED]: <CheckCircleIcon className="h-6 w-6 text-green-500" />, [SUBSCRIPTION_STATUS.COMPLETED]: <CheckCircleIcon className="h-6 w-6 text-success" />,
}; };
const STATUS_VARIANT_MAP: Record<SubscriptionStatus, SubscriptionStatusVariant> = { const STATUS_VARIANT_MAP: Record<SubscriptionStatus, SubscriptionStatusVariant> = {

View File

@ -1,7 +1,5 @@
"use client"; "use client";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { DetailHeader } from "@/components/molecules/DetailHeader/DetailHeader";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation"; import { useParams, useSearchParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
@ -11,41 +9,35 @@ import { useSubscription } from "@/features/subscriptions/hooks";
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList"; import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
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 { StatusPill } from "@/components/atoms/status-pill";
const { formatCurrency: sharedFormatCurrency } = Formatting; const { formatCurrency: sharedFormatCurrency } = Formatting;
import { SimManagementSection } from "@/features/sim-management"; import { SimManagementSection } from "@/features/sim-management";
import { import {
getBillingCycleLabel, getBillingCycleLabel,
getSubscriptionStatusIcon,
getSubscriptionStatusVariant, getSubscriptionStatusVariant,
} from "@/features/subscriptions/utils/status-presenters"; } from "@/features/subscriptions/utils/status-presenters";
import { cn } from "@/lib/utils";
export function SubscriptionDetailContainer() { export function SubscriptionDetailContainer() {
const params = useParams(); const params = useParams();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [showInvoices, setShowInvoices] = useState(true); const [activeTab, setActiveTab] = useState<"overview" | "sim">("overview");
const [showSimManagement, setShowSimManagement] = useState(false);
const subscriptionId = parseInt(params.id as string); const subscriptionId = parseInt(params.id as string);
const { data: subscription, isLoading, error } = useSubscription(subscriptionId); const { data: subscription, isLoading, error } = useSubscription(subscriptionId);
useEffect(() => { useEffect(() => {
const updateVisibility = () => { const updateTab = () => {
const hash = typeof window !== "undefined" ? window.location.hash : ""; const hash = typeof window !== "undefined" ? window.location.hash : "";
const service = (searchParams.get("service") || "").toLowerCase(); const service = (searchParams.get("service") || "").toLowerCase();
const isSimContext = hash.includes("sim-management") || service === "sim"; const isSimContext = hash.includes("sim-management") || service === "sim";
if (isSimContext) { setActiveTab(isSimContext ? "sim" : "overview");
setShowInvoices(false);
setShowSimManagement(true);
} else {
setShowInvoices(true);
setShowSimManagement(false);
}
}; };
updateVisibility(); updateTab();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.addEventListener("hashchange", updateVisibility); window.addEventListener("hashchange", updateTab);
return () => window.removeEventListener("hashchange", updateVisibility); return () => window.removeEventListener("hashchange", updateTab);
} }
return; return;
}, [searchParams]); }, [searchParams]);
@ -87,104 +79,114 @@ export function SubscriptionDetailContainer() {
error={pageError} error={pageError}
> >
{subscription ? ( {subscription ? (
<div className="max-w-7xl mx-auto"> <div className="space-y-6">
<SubCard className="mb-6"> {/* Main Subscription Card */}
<DetailHeader <div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden">
title="Subscription Details" {/* Header with status */}
subtitle="Service subscription information" <div className="px-6 py-5 border-b border-border">
leftIcon={getSubscriptionStatusIcon(subscription.status)} <div className="flex items-center justify-between">
status={{ <div className="flex items-center gap-3">
label: subscription.status, <div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center">
variant: getSubscriptionStatusVariant(subscription.status), <ServerIcon className="h-5 w-5 text-primary" />
}}
/>
<div className="pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
Billing Amount
</h4>
<div className="mt-2 flex items-baseline gap-2">
<p className="text-2xl font-bold text-foreground">
{formatCurrency(subscription.amount)}
</p>
<span className="text-sm text-muted-foreground">
{getBillingCycleLabel(subscription.cycle)}
</span>
</div> </div>
</div> <div>
<div> <h2 className="text-lg font-semibold text-foreground">Subscription Details</h2>
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider"> <p className="text-sm text-muted-foreground">
Next Due Date Service subscription information
</h4>
<div className="flex items-center mt-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground/70 mr-2" />
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.nextDue)}
</p> </p>
</div> </div>
</div> </div>
<div> <StatusPill
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wider"> label={subscription.status}
Registration Date variant={getSubscriptionStatusVariant(subscription.status)}
</h4> />
<div className="flex items-center mt-2"> </div>
<CalendarIcon className="h-4 w-4 text-muted-foreground/70 mr-2" /> </div>
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.registrationDate)} {/* Stats Row */}
</p> <div className="px-6 py-5 grid grid-cols-1 md:grid-cols-3 gap-6 bg-muted/20">
</div> <div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Billing Amount
</h4>
<div className="mt-2 flex items-baseline gap-2">
<p className="text-2xl font-bold text-foreground">
{formatCurrency(subscription.amount)}
</p>
<span className="text-sm text-muted-foreground">
{getBillingCycleLabel(subscription.cycle)}
</span>
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Next Due Date
</h4>
<div className="flex items-center mt-2 gap-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.nextDue)}
</p>
</div>
</div>
<div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Registration Date
</h4>
<div className="flex items-center mt-2 gap-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<p className="text-lg font-medium text-foreground">
{formatDate(subscription.registrationDate)}
</p>
</div> </div>
</div> </div>
</div> </div>
</SubCard> </div>
{/* Tab Navigation for SIM Services */}
{isSimService && ( {isSimService && (
<div className="mb-8"> <div className="flex items-center gap-1 p-1 bg-muted rounded-lg w-fit">
<SubCard> <Link
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4"> href={`/subscriptions/${subscriptionId}`}
<div> className={cn(
<h3 className="text-xl font-semibold text-foreground">Service Management</h3> "px-4 py-2 text-sm font-medium rounded-md transition-all flex items-center gap-2",
<p className="text-sm text-muted-foreground mt-1"> activeTab === "overview"
Switch between billing and SIM management views ? "bg-card text-foreground shadow-sm"
</p> : "text-muted-foreground hover:text-foreground"
</div> )}
<div className="flex flex-col sm:flex-row gap-2 bg-muted rounded-xl p-2 border border-border/60"> >
<Link <DocumentTextIcon className="h-4 w-4" />
href={`/subscriptions/${subscriptionId}#sim-management`} Overview & Billing
className={`px-6 py-3 text-sm font-semibold rounded-lg transition-colors min-w-[140px] text-center ${ </Link>
showSimManagement <Link
? "bg-card text-primary shadow-[var(--cp-shadow-1)]" href={`/subscriptions/${subscriptionId}#sim-management`}
: "text-muted-foreground hover:text-foreground hover:bg-card/60" className={cn(
}`} "px-4 py-2 text-sm font-medium rounded-md transition-all flex items-center gap-2",
> activeTab === "sim"
<ServerIcon className="h-4 w-4 inline mr-2" /> ? "bg-card text-foreground shadow-sm"
SIM Management : "text-muted-foreground hover:text-foreground"
</Link> )}
<Link >
href={`/subscriptions/${subscriptionId}`} <ServerIcon className="h-4 w-4" />
className={`px-6 py-3 text-sm font-semibold rounded-lg transition-colors min-w-[120px] text-center ${ SIM Management
showInvoices </Link>
? "bg-card text-primary shadow-[var(--cp-shadow-1)]"
: "text-muted-foreground hover:text-foreground hover:bg-card/60"
}`}
>
<DocumentTextIcon className="h-4 w-4 inline mr-2" />
Invoices
</Link>
</div>
</div>
</SubCard>
</div> </div>
)} )}
{showSimManagement && ( {/* SIM Management Section */}
<div className="mb-10"> {activeTab === "sim" && isSimService && (
<SimManagementSection subscriptionId={subscriptionId} /> <SimManagementSection subscriptionId={subscriptionId} />
</div>
)} )}
{showInvoices && <InvoicesList subscriptionId={subscriptionId} pageSize={5} />} {/* Billing History Section */}
{activeTab === "overview" && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Billing History</h3>
</div>
<InvoicesList subscriptionId={subscriptionId} pageSize={5} showFilters={false} />
</div>
)}
</div> </div>
) : null} ) : null}
</PageLayout> </PageLayout>

View File

@ -101,56 +101,46 @@ export function SubscriptionsListContainer() {
> >
<ErrorBoundary> <ErrorBoundary>
{stats && ( {stats && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<SubCard> <div className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]">
<div className="flex items-center"> <div className="flex items-center gap-4">
<div className="flex-shrink-0"> <div className="h-10 w-10 rounded-lg bg-success/10 flex items-center justify-center">
<CheckCircleIcon className="h-8 w-8 text-success" /> <CheckCircleIcon className="h-5 w-5 text-success" />
</div> </div>
<div className="ml-5 w-0 flex-1"> <div>
<dl> <p className="text-sm font-medium text-muted-foreground">Active</p>
<dt className="text-sm font-medium text-muted-foreground truncate">Active</dt> <p className="text-2xl font-bold text-foreground">{stats.active}</p>
<dd className="text-lg font-medium text-foreground">{stats.active}</dd>
</dl>
</div> </div>
</div> </div>
</SubCard> </div>
<SubCard> <div className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]">
<div className="flex items-center"> <div className="flex items-center gap-4">
<div className="flex-shrink-0"> <div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<CheckCircleIcon className="h-8 w-8 text-primary" /> <CheckCircleIcon className="h-5 w-5 text-primary" />
</div> </div>
<div className="ml-5 w-0 flex-1"> <div>
<dl> <p className="text-sm font-medium text-muted-foreground">Completed</p>
<dt className="text-sm font-medium text-muted-foreground truncate"> <p className="text-2xl font-bold text-foreground">{stats.completed}</p>
Completed
</dt>
<dd className="text-lg font-medium text-foreground">{stats.completed}</dd>
</dl>
</div> </div>
</div> </div>
</SubCard> </div>
<SubCard> <div className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]">
<div className="flex items-center"> <div className="flex items-center gap-4">
<div className="flex-shrink-0"> <div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center">
<XCircleIcon className="h-8 w-8 text-muted-foreground" /> <XCircleIcon className="h-5 w-5 text-muted-foreground" />
</div> </div>
<div className="ml-5 w-0 flex-1"> <div>
<dl> <p className="text-sm font-medium text-muted-foreground">Cancelled</p>
<dt className="text-sm font-medium text-muted-foreground truncate"> <p className="text-2xl font-bold text-foreground">{stats.cancelled}</p>
Cancelled
</dt>
<dd className="text-lg font-medium text-foreground">{stats.cancelled}</dd>
</dl>
</div> </div>
</div> </div>
</SubCard> </div>
</div> </div>
)} )}
<div className="space-y-4"> <div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
{/* Search/Filter Header */} {/* Search/Filter Header */}
<div className="bg-card rounded-xl border border-border px-5 py-4 shadow-[var(--cp-shadow-1)]"> <div className="px-6 py-4 border-b border-border">
<SearchFilterBar <SearchFilterBar
searchValue={searchTerm} searchValue={searchTerm}
onSearchChange={setSearchTerm} onSearchChange={setSearchTerm}
@ -163,13 +153,11 @@ export function SubscriptionsListContainer() {
</div> </div>
{/* Subscriptions Table */} {/* Subscriptions Table */}
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden"> <SubscriptionTable
<SubscriptionTable subscriptions={filteredSubscriptions}
subscriptions={filteredSubscriptions} loading={isLoading}
loading={isLoading} className="border-0 rounded-none shadow-none"
className="border-0 rounded-none shadow-none" />
/>
</div>
</div> </div>
</ErrorBoundary> </ErrorBoundary>
</PageLayout> </PageLayout>

View File

@ -28,15 +28,15 @@ const ICON_SIZE_CLASSES: Record<IconSize, string> = {
* Status to icon mapping * Status to icon mapping
*/ */
const STATUS_ICON_MAP: Record<string, (className: string) => ReactNode> = { const STATUS_ICON_MAP: Record<string, (className: string) => ReactNode> = {
[SUPPORT_CASE_STATUS.RESOLVED]: cls => <CheckCircleIcon className={`${cls} text-green-500`} />, [SUPPORT_CASE_STATUS.RESOLVED]: cls => <CheckCircleIcon className={`${cls} text-success`} />,
[SUPPORT_CASE_STATUS.CLOSED]: cls => <CheckCircleIcon className={`${cls} text-green-500`} />, [SUPPORT_CASE_STATUS.CLOSED]: cls => <CheckCircleIcon className={`${cls} text-success`} />,
[SUPPORT_CASE_STATUS.VPN_PENDING]: cls => <CheckCircleIcon className={`${cls} text-green-500`} />, [SUPPORT_CASE_STATUS.VPN_PENDING]: cls => <CheckCircleIcon className={`${cls} text-success`} />,
[SUPPORT_CASE_STATUS.PENDING]: cls => <CheckCircleIcon className={`${cls} text-green-500`} />, [SUPPORT_CASE_STATUS.PENDING]: cls => <CheckCircleIcon className={`${cls} text-success`} />,
[SUPPORT_CASE_STATUS.IN_PROGRESS]: cls => <ClockIcon className={`${cls} text-blue-500`} />, [SUPPORT_CASE_STATUS.IN_PROGRESS]: cls => <ClockIcon className={`${cls} text-info`} />,
[SUPPORT_CASE_STATUS.AWAITING_APPROVAL]: cls => ( [SUPPORT_CASE_STATUS.AWAITING_APPROVAL]: cls => (
<ExclamationTriangleIcon className={`${cls} text-amber-500`} /> <ExclamationTriangleIcon className={`${cls} text-warning`} />
), ),
[SUPPORT_CASE_STATUS.NEW]: cls => <SparklesIcon className={`${cls} text-purple-500`} />, [SUPPORT_CASE_STATUS.NEW]: cls => <SparklesIcon className={`${cls} text-primary`} />,
}; };
/** /**
@ -65,42 +65,42 @@ const PRIORITY_VARIANT_MAP: Record<string, CasePriorityVariant> = {
* Tailwind class mappings for status variants * Tailwind class mappings for status variants
*/ */
const STATUS_CLASSES: Record<CaseStatusVariant, string> = { const STATUS_CLASSES: Record<CaseStatusVariant, string> = {
success: "text-green-700 bg-green-50", success: "text-success bg-success-soft",
info: "text-blue-700 bg-blue-50", info: "text-info bg-info-soft",
warning: "text-amber-700 bg-amber-50", warning: "text-warning bg-warning-soft",
purple: "text-purple-700 bg-purple-50", purple: "text-primary bg-primary/10",
neutral: "text-gray-700 bg-gray-50", neutral: "text-muted-foreground bg-muted",
}; };
/** /**
* Tailwind class mappings for status variants with border * Tailwind class mappings for status variants with border
*/ */
const STATUS_CLASSES_WITH_BORDER: Record<CaseStatusVariant, string> = { const STATUS_CLASSES_WITH_BORDER: Record<CaseStatusVariant, string> = {
success: "text-green-700 bg-green-50 border-green-200", success: "text-success bg-success-soft border-success/20",
info: "text-blue-700 bg-blue-50 border-blue-200", info: "text-info bg-info-soft border-info/20",
warning: "text-amber-700 bg-amber-50 border-amber-200", warning: "text-warning bg-warning-soft border-warning/20",
purple: "text-purple-700 bg-purple-50 border-purple-200", purple: "text-primary bg-primary/10 border-primary/20",
neutral: "text-gray-700 bg-gray-50 border-gray-200", neutral: "text-muted-foreground bg-muted border-border",
}; };
/** /**
* Tailwind class mappings for priority variants * Tailwind class mappings for priority variants
*/ */
const PRIORITY_CLASSES: Record<CasePriorityVariant, string> = { const PRIORITY_CLASSES: Record<CasePriorityVariant, string> = {
high: "text-red-700 bg-red-50", high: "text-destructive bg-destructive-soft",
medium: "text-amber-700 bg-amber-50", medium: "text-warning bg-warning-soft",
low: "text-green-700 bg-green-50", low: "text-success bg-success-soft",
neutral: "text-gray-700 bg-gray-50", neutral: "text-muted-foreground bg-muted",
}; };
/** /**
* Tailwind class mappings for priority variants with border * Tailwind class mappings for priority variants with border
*/ */
const PRIORITY_CLASSES_WITH_BORDER: Record<CasePriorityVariant, string> = { const PRIORITY_CLASSES_WITH_BORDER: Record<CasePriorityVariant, string> = {
high: "text-red-700 bg-red-50 border-red-200", high: "text-destructive bg-destructive-soft border-destructive/20",
medium: "text-amber-700 bg-amber-50 border-amber-200", medium: "text-warning bg-warning-soft border-warning/20",
low: "text-green-700 bg-green-50 border-green-200", low: "text-success bg-success-soft border-success/20",
neutral: "text-gray-700 bg-gray-50 border-gray-200", neutral: "text-muted-foreground bg-muted border-border",
}; };
/** /**
@ -114,7 +114,7 @@ export function getCaseStatusIcon(status: string, size: IconSize = "sm"): ReactN
return iconFn(sizeClass); return iconFn(sizeClass);
} }
return <ChatBubbleLeftRightIcon className={`${sizeClass} text-gray-400`} />; return <ChatBubbleLeftRightIcon className={`${sizeClass} text-muted-foreground`} />;
} }
/** /**

View File

@ -128,14 +128,14 @@ export function SupportCasesView() {
{/* Search */} {/* Search */}
<div className="flex-1 relative"> <div className="flex-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-4 w-4 text-muted-foreground/70" /> <MagnifyingGlassIcon className="h-4 w-4 text-muted-foreground" />
</div> </div>
<input <input
type="text" type="text"
placeholder="Search by case number or subject..." placeholder="Search by case number or subject..."
value={searchTerm} value={searchTerm}
onChange={event => setSearchTerm(event.target.value)} onChange={event => setSearchTerm(event.target.value)}
className="block w-full pl-9 pr-3 py-2 border border-input rounded-lg text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors" className="block w-full pl-9 pr-3 py-2 border border-border rounded-lg text-sm bg-card shadow-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors"
/> />
</div> </div>
@ -144,7 +144,7 @@ export function SupportCasesView() {
<select <select
value={statusFilter} value={statusFilter}
onChange={event => setStatusFilter(event.target.value)} onChange={event => setStatusFilter(event.target.value)}
className="appearance-none pl-3 pr-8 py-2 border border-input rounded-lg text-sm bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors cursor-pointer" className="appearance-none pl-3 pr-8 py-2 border border-border rounded-lg text-sm bg-card shadow-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors cursor-pointer"
> >
{statusFilterOptions.map(option => ( {statusFilterOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@ -156,7 +156,7 @@ export function SupportCasesView() {
<select <select
value={priorityFilter} value={priorityFilter}
onChange={event => setPriorityFilter(event.target.value)} onChange={event => setPriorityFilter(event.target.value)}
className="appearance-none pl-3 pr-8 py-2 border border-input rounded-lg text-sm bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors cursor-pointer" className="appearance-none pl-3 pr-8 py-2 border border-border rounded-lg text-sm bg-card shadow-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors cursor-pointer"
> >
{priorityFilterOptions.map(option => ( {priorityFilterOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>

View File

@ -78,8 +78,8 @@
--cp-card-shadow-lg: var(--cp-shadow-3); --cp-card-shadow-lg: var(--cp-shadow-3);
/* ============= SHADOWS ============= */ /* ============= SHADOWS ============= */
--cp-shadow-1: 0 1px 2px 0 rgb(0 0 0 / 0.06); --cp-shadow-1: 0 1px 3px 0 rgb(0 0 0 / 0.08), 0 1px 2px -1px rgb(0 0 0 / 0.06);
--cp-shadow-2: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.08); --cp-shadow-2: 0 2px 4px -1px rgb(0 0 0 / 0.1), 0 1px 3px 0 rgb(0 0 0 / 0.08);
--cp-shadow-3: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05); --cp-shadow-3: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
/* ============= MOTION ============= */ /* ============= MOTION ============= */
@ -93,20 +93,20 @@
--cp-transition-slow: var(--cp-duration-slow); --cp-transition-slow: var(--cp-duration-slow);
/* ============= COLOR (LIGHT) ============= */ /* ============= COLOR (LIGHT) ============= */
/* Core neutrals (light) */ /* Core neutrals (light) - clean white main area */
--cp-bg: oklch(0.98 0 0); --cp-bg: oklch(1 0 0); /* pure white for main content area */
--cp-fg: oklch(0.16 0 0); --cp-fg: oklch(0.16 0 0);
--cp-surface: oklch(0.95 0 0); /* panels/strips */ --cp-surface: oklch(1 0 0); /* pure white for cards */
--cp-muted: oklch(0.93 0 0); /* chips/subtle */ --cp-muted: oklch(0.97 0.003 250); /* subtle cool gray for table headers */
--cp-border: oklch(0.9 0 0); --cp-border: oklch(0.9 0 0);
--cp-border-muted: oklch(0.88 0 0); --cp-border-muted: oklch(0.92 0 0);
/* Brand & focus (azure) */ /* Brand & focus - matches logo cyan blue */
--cp-primary: oklch(0.62 0.17 255); --cp-primary: oklch(0.68 0.14 220); /* bright cyan blue from logo */
--cp-on-primary: oklch(0.99 0 0); --cp-on-primary: oklch(0.99 0 0);
--cp-primary-hover: oklch(0.58 0.17 255); --cp-primary-hover: oklch(0.63 0.14 220);
--cp-primary-active: oklch(0.54 0.17 255); --cp-primary-active: oklch(0.58 0.14 220);
--cp-ring: oklch(0.68 0.16 255); --cp-ring: oklch(0.72 0.12 220);
/* Semantic */ /* Semantic */
--cp-success: oklch(0.67 0.14 150); --cp-success: oklch(0.67 0.14 150);
@ -121,20 +121,21 @@
--cp-on-error: oklch(0.99 0 0); --cp-on-error: oklch(0.99 0 0);
--cp-error-soft: oklch(0.96 0.05 27); --cp-error-soft: oklch(0.96 0.05 27);
--cp-info: oklch(0.64 0.16 255); --cp-info: oklch(0.68 0.14 220); /* matches primary */
--cp-on-info: oklch(0.99 0 0); --cp-on-info: oklch(0.99 0 0);
--cp-info-soft: oklch(0.95 0.05 255); --cp-info-soft: oklch(0.95 0.06 220);
/* Sidebar/Header derive from core tokens */ /* Sidebar - Brand navy blue from logo */
--cp-sidebar-bg: var(--cp-bg); --cp-sidebar-bg: oklch(0.28 0.1 265); /* deep navy blue from logo */
--cp-sidebar-border: var(--cp-border); --cp-sidebar-border: oklch(0.35 0.08 265); /* lighter navy for borders */
--cp-sidebar-text: oklch(0.35 0 0); --cp-sidebar-text: oklch(1 0 0); /* pure white text */
--cp-sidebar-text-hover: oklch(0.22 0 0); --cp-sidebar-text-hover: oklch(1 0 0); /* pure white on hover */
--cp-sidebar-hover-bg: var(--cp-muted); --cp-sidebar-hover-bg: oklch(0.35 0.09 265); /* lighter navy on hover */
--cp-sidebar-active-bg: color-mix(in oklch, var(--cp-primary) 12%, transparent); --cp-sidebar-active-bg: oklch(1 0 0 / 0.18); /* white overlay for active */
--cp-sidebar-active-text: var(--cp-primary); --cp-sidebar-active-text: oklch(1 0 0); /* white for active text */
--cp-header-bg: oklch(1 0 0 / 0.85); /* Header */
--cp-header-bg: oklch(1 0 0 / 0.95); /* slightly more opaque white */
--cp-header-border: var(--cp-border); --cp-header-border: var(--cp-border);
--cp-header-text: oklch(0.2 0 0); --cp-header-text: oklch(0.2 0 0);
@ -171,11 +172,11 @@
--cp-border: oklch(0.32 0 0); --cp-border: oklch(0.32 0 0);
--cp-border-muted: oklch(0.28 0 0); --cp-border-muted: oklch(0.28 0 0);
--cp-primary: oklch(0.74 0.16 255); --cp-primary: oklch(0.75 0.12 220); /* lighter cyan for dark mode */
--cp-on-primary: oklch(0.15 0 0); --cp-on-primary: oklch(0.15 0 0);
--cp-primary-hover: oklch(0.7 0.16 255); --cp-primary-hover: oklch(0.7 0.12 220);
--cp-primary-active: oklch(0.66 0.16 255); --cp-primary-active: oklch(0.65 0.12 220);
--cp-ring: oklch(0.78 0.13 255); --cp-ring: oklch(0.78 0.1 220);
--cp-success: oklch(0.76 0.14 150); --cp-success: oklch(0.76 0.14 150);
--cp-on-success: oklch(0.15 0 0); --cp-on-success: oklch(0.15 0 0);
@ -189,18 +190,20 @@
--cp-on-error: oklch(0.15 0 0); --cp-on-error: oklch(0.15 0 0);
--cp-error-soft: oklch(0.25 0.05 27); --cp-error-soft: oklch(0.25 0.05 27);
--cp-info: oklch(0.78 0.15 255); --cp-info: oklch(0.75 0.12 220);
--cp-on-info: oklch(0.15 0 0); --cp-on-info: oklch(0.15 0 0);
--cp-info-soft: oklch(0.24 0.05 255); --cp-info-soft: oklch(0.24 0.05 220);
--cp-sidebar-bg: var(--cp-surface); /* Sidebar - Dark navy from logo */
--cp-sidebar-hover-bg: oklch(0.24 0 0); --cp-sidebar-bg: oklch(0.2 0.08 265); /* darker navy */
--cp-sidebar-text: oklch(0.9 0 0); --cp-sidebar-border: oklch(0.28 0.06 265);
--cp-sidebar-text-hover: oklch(0.98 0 0); --cp-sidebar-hover-bg: oklch(0.28 0.07 265);
--cp-sidebar-active-bg: color-mix(in oklch, var(--cp-primary) 18%, transparent); --cp-sidebar-text: oklch(1 0 0); /* pure white */
--cp-sidebar-active-text: var(--cp-primary); --cp-sidebar-text-hover: oklch(1 0 0);
--cp-sidebar-active-bg: oklch(1 0 0 / 0.18);
--cp-sidebar-active-text: oklch(1 0 0);
--cp-header-bg: oklch(0.18 0 0 / 0.9); --cp-header-bg: oklch(0.18 0 0 / 0.95);
--cp-header-border: var(--cp-border); --cp-header-border: var(--cp-border);
--cp-header-text: var(--cp-fg); --cp-header-text: var(--cp-fg);
} }

View File

@ -11,10 +11,18 @@
.cp-card { .cp-card {
background: var(--card); background: var(--card);
color: var(--card-foreground); color: var(--card-foreground);
border: 1px solid var(--border); border: 1px solid color-mix(in oklch, var(--border) 60%, transparent);
border-radius: var(--cp-card-radius); border-radius: var(--cp-card-radius);
box-shadow: var(--cp-card-shadow); box-shadow: var(--cp-card-shadow);
padding: var(--cp-card-padding); padding: var(--cp-card-padding);
transition:
box-shadow 0.2s ease,
border-color 0.2s ease;
}
.cp-card:hover {
box-shadow: var(--cp-card-shadow-lg);
border-color: var(--border);
} }
.cp-card-sm { .cp-card-sm {
@ -26,6 +34,18 @@
box-shadow: var(--cp-card-shadow-lg); box-shadow: var(--cp-card-shadow-lg);
} }
.cp-card-interactive {
cursor: pointer;
}
.cp-card-interactive:hover {
transform: translateY(-1px);
}
.cp-card-interactive:active {
transform: translateY(0);
}
/* ===== BADGE ===== */ /* ===== BADGE ===== */
.cp-badge { .cp-badge {
display: inline-flex; display: inline-flex;