From b99799c2fec48b92d43cc38beab666d7650509af Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 16 Dec 2025 13:54:31 +0900 Subject: [PATCH] Refactor UI components and enhance styling consistency across the portal - Updated various components to use consistent color tokens, improving visual coherence. - Refactored layout components to utilize the new PublicShell for better structure. - Enhanced error and status messaging styles for improved user feedback. - Standardized button usage across forms and modals for a unified interaction experience. - Introduced new UI design tokens and guidelines in documentation to support future development. --- apps/bff/src/core/logging/logging.module.ts | 17 +- apps/portal/src/app/(public)/layout.tsx | 16 +- .../src/components/atoms/LoadingOverlay.tsx | 8 +- apps/portal/src/components/atoms/badge.tsx | 41 +- apps/portal/src/components/atoms/button.tsx | 17 +- apps/portal/src/components/atoms/checkbox.tsx | 12 +- .../src/components/atoms/empty-state.tsx | 10 +- .../src/components/atoms/error-state.tsx | 14 +- apps/portal/src/components/atoms/input.tsx | 5 +- .../src/components/atoms/loading-skeleton.tsx | 10 +- .../src/components/atoms/status-pill.tsx | 10 +- .../src/components/atoms/step-header.tsx | 8 +- .../molecules/AlertBanner/AlertBanner.tsx | 38 +- .../molecules/AnimatedCard/AnimatedCard.tsx | 14 +- .../molecules/AsyncBlock/AsyncBlock.tsx | 9 +- .../molecules/DataTable/DataTable.tsx | 2 +- .../molecules/DetailHeader/DetailHeader.tsx | 6 +- .../molecules/FormField/FormField.tsx | 18 +- .../molecules/PaginationBar/PaginationBar.tsx | 10 +- .../molecules/ProgressSteps/ProgressSteps.tsx | 18 +- .../SearchFilterBar/SearchFilterBar.tsx | 12 +- .../molecules/SectionHeader/SectionHeader.tsx | 2 +- .../components/molecules/SubCard/SubCard.tsx | 2 +- .../components/molecules/error-boundary.tsx | 16 +- .../AgentforceWidget/AgentforceWidget.tsx | 26 +- .../components/organisms/AppShell/Header.tsx | 6 +- .../components/organisms/AppShell/Sidebar.tsx | 10 +- .../templates/AuthLayout/AuthLayout.tsx | 20 +- .../templates/PageLayout/PageLayout.tsx | 5 +- .../templates/PublicShell/PublicShell.tsx | 73 ++ apps/portal/src/components/templates/index.ts | 3 + .../account/components/PersonalInfoCard.tsx | 96 +-- .../account/views/ProfileContainer.tsx | 599 +++++++++-------- .../PasswordResetForm/PasswordResetForm.tsx | 34 +- .../SetPasswordForm/SetPasswordForm.tsx | 66 +- .../components/SignupForm/MultiStepForm.tsx | 26 +- .../auth/components/SignupForm/SignupForm.tsx | 16 +- .../SignupForm/steps/AccountStep.tsx | 11 +- .../SignupForm/steps/AddressStep.tsx | 16 +- .../SignupForm/steps/PasswordStep.tsx | 25 +- .../SignupForm/steps/ReviewStep.tsx | 86 +-- .../src/features/auth/views/LinkWhmcsView.tsx | 47 +- .../features/billing/views/InvoiceDetail.tsx | 65 +- .../components/base/AddressConfirmation.tsx | 77 +-- .../catalog/components/base/AddressForm.tsx | 82 ++- .../catalog/components/sim/ActivationForm.tsx | 34 +- .../components/sim/SimConfigureView.tsx | 138 ++-- .../features/catalog/views/CatalogHome.tsx | 156 ++--- .../features/catalog/views/InternetPlans.tsx | 279 ++++---- .../checkout/views/CheckoutContainer.tsx | 77 +-- .../components/AccountStatusCard.tsx | 12 +- .../components/DashboardActivityItem.tsx | 39 +- .../dashboard/components/QuickAction.tsx | 8 +- .../dashboard/components/StatCard.tsx | 10 +- .../dashboard/views/DashboardView.tsx | 379 +++++------ .../landing-page/views/PublicLandingView.tsx | 228 +++---- .../src/features/orders/views/OrderDetail.tsx | 78 +-- .../src/features/orders/views/OrdersList.tsx | 32 +- .../components/ServiceManagementSection.tsx | 60 +- .../components/ReissueSimModal.tsx | 112 ++-- .../sim-management/components/SimActions.tsx | 125 ++-- .../components/SimDetailsCard.tsx | 145 ++-- .../components/SimFeatureToggles.tsx | 215 +++--- .../components/SimManagementSection.tsx | 159 ++--- .../subscriptions/utils/status-presenters.tsx | 10 +- .../subscriptions/views/SimCallHistory.tsx | 152 +++-- .../subscriptions/views/SimCancel.tsx | 623 +++++++++--------- .../subscriptions/views/SimChangePlan.tsx | 102 +-- .../subscriptions/views/SimReissue.tsx | 113 ++-- .../features/subscriptions/views/SimTopUp.tsx | 54 +- .../views/SubscriptionDetail.tsx | 276 ++++---- .../subscriptions/views/SubscriptionsList.tsx | 32 +- .../support/views/NewSupportCaseView.tsx | 128 ++-- .../support/views/SupportCaseDetailView.tsx | 48 +- .../support/views/SupportCasesView.tsx | 70 +- .../support/views/SupportHomeView.tsx | 50 +- apps/portal/src/hooks/useZodForm.ts | 51 +- apps/portal/src/styles/tokens.css | 67 +- docs/portal-guides/README.md | 1 + docs/portal-guides/ui-design-system.md | 36 + 80 files changed, 3041 insertions(+), 2762 deletions(-) create mode 100644 apps/portal/src/components/templates/PublicShell/PublicShell.tsx create mode 100644 docs/portal-guides/ui-design-system.md diff --git a/apps/bff/src/core/logging/logging.module.ts b/apps/bff/src/core/logging/logging.module.ts index a46dbe47..60082ff1 100644 --- a/apps/bff/src/core/logging/logging.module.ts +++ b/apps/bff/src/core/logging/logging.module.ts @@ -1,7 +1,8 @@ import { Global, Module } from "@nestjs/common"; import { LoggerModule } from "nestjs-pino"; -const prettyLogsEnabled = process.env.PRETTY_LOGS === "true" || process.env.NODE_ENV !== "production"; +const prettyLogsEnabled = + process.env.PRETTY_LOGS === "true" || process.env.NODE_ENV !== "production"; @Global() @Module({ @@ -10,6 +11,20 @@ const prettyLogsEnabled = process.env.PRETTY_LOGS === "true" || process.env.NODE pinoHttp: { level: process.env.LOG_LEVEL || "info", name: process.env.APP_NAME || "customer-portal-bff", + /** + * Reduce noise from pino-http auto logging: + * - successful requests => debug (hidden when LOG_LEVEL=info) + * - 4xx => warn + * - 5xx / errors => error + * + * This keeps production logs focused on actionable events while still + * allowing full request logging by setting LOG_LEVEL=debug. + */ + customLogLevel: (_req, res, err) => { + if (err || (res?.statusCode && res.statusCode >= 500)) return "error"; + if (res?.statusCode && res.statusCode >= 400) return "warn"; + return "debug"; + }, autoLogging: { ignore: req => { const url = req.url || ""; diff --git a/apps/portal/src/app/(public)/layout.tsx b/apps/portal/src/app/(public)/layout.tsx index 4b116e63..aa1e7f26 100644 --- a/apps/portal/src/app/(public)/layout.tsx +++ b/apps/portal/src/app/(public)/layout.tsx @@ -1,15 +1,11 @@ /** * Public Layout - * - * Layout for public-facing pages (landing, auth, etc.) - * Route groups in Next.js 15 require a layout file for standalone builds + * + * Shared shell for public-facing pages (landing, auth, etc.) */ -export default function PublicLayout({ - children, -}: { - children: React.ReactNode; -}) { - return <>{children}; -} +import { PublicShell } from "@/components/templates"; +export default function PublicLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/portal/src/components/atoms/LoadingOverlay.tsx b/apps/portal/src/components/atoms/LoadingOverlay.tsx index d4afcf52..4a2399db 100644 --- a/apps/portal/src/components/atoms/LoadingOverlay.tsx +++ b/apps/portal/src/components/atoms/LoadingOverlay.tsx @@ -20,8 +20,8 @@ export function LoadingOverlay({ title, subtitle, spinnerSize = "xl", - spinnerClassName = "text-blue-600", - overlayClassName = "bg-white/80 backdrop-blur-sm", + spinnerClassName = "text-primary", + overlayClassName = "bg-background/80 backdrop-blur-sm", }: LoadingOverlayProps) { if (!isVisible) { return null; @@ -33,8 +33,8 @@ export function LoadingOverlay({
-

{title}

- {subtitle &&

{subtitle}

} +

{title}

+ {subtitle &&

{subtitle}

} ); diff --git a/apps/portal/src/components/atoms/badge.tsx b/apps/portal/src/components/atoms/badge.tsx index 2364763e..7fdcabd6 100644 --- a/apps/portal/src/components/atoms/badge.tsx +++ b/apps/portal/src/components/atoms/badge.tsx @@ -7,14 +7,14 @@ const badgeVariants = cva( { variants: { variant: { - default: "bg-blue-600 text-white hover:bg-blue-700", - secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200", - success: "bg-green-100 text-green-800 hover:bg-green-200", - warning: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200", - error: "bg-red-100 text-red-800 hover:bg-red-200", - info: "bg-blue-100 text-blue-800 hover:bg-blue-200", - outline: "border border-gray-300 bg-white text-gray-900 hover:bg-gray-50", - ghost: "text-gray-900 hover:bg-gray-100", + default: "bg-primary text-primary-foreground hover:bg-primary-hover", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + success: "bg-success-soft text-success hover:bg-success-soft/80", + warning: "bg-warning-soft text-foreground hover:bg-warning-soft/80", + error: "bg-destructive-soft text-destructive hover:bg-destructive-soft/80", + info: "bg-info-soft text-info hover:bg-info-soft/80", + outline: "border border-border bg-background text-foreground hover:bg-muted", + ghost: "text-foreground hover:bg-muted", }, size: { sm: "px-2 py-0.5 text-xs rounded", @@ -30,8 +30,7 @@ const badgeVariants = cva( ); interface BadgeProps - extends React.HTMLAttributes, - VariantProps { + extends React.HTMLAttributes, VariantProps { icon?: React.ReactNode; dot?: boolean; removable?: boolean; @@ -46,14 +45,14 @@ const Badge = forwardRef( )} @@ -64,9 +63,9 @@ const Badge = forwardRef( type="button" onClick={onRemove} className={cn( - "ml-1 inline-flex h-3 w-3 items-center justify-center rounded-full hover:bg-black/10 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-white/20", - variant === "default" && "text-white hover:bg-white/20", - variant === "secondary" && "text-gray-600 hover:bg-gray-300", + "ml-1 inline-flex h-3 w-3 items-center justify-center rounded-full hover:bg-black/10 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring/20", + variant === "default" && "text-primary-foreground hover:bg-primary-foreground/10", + variant === "secondary" && "text-secondary-foreground hover:bg-black/10", (variant === "success" || variant === "warning" || variant === "error" || diff --git a/apps/portal/src/components/atoms/button.tsx b/apps/portal/src/components/atoms/button.tsx index 3dcc311a..f6dc8b87 100644 --- a/apps/portal/src/components/atoms/button.tsx +++ b/apps/portal/src/components/atoms/button.tsx @@ -5,17 +5,20 @@ import { cn } from "@/lib/utils"; import { Spinner } from "./Spinner"; const buttonVariants = cva( - "inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background active:scale-[0.98]", + "group inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors duration-[var(--cp-duration-normal)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 disabled:pointer-events-none active:scale-[0.98]", { variants: { variant: { - default: "bg-blue-600 text-white hover:bg-blue-700 shadow-sm hover:shadow-md", - destructive: "bg-red-600 text-white hover:bg-red-700 shadow-sm hover:shadow-md", + default: + "bg-primary text-primary-foreground hover:bg-primary-hover shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", outline: - "border border-gray-300 bg-white hover:bg-gray-50 hover:border-gray-400 shadow-sm hover:shadow-md", - secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 shadow-sm hover:shadow-md", - ghost: "hover:bg-gray-100 hover:shadow-sm", - link: "underline-offset-4 hover:underline text-blue-600", + "border border-border bg-background text-foreground hover:bg-muted shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-[var(--cp-shadow-1)] hover:shadow-[var(--cp-shadow-2)]", + ghost: "text-foreground hover:bg-muted", + link: "underline-offset-4 hover:underline text-primary", }, size: { default: "h-11 py-2.5 px-4", diff --git a/apps/portal/src/components/atoms/checkbox.tsx b/apps/portal/src/components/atoms/checkbox.tsx index 189f9f2a..b5e1890c 100644 --- a/apps/portal/src/components/atoms/checkbox.tsx +++ b/apps/portal/src/components/atoms/checkbox.tsx @@ -24,8 +24,8 @@ export const Checkbox = React.forwardRef( id={checkboxId} ref={ref} className={cn( - "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 focus:ring-2", - error && "border-red-500", + "h-4 w-4 rounded border-input text-primary focus:ring-ring focus:ring-2", + error && "border-destructive", className )} {...props} @@ -34,16 +34,16 @@ export const Checkbox = React.forwardRef( )} - {helperText && !error &&

{helperText}

} - {error &&

{error}

} + {helperText && !error &&

{helperText}

} + {error &&

{error}

} ); } diff --git a/apps/portal/src/components/atoms/empty-state.tsx b/apps/portal/src/components/atoms/empty-state.tsx index 668a4d24..e5820cfc 100644 --- a/apps/portal/src/components/atoms/empty-state.tsx +++ b/apps/portal/src/components/atoms/empty-state.tsx @@ -33,16 +33,20 @@ export function EmptyState({ className )} > - {icon &&
{icon}
} + {icon && ( +
+ {icon} +
+ )} -

+

{title}

{description && (

diff --git a/apps/portal/src/components/atoms/error-state.tsx b/apps/portal/src/components/atoms/error-state.tsx index 01d5dcaf..daac3155 100644 --- a/apps/portal/src/components/atoms/error-state.tsx +++ b/apps/portal/src/components/atoms/error-state.tsx @@ -23,8 +23,8 @@ export function ErrorState({ const variantClasses = { page: "min-h-[400px] py-12", - card: "bg-white border border-red-200 rounded-[var(--cp-card-radius)] p-[var(--cp-card-padding)] shadow-[var(--cp-card-shadow)]", - inline: "bg-red-50 border border-red-200 rounded-md p-4", + card: "bg-card text-card-foreground border border-destructive/25 rounded-[var(--cp-card-radius)] p-[var(--cp-card-padding)] shadow-[var(--cp-card-shadow)]", + inline: "bg-destructive-soft border border-destructive/25 rounded-md p-4", }; const iconSizes = { @@ -47,21 +47,23 @@ export function ErrorState({ return (

-
+
-

{title}

+

{title}

-

{message}

+

+ {message} +

{onRetry && ( diff --git a/apps/portal/src/components/molecules/AnimatedCard/AnimatedCard.tsx b/apps/portal/src/components/molecules/AnimatedCard/AnimatedCard.tsx index 1bd5bea6..8f5080df 100644 --- a/apps/portal/src/components/molecules/AnimatedCard/AnimatedCard.tsx +++ b/apps/portal/src/components/molecules/AnimatedCard/AnimatedCard.tsx @@ -16,18 +16,16 @@ export function AnimatedCard({ disabled = false, }: AnimatedCardProps) { const baseClasses = - "bg-white rounded-xl border-2 shadow-sm transition-all duration-300 ease-in-out transform"; + "bg-card text-card-foreground rounded-xl border shadow-[var(--cp-shadow-1)] transition-shadow duration-[var(--cp-duration-normal)]"; const variantClasses: Record<"default" | "highlighted" | "success" | "static", string> = { - default: "border-gray-200 hover:shadow-xl hover:-translate-y-1", - highlighted: - "border-blue-300 ring-2 ring-blue-100 shadow-md hover:shadow-xl hover:-translate-y-1", - success: - "border-green-300 ring-2 ring-green-100 shadow-md hover:shadow-xl hover:-translate-y-1", - static: "border-gray-200 shadow-sm", // No hover animations for static containers + default: "border-border hover:shadow-[var(--cp-shadow-2)]", + highlighted: "border-primary/35 ring-1 ring-primary/15 hover:shadow-[var(--cp-shadow-2)]", + success: "border-success/25 ring-1 ring-success/15 hover:shadow-[var(--cp-shadow-2)]", + static: "border-border shadow-[var(--cp-shadow-1)]", // No hover animations for static containers }; - const interactiveClasses = onClick && !disabled ? "cursor-pointer hover:scale-[1.02]" : ""; + const interactiveClasses = onClick && !disabled ? "cursor-pointer" : ""; const disabledClasses = disabled ? "opacity-50 cursor-not-allowed" : ""; diff --git a/apps/portal/src/components/molecules/AsyncBlock/AsyncBlock.tsx b/apps/portal/src/components/molecules/AsyncBlock/AsyncBlock.tsx index ad69802b..165ee91b 100644 --- a/apps/portal/src/components/molecules/AsyncBlock/AsyncBlock.tsx +++ b/apps/portal/src/components/molecules/AsyncBlock/AsyncBlock.tsx @@ -25,7 +25,7 @@ export function AsyncBlock({ return (
- {loadingText ?

{loadingText}

: null} + {loadingText ?

{loadingText}

: null}
@@ -35,7 +35,10 @@ export function AsyncBlock({
{Array.from({ length: 3 }).map((_, i) => ( -
+
@@ -47,7 +50,7 @@ export function AsyncBlock({ } return (
- {loadingText ?

{loadingText}

: null} + {loadingText ?

{loadingText}

: null} diff --git a/apps/portal/src/components/molecules/DataTable/DataTable.tsx b/apps/portal/src/components/molecules/DataTable/DataTable.tsx index 25c4cae4..4256abe7 100644 --- a/apps/portal/src/components/molecules/DataTable/DataTable.tsx +++ b/apps/portal/src/components/molecules/DataTable/DataTable.tsx @@ -55,7 +55,7 @@ export function DataTable({ ))} - + {data.map(item => ( +
{leftIcon}
-

{title}

- {subtitle &&

{subtitle}

} +

{title}

+ {subtitle &&

{subtitle}

}
{status && } diff --git a/apps/portal/src/components/molecules/FormField/FormField.tsx b/apps/portal/src/components/molecules/FormField/FormField.tsx index abc37abc..2471ecf2 100644 --- a/apps/portal/src/components/molecules/FormField/FormField.tsx +++ b/apps/portal/src/components/molecules/FormField/FormField.tsx @@ -4,11 +4,10 @@ import { Label, type LabelProps } from "@/components/atoms/label"; import { Input, type InputProps } from "@/components/atoms/input"; import { ErrorMessage } from "@/components/atoms/error-message"; -interface FormFieldProps - extends Omit< - InputProps, - "id" | "aria-describedby" | "aria-invalid" | "children" | "dangerouslySetInnerHTML" - > { +interface FormFieldProps extends Omit< + InputProps, + "id" | "aria-describedby" | "aria-invalid" | "children" | "dangerouslySetInnerHTML" +> { label?: string; error?: string; helperText?: string; @@ -49,8 +48,8 @@ const FormField = forwardRef(
-

+

Showing {(currentPage - 1) * pageSize + 1} to{" "} {Math.min(currentPage * pageSize, totalItems)} of{" "} {totalItems} results @@ -55,14 +55,14 @@ export function PaginationBar({ diff --git a/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx b/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx index fc68dbb9..d0743ec9 100644 --- a/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx +++ b/apps/portal/src/components/molecules/ProgressSteps/ProgressSteps.tsx @@ -15,7 +15,7 @@ interface ProgressStepsProps { export function ProgressSteps({ steps, currentStep, className = "" }: ProgressStepsProps) { return (

-
+
{steps.map((step, index) => ( @@ -24,10 +24,10 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
{step.completed ? ( @@ -41,10 +41,10 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt {step.title} @@ -52,10 +52,10 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
{index < steps.length - 1 && (
-
+
-
+
+
{/* Search */}
- +
onSearchChange(e.target.value)} @@ -53,7 +53,7 @@ export function SearchFilterBar({
- +
)} diff --git a/apps/portal/src/components/molecules/SectionHeader/SectionHeader.tsx b/apps/portal/src/components/molecules/SectionHeader/SectionHeader.tsx index 3eedc56d..ac1bf42f 100644 --- a/apps/portal/src/components/molecules/SectionHeader/SectionHeader.tsx +++ b/apps/portal/src/components/molecules/SectionHeader/SectionHeader.tsx @@ -11,7 +11,7 @@ interface SectionHeaderProps { export function SectionHeader({ title, children, className }: SectionHeaderProps) { return (
-

{title}

+

{title}

{children}
); diff --git a/apps/portal/src/components/molecules/SubCard/SubCard.tsx b/apps/portal/src/components/molecules/SubCard/SubCard.tsx index 0675d8a7..7d15cc29 100644 --- a/apps/portal/src/components/molecules/SubCard/SubCard.tsx +++ b/apps/portal/src/components/molecules/SubCard/SubCard.tsx @@ -29,7 +29,7 @@ export const SubCard = forwardRef( ) => (
{header ? (
{header}
diff --git a/apps/portal/src/components/molecules/error-boundary.tsx b/apps/portal/src/components/molecules/error-boundary.tsx index fc22b838..4c19171d 100644 --- a/apps/portal/src/components/molecules/error-boundary.tsx +++ b/apps/portal/src/components/molecules/error-boundary.tsx @@ -2,6 +2,7 @@ import { Component, ReactNode, ErrorInfo } from "react"; import { log } from "@/lib/logger"; +import { Button } from "@/components/atoms/button"; interface ErrorBoundaryState { hasError: boolean; @@ -51,16 +52,15 @@ export class ErrorBoundary extends Component
-

Something went wrong

-

- {this.state.error?.message || "An unexpected error occurred"} +

Something went wrong

+

+ {process.env.NODE_ENV === "development" + ? this.state.error?.message || "An unexpected error occurred" + : "An unexpected error occurred. Please try again."}

- +
); diff --git a/apps/portal/src/components/organisms/AgentforceWidget/AgentforceWidget.tsx b/apps/portal/src/components/organisms/AgentforceWidget/AgentforceWidget.tsx index b2d2d9a6..2b2f4a0f 100644 --- a/apps/portal/src/components/organisms/AgentforceWidget/AgentforceWidget.tsx +++ b/apps/portal/src/components/organisms/AgentforceWidget/AgentforceWidget.tsx @@ -59,7 +59,9 @@ export function AgentforceWidget({ showFloatingButton = true }: AgentforceWidget useEffect(() => { // Skip if not configured if (!scriptUrl || !orgId || !deploymentId || !baseSiteUrl || !scrt2Url) { - setError("Agentforce widget is not configured. Please set the required environment variables."); + setError( + "Agentforce widget is not configured. Please set the required environment variables." + ); return; } @@ -77,12 +79,9 @@ export function AgentforceWidget({ showFloatingButton = true }: AgentforceWidget try { if (window.embeddedservice_bootstrap) { window.embeddedservice_bootstrap.settings.language = "en"; - window.embeddedservice_bootstrap.init( - orgId, - deploymentId, - baseSiteUrl, - { scrt2URL: scrt2Url } - ); + window.embeddedservice_bootstrap.init(orgId, deploymentId, baseSiteUrl, { + scrt2URL: scrt2Url, + }); setIsLoaded(true); } } catch (err) { @@ -121,7 +120,7 @@ export function AgentforceWidget({ showFloatingButton = true }: AgentforceWidget
{isOpen && ( -
-
-

AI Assistant

-

Loading...

+
+
+

AI Assistant

+

Loading...

-
+
)} @@ -149,4 +148,3 @@ export function AgentforceWidget({ showFloatingButton = true }: AgentforceWidget // Once loaded, Salesforce handles the UI return null; } - diff --git a/apps/portal/src/components/organisms/AppShell/Header.tsx b/apps/portal/src/components/organisms/AppShell/Header.tsx index 47b539df..dda7cf0a 100644 --- a/apps/portal/src/components/organisms/AppShell/Header.tsx +++ b/apps/portal/src/components/organisms/AppShell/Header.tsx @@ -22,7 +22,7 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
+ )}
@@ -46,72 +49,81 @@ export function PersonalInfoCard({
- -
-

- {data.firstname || Not provided} + +

+

+ {data.firstname || ( + Not provided + )} +

+

+ Name cannot be changed from the portal.

-

Name cannot be changed from the portal.

- -
-

- {data.lastname || Not provided} + +

+

+ {data.lastname || ( + Not provided + )} +

+

+ Name cannot be changed from the portal.

-

Name cannot be changed from the portal.

- + {isEditing ? ( - onChange("email", e.target.value)} - className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" /> ) : ( -
+
-

{data.email}

+

{data.email}

-

Email can be updated from the portal.

+

+ Email can be updated from the portal. +

)}
{isEditing && ( -
- - + Save Changes +
)}
diff --git a/apps/portal/src/features/account/views/ProfileContainer.tsx b/apps/portal/src/features/account/views/ProfileContainer.tsx index f0b82d95..8d373b17 100644 --- a/apps/portal/src/features/account/views/ProfileContainer.tsx +++ b/apps/portal/src/features/account/views/ProfileContainer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Skeleton } from "@/components/atoms/loading-skeleton"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { @@ -16,6 +16,7 @@ import { useProfileEdit } from "@/features/account/hooks/useProfileEdit"; import { AddressForm } from "@/features/catalog/components/base/AddressForm"; import { Button } from "@/components/atoms/button"; import { useAddressEdit } from "@/features/account/hooks/useAddressEdit"; +import { PageLayout } from "@/components/templates"; export default function ProfileContainer() { const { user } = useAuthStore(); @@ -23,7 +24,7 @@ export default function ProfileContainer() { const [error, setError] = useState(null); const [editingProfile, setEditingProfile] = useState(false); const [editingAddress, setEditingAddress] = useState(false); - const hasLoadedRef = useRef(false); + const [reloadKey, setReloadKey] = useState(0); const profile = useProfileEdit({ email: user?.email || "", @@ -42,32 +43,35 @@ export default function ProfileContainer() { phoneCountryCode: "", }); - useEffect(() => { - // Only load data once on mount - if (hasLoadedRef.current) return; - hasLoadedRef.current = true; + // Extract stable setValue functions to avoid infinite re-render loop. + // The hook objects (address, profile) are recreated every render, but + // the setValue callbacks inside them are stable (memoized with useCallback). + const setAddressValue = address.setValue; + const setProfileValue = profile.setValue; + const loadProfile = useCallback(() => { void (async () => { try { + setError(null); setLoading(true); const [addr, prof] = await Promise.all([ accountService.getAddress().catch(() => null), accountService.getProfile().catch(() => null), ]); if (addr) { - address.setValue("address1", addr.address1 ?? ""); - address.setValue("address2", addr.address2 ?? ""); - address.setValue("city", addr.city ?? ""); - address.setValue("state", addr.state ?? ""); - address.setValue("postcode", addr.postcode ?? ""); - address.setValue("country", addr.country ?? ""); - address.setValue("countryCode", addr.countryCode ?? ""); - address.setValue("phoneNumber", addr.phoneNumber ?? ""); - address.setValue("phoneCountryCode", addr.phoneCountryCode ?? ""); + setAddressValue("address1", addr.address1 ?? ""); + setAddressValue("address2", addr.address2 ?? ""); + setAddressValue("city", addr.city ?? ""); + setAddressValue("state", addr.state ?? ""); + setAddressValue("postcode", addr.postcode ?? ""); + setAddressValue("country", addr.country ?? ""); + setAddressValue("countryCode", addr.countryCode ?? ""); + setAddressValue("phoneNumber", addr.phoneNumber ?? ""); + setAddressValue("phoneCountryCode", addr.phoneCountryCode ?? ""); } if (prof) { - profile.setValue("email", prof.email || ""); - profile.setValue("phonenumber", prof.phonenumber || ""); + setProfileValue("email", prof.email || ""); + setProfileValue("phonenumber", prof.phonenumber || ""); useAuthStore.setState(state => ({ ...state, user: state.user @@ -80,26 +84,35 @@ export default function ProfileContainer() { })); } } catch (e) { + // Keep message customer-safe (no internal details) setError(e instanceof Error ? e.message : "Failed to load profile data"); } finally { setLoading(false); } })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [setAddressValue, setProfileValue]); + + useEffect(() => { + loadProfile(); + }, [loadProfile, reloadKey]); if (loading) { return ( -
-
-
-
+ } + title="Profile" + description="Manage your account information" + loading + > +
+
+
-
-
+
+
-
+
@@ -112,7 +125,7 @@ export default function ProfileContainer() { ))}
-
+
@@ -121,25 +134,25 @@ export default function ProfileContainer() {
-
+
-
-
+
+
-
-
+
+
-
+
-
+
@@ -154,150 +167,251 @@ export default function ProfileContainer() {
-
+ ); } return ( -
-
- {error && ( - - {error} - - )} + } + title="Profile" + description="Manage your account information" + error={error} + onRetry={() => setReloadKey(k => k + 1)} + > + {error && ( + + {error} + + )} -
-
-
-
- -

Personal Information

+
+
+
+
+ +

Personal Information

+
+ {!editingProfile && ( + + )} +
+
+ +
+
+
+ +
+

+ {user?.firstname || ( + Not provided + )} +

+

+ Name cannot be changed from the portal. +

- {!editingProfile && ( - +
+
+ +
+

+ {user?.lastname || ( + Not provided + )} +

+

+ Name cannot be changed from the portal. +

+
+
+
+ + {editingProfile ? ( + profile.setValue("email", e.target.value)} + 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" + /> + ) : ( +
+
+

{user?.email}

+
+

+ Email can be updated from the portal. +

+
)}
+ +
+ +
+

+ {user?.sfNumber || ( + Not available + )} +

+

Customer number is read-only.

+
+
+ +
+ +
+

+ {user?.dateOfBirth || ( + Not provided + )} +

+

+ Date of birth is stored in billing profile. +

+
+
+
+ + {editingProfile ? ( + profile.setValue("phonenumber", e.target.value)} + placeholder="+81 XX-XXXX-XXXX" + 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" + /> + ) : ( +

+ {user?.phonenumber || ( + Not provided + )} +

+ )} +
+ +
+ +
+

+ {user?.gender || ( + Not provided + )} +

+

+ Gender is stored in billing profile. +

+
+
-
-
-
- -
-

- {user?.firstname || Not provided} -

-

- Name cannot be changed from the portal. -

-
-
-
- -
-

- {user?.lastname || Not provided} -

-

- Name cannot be changed from the portal. -

-
-
-
- - {editingProfile ? ( - profile.setValue("email", e.target.value)} - className="block w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" - /> - ) : ( -
-
-

{user?.email}

-
-

- Email can be updated from the portal. -

-
- )} -
- -
- -
-

- {user?.sfNumber || Not available} -

-

Customer number is read-only.

-
-
- -
- -
-

- {user?.dateOfBirth || ( - Not provided - )} -

-

- Date of birth is stored in billing profile. -

-
-
-
- - {editingProfile ? ( - profile.setValue("phonenumber", e.target.value)} - placeholder="+81 XX-XXXX-XXXX" - className="block w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" - /> - ) : ( -

- {user?.phonenumber || ( - Not provided - )} -

- )} -
- -
- -
-

- {user?.gender || Not provided} -

-

Gender is stored in billing profile.

-
-
+ {editingProfile && ( +
+ +
+ )} +
+
- {editingProfile && ( -
+
+
+
+
+ +

Address Information

+
+ {!editingAddress && ( + + )} +
+
+ +
+ {editingAddress ? ( +
+ { + address.setValue("address1", a.address1 ?? ""); + address.setValue("address2", a.address2 ?? ""); + address.setValue("city", a.city ?? ""); + address.setValue("state", a.state ?? ""); + address.setValue("postcode", a.postcode ?? ""); + address.setValue("country", a.country ?? ""); + address.setValue("countryCode", a.countryCode ?? ""); + address.setValue("phoneNumber", a.phoneNumber ?? ""); + address.setValue("phoneCountryCode", a.phoneCountryCode ?? ""); + }} + title="Mailing Address" + /> +
- )} -
-
- -
-
-
-
- -

Address Information

-
- {!editingAddress && ( - + {address.submitError && ( + + {address.submitError} + )}
-
- -
- {editingAddress ? ( -
- { - address.setValue("address1", a.address1 ?? ""); - address.setValue("address2", a.address2 ?? ""); - address.setValue("city", a.city ?? ""); - address.setValue("state", a.state ?? ""); - address.setValue("postcode", a.postcode ?? ""); - address.setValue("country", a.country ?? ""); - address.setValue("countryCode", a.countryCode ?? ""); - address.setValue("phoneNumber", a.phoneNumber ?? ""); - address.setValue("phoneCountryCode", a.phoneCountryCode ?? ""); - }} - title="Mailing Address" - /> -
+ ) : ( +
+ {address.values.address1 || address.values.city ? ( +
+
+ {address.values.address1 && ( +

{address.values.address1}

+ )} + {address.values.address2 && ( +

{address.values.address2}

+ )} +

+ {[address.values.city, address.values.state, address.values.postcode] + .filter(Boolean) + .join(", ")} +

+

{address.values.country}

+
+
+ ) : ( +
+ +

No address on file

-
- {address.submitError && ( - - {address.submitError} - - )} -
- ) : ( -
- {address.values.address1 || address.values.city ? ( -
-
- {address.values.address1 && ( -

{address.values.address1}

- )} - {address.values.address2 && ( -

{address.values.address2}

- )} -

- {[address.values.city, address.values.state, address.values.postcode] - .filter(Boolean) - .join(", ")} -

-

{address.values.country}

-
-
- ) : ( -
- -

No address on file

- -
- )} -
- )} -
+ )} +
+ )}
-
+ ); } diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx index ea957bc4..fbc7a06a 100644 --- a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx +++ b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx @@ -89,7 +89,11 @@ export function PasswordResetForm({ }, }); - // Get the current form based on mode + // Extract stable reset functions to avoid unnecessary effect runs. + // The form objects change when internal state changes, but reset is stable. + const requestFormReset = requestForm.reset; + const resetFormReset = resetForm.reset; + // Handle errors from auth hooks useEffect(() => { if (error) { @@ -100,15 +104,19 @@ export function PasswordResetForm({ // Clear errors when switching modes useEffect(() => { clearError(); - requestForm.reset(); - resetForm.reset(); - }, [mode, clearError, requestForm, resetForm]); + requestFormReset(); + resetFormReset(); + }, [mode, clearError, requestFormReset, resetFormReset]); if (mode === "request") { return (
void requestForm.handleSubmit(event)} className="space-y-4"> - + Back to login @@ -150,7 +158,11 @@ export function PasswordResetForm({ return (
void resetForm.handleSubmit(event)} className="space-y-4"> - + - + Back to login diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx index 7eb2df42..5fb4d636 100644 --- a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx +++ b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx @@ -32,7 +32,12 @@ interface SetPasswordFormProps { className?: string; } -export function SetPasswordForm({ email = "", onSuccess, onError, className = "" }: SetPasswordFormProps) { +export function SetPasswordForm({ + email = "", + onSuccess, + onError, + className = "", +}: SetPasswordFormProps) { const { setPassword, loading, error, clearError } = useWhmcsLink(); const form = useZodForm({ @@ -60,9 +65,14 @@ export function SetPasswordForm({ email = "", onSuccess, onError, className = "" if (error) onError?.(error); }, [error, onError]); + // Extract stable setValue to avoid re-running effect on every render. + // The form object is recreated each render, but setValue is memoized. + const formSetValue = form.setValue; + const formEmailValue = form.values.email; + useEffect(() => { - if (email && email !== form.values.email) form.setValue("email", email); - }, [email, form]); + if (email && email !== formEmailValue) formSetValue("email", email); + }, [email, formEmailValue, formSetValue]); return ( void form.handleSubmit(e)} className={`space-y-5 ${className}`}> @@ -73,12 +83,18 @@ export function SetPasswordForm({ email = "", onSuccess, onError, className = "" onChange={e => !isEmailProvided && form.setValue("email", e.target.value)} disabled={isLoading || isEmailProvided} readOnly={isEmailProvided} - className={isEmailProvided ? "bg-gray-50 text-gray-600" : ""} + className={isEmailProvided ? "bg-muted text-muted-foreground" : ""} /> - {isEmailProvided &&

Verified during account transfer

} + {isEmailProvided && ( +

Verified during account transfer

+ )}
- +
-
-
+
+
- {label} + + {label} +
{requirements.map(r => (
- {r.met ? "✓" : "○"} - {r.label} + + {r.met ? "✓" : "○"} + + {r.label}
))}
)} - + {form.values.confirmPassword && ( -

+

{passwordsMatch ? "✓ Passwords match" : "✗ Passwords do not match"}

)} {(error || form.errors._form) && {form.errors._form || error}} -
- Back to login + + Back to login +
); diff --git a/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx b/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx index 2799242e..1070078e 100644 --- a/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx @@ -65,23 +65,19 @@ export function MultiStepForm({ transition-all duration-200 ${ isCompleted - ? "bg-green-500 text-white" + ? "bg-success text-success-foreground" : isCurrent - ? "bg-blue-600 text-white ring-4 ring-blue-100" - : "bg-gray-200 text-gray-500" + ? "bg-primary text-primary-foreground ring-4 ring-primary/15" + : "bg-muted text-muted-foreground" } `} > - {isCompleted ? ( - - ) : ( - idx + 1 - )} + {isCompleted ? : idx + 1}
{idx < totalSteps - 1 && (
)} @@ -92,15 +88,15 @@ export function MultiStepForm({ {/* Step Title & Description */}
-

{step?.title}

-

{step?.description}

+

{step?.title}

+

{step?.description}

{/* Step Content */}
{step?.content}
{/* Navigation Buttons */} -
+
diff --git a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx index 3d3c511d..8df07c01 100644 --- a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx @@ -264,7 +264,7 @@ export function SignupForm({ onSuccess, onError, className = "" }: SignupFormPro return (
-
+
{error && ( - {error} + + {error} + )} -
-

+

+

Already have an account?{" "} Sign in

-

+

Existing customer?{" "} Migrate your account diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx index a03ca98f..44acf5e1 100644 --- a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx @@ -34,7 +34,7 @@ export function AccountStep({ form }: AccountStepProps) { return (

{/* Customer Number - Highlighted */} -
+
setValue("sfNumber", e.target.value)} onBlur={() => setTouchedField("sfNumber")} placeholder="e.g., AST-123456" - className="bg-white" autoFocus /> @@ -149,12 +148,12 @@ export function AccountStep({ form }: AccountStepProps) { onChange={e => setValue("gender", e.target.value || undefined)} onBlur={() => setTouchedField("gender")} className={[ - "flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm", - "ring-offset-background placeholder:text-gray-500 focus-visible:outline-none", - "focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2", + "flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm", + "ring-offset-background 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", getError("gender") - ? "border-red-500 focus-visible:ring-red-500 focus-visible:ring-offset-2" + ? "border-destructive focus-visible:ring-destructive focus-visible:ring-offset-2" : "", ].join(" ")} aria-invalid={Boolean(getError("gender")) || undefined} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx index 1ed55bda..35246afa 100644 --- a/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx @@ -1,6 +1,6 @@ /** * Address Step - Japanese address input for WHMCS - * + * * Field mapping to WHMCS: * - postcode → postcode * - state → state (prefecture) @@ -94,13 +94,13 @@ export function AddressStep({ form }: AddressStepProps) { updateAddress("city", e.target.value)} + onChange={e => updateAddress("city", e.target.value)} onBlur={markTouched} placeholder="Shibuya-ku" autoComplete="address-level2" @@ -135,7 +135,7 @@ export function AddressStep({ form }: AddressStepProps) { updateAddress("address1", e.target.value)} + onChange={e => updateAddress("address1", e.target.value)} onBlur={markTouched} placeholder="3-8-2 Higashi Azabu" autoComplete="address-line1" @@ -151,14 +151,14 @@ export function AddressStep({ form }: AddressStepProps) { updateAddress("address2", e.target.value)} + onChange={e => updateAddress("address2", e.target.value)} onBlur={markTouched} placeholder="3F Azabu Maruka Bldg" autoComplete="address-line2" /> -

+

Please input your address in Japan. This will be used for service delivery and setup.

diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx index 2a5e3108..7defa03b 100644 --- a/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx @@ -38,11 +38,7 @@ export function PasswordStep({ form }: PasswordStepProps) { aria-hidden="true" /> - +
-
-
+
+
- {label} + + {label} +
{requirements.map(r => (
- + {r.met ? "✓" : "○"} - {r.label} + {r.label}
))}
@@ -92,7 +95,7 @@ export function PasswordStep({ form }: PasswordStepProps) { {values.confirmPassword && ( -

+

{passwordsMatch ? "✓ Passwords match" : "✗ Passwords do not match"}

)} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx index 5bb1d26d..bbde5d74 100644 --- a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx +++ b/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx @@ -48,11 +48,11 @@ function ReadyMessage({ errors }: { errors: FormErrors }) { if (hasErrors) return null; return ( -
+
🚀

Ready to create your account!

-

+

Click "Create Account" below to complete your registration.

@@ -108,50 +108,50 @@ export function ReviewStep({ form }: ReviewStepProps) { return (
{/* Account Summary */} -
-

- +
+

+ Account Summary

-
-
Customer Number
-
{values.sfNumber}
+
+
Customer Number
+
{values.sfNumber}
-
-
Name
-
+
+
Name
+
{values.firstName} {values.lastName}
-
-
Email
-
{values.email}
+
+
Email
+
{values.email}
-
-
Phone
-
+
+
Phone
+
{values.phoneCountryCode} {values.phone}
{values.company && ( -
-
Company
-
{values.company}
+
+
Company
+
{values.company}
)} {values.dateOfBirth && ( -
-
Date of Birth
-
{values.dateOfBirth}
+
+
Date of Birth
+
{values.dateOfBirth}
)} {values.gender && ( -
-
Gender
-
{values.gender}
+
+
Gender
+
{values.gender}
)}
@@ -159,34 +159,34 @@ export function ReviewStep({ form }: ReviewStepProps) { {/* Address Summary */} {address?.address1 && ( -
-

- +
+

+ 📍 Delivery Address

-

{formattedAddress}

+

{formattedAddress}

)} {/* Terms & Conditions */} -
-

Terms & Agreements

+
+

Terms & Agreements

-
diff --git a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx index 28ae0f15..8d63a4e7 100644 --- a/apps/portal/src/features/auth/views/LinkWhmcsView.tsx +++ b/apps/portal/src/features/auth/views/LinkWhmcsView.tsx @@ -20,21 +20,25 @@ export function LinkWhmcsView() { >
{/* What transfers */} -
-

What gets transferred:

-
    +
    +

    What gets transferred:

    +
      {MIGRATION_TRANSFER_ITEMS.map((item, i) => (
    • - {item} + {item}
    • ))}
    {/* Form */} -
    -

    Enter Legacy Portal Credentials

    -

    Use your previous Assist Solutions portal email and password.

    +
    +

    + Enter Legacy Portal Credentials +

    +

    + Use your previous Assist Solutions portal email and password. +

    { if (result.needsPasswordSet) { @@ -48,31 +52,40 @@ export function LinkWhmcsView() { {/* Links */}
    - - New customer? Create account + + New customer?{" "} + + Create account + - - Already transferred? Sign in + + Already transferred?{" "} + + Sign in +
    {/* Steps */} -
    -

    How it works

    +
    +

    How it works

      {MIGRATION_STEPS.map((step, i) => (
    1. - + {i + 1} - {step} + {step}
    2. ))}
    -

    - Need help? Contact support +

    + Need help?{" "} + + Contact support +

    diff --git a/apps/portal/src/features/billing/views/InvoiceDetail.tsx b/apps/portal/src/features/billing/views/InvoiceDetail.tsx index 959e9c24..c24ce1a4 100644 --- a/apps/portal/src/features/billing/views/InvoiceDetail.tsx +++ b/apps/portal/src/features/billing/views/InvoiceDetail.tsx @@ -51,9 +51,9 @@ export function InvoiceDetailContainer() { title="Invoice" description="Invoice details and actions" > -
    +
    -
    +
    @@ -100,33 +100,23 @@ export function InvoiceDetailContainer() { } return ( -
    -
    - {/* Navigation */} -
    - - - - - Back to Invoices - -
    - - {/* Main Invoice Card */} -
    + } + title={`Invoice #${invoice.id}`} + description="Invoice details and actions" + breadcrumbs={[ + { label: "Billing", href: "/billing/invoices" }, + { label: "Invoices", href: "/billing/invoices" }, + { label: `#${invoice.id}` }, + ]} + actions={ + + Back to invoices + + } + > +
    +
    handleCreateSsoLink("pay")} /> - {/* Success Banner for Paid Invoices */} {invoice.status === "Paid" && ( -
    +
    - +
    -

    Payment Received

    -

    +

    Payment received

    +

    Paid on {invoice.paidDate || invoice.issuedAt}

    @@ -152,14 +141,10 @@ export function InvoiceDetailContainer() {
    )} - {/* Content */}
    - {/* Invoice Items */} - - {/* Invoice Summary - Full Width */} -
    +
    -
    + ); } diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index 67b2721e..db5cd99e 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -221,7 +221,7 @@ export function AddressConfirmation({ return wrap(
    - Loading address information... + Loading address information...
    ); } @@ -250,9 +250,9 @@ export function AddressConfirmation({ <>
    - +
    -

    +

    {isInternetOrder ? "Installation Address" : billingInfo.isComplete @@ -275,7 +275,9 @@ export function AddressConfirmation({ {editing ? (
    - + (prev ? { ...prev, address1: e.target.value } : null)); }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors" placeholder="123 Main Street" required />
    -
    - + (prev ? { ...prev, city: e.target.value } : null)); }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors" placeholder="Tokyo" />
    -
    - + (prev ? { ...prev, postcode: e.target.value } : null)); }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors" placeholder="100-0001" />
    - +
    -

    {option.title}

    +

    + {option.title} +