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:
parent
b5c917be33
commit
9d6c7dcde0
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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't have an account?{" "}
|
Don'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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
|||||||
You’re almost done. Confirm your details above, then submit your order. We’ll review and
|
You’re almost done. Confirm your details above, then submit your order. We’ll 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
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
|
||||||
@ -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 };
|
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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> = {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user