fix: resolve BFF TypeScript errors and improve mobile UX

BFF fixes:
- Fix pino-http type import by using Params from nestjs-pino
- Use Prisma-generated AuditAction enum instead of local duplicate
- Add null check for sfAccountId in mapping mapper

Portal mobile UX improvements:
- DataTable: Add responsive card view for mobile with stacked layout
- Header: Increase touch targets to 44px minimum, better spacing
- PageLayout: Optimize padding and make breadcrumbs scrollable
- PublicShell: Add iOS safe area support, slide animation, language
  switcher and sign-in button visible in mobile header

Also removes "Trusted by Leading Companies" section from AboutUsView.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Temuuleenn 2026-02-04 18:23:58 +09:00
parent 4c724da7ae
commit df742e50bc
8 changed files with 299 additions and 255 deletions

View File

@ -1,12 +1,13 @@
import { Global, Module } from "@nestjs/common";
import { LoggerModule } from "nestjs-pino";
import type { Options as PinoHttpOptions } from "pino-http";
import { LoggerModule, type Params } from "nestjs-pino";
import type { IncomingMessage, ServerResponse } from "http";
const prettyLogsEnabled =
process.env["PRETTY_LOGS"] === "true" || process.env["NODE_ENV"] !== "production";
// Build pinoHttp config - extracted to avoid type issues with exactOptionalPropertyTypes
const pinoHttpConfig: PinoHttpOptions = {
// Using NonNullable to extract the pinoHttp type from Params
const pinoHttpConfig: NonNullable<Params["pinoHttp"]> = {
level: process.env["LOG_LEVEL"] || "info",
name: process.env["APP_NAME"] || "customer-portal-bff",
/**
@ -18,13 +19,13 @@ const pinoHttpConfig: PinoHttpOptions = {
* This keeps production logs focused on actionable events while still
* allowing full request logging by setting LOG_LEVEL=debug.
*/
customLogLevel: (_req, res, err) => {
customLogLevel: (_req: IncomingMessage, res: ServerResponse, err: Error | undefined) => {
if (err || (res?.statusCode && res.statusCode >= 500)) return "error";
if (res?.statusCode && res.statusCode >= 400) return "warn";
return "debug";
},
autoLogging: {
ignore: req => {
ignore: (req: IncomingMessage) => {
const url = req.url || "";
return (
url.includes("/health") ||

View File

@ -1,35 +1,12 @@
import { Injectable, Inject } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { Prisma, AuditAction } from "@prisma/client";
import { PrismaService } from "../database/prisma.service.js";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { extractClientIp, extractUserAgent } from "@bff/core/http/request-context.util.js";
export enum AuditAction {
LOGIN_SUCCESS = "LOGIN_SUCCESS",
LOGIN_FAILED = "LOGIN_FAILED",
LOGIN_OTP_SENT = "LOGIN_OTP_SENT",
LOGIN_OTP_VERIFIED = "LOGIN_OTP_VERIFIED",
LOGIN_OTP_FAILED = "LOGIN_OTP_FAILED",
LOGIN_SESSION_INVALIDATED = "LOGIN_SESSION_INVALIDATED",
LOGOUT = "LOGOUT",
SIGNUP = "SIGNUP",
PASSWORD_RESET = "PASSWORD_RESET",
PASSWORD_CHANGE = "PASSWORD_CHANGE",
ACCOUNT_LOCKED = "ACCOUNT_LOCKED",
ACCOUNT_UNLOCKED = "ACCOUNT_UNLOCKED",
PROFILE_UPDATE = "PROFILE_UPDATE",
MFA_ENABLED = "MFA_ENABLED",
MFA_DISABLED = "MFA_DISABLED",
API_ACCESS = "API_ACCESS",
SYSTEM_MAINTENANCE = "SYSTEM_MAINTENANCE",
// Address reconciliation (Salesforce dual-write)
ADDRESS_UPDATE = "ADDRESS_UPDATE",
ADDRESS_UPDATE_PARTIAL = "ADDRESS_UPDATE_PARTIAL",
ADDRESS_RECONCILE_QUEUED = "ADDRESS_RECONCILE_QUEUED",
ADDRESS_RECONCILE_SUCCESS = "ADDRESS_RECONCILE_SUCCESS",
ADDRESS_RECONCILE_FAILED = "ADDRESS_RECONCILE_FAILED",
}
// Re-export AuditAction from Prisma for consumers
export { AuditAction } from "@prisma/client";
export interface AuditLogData {
userId?: string | undefined;

View File

@ -14,6 +14,9 @@ import type { UserIdMapping } from "@bff/modules/id-mappings/domain/index.js";
* Maps Prisma IdMapping entity to Domain UserIdMapping type
*/
export function mapPrismaMappingToDomain(mapping: PrismaIdMapping): UserIdMapping {
if (!mapping.sfAccountId) {
throw new Error(`IdMapping for user ${mapping.userId} is missing sfAccountId`);
}
return {
id: mapping.userId,
userId: mapping.userId,

View File

@ -1,11 +1,16 @@
import type { ReactNode } from "react";
import { EmptyState } from "@/components/atoms/empty-state";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
interface Column<T> {
key: string;
header: string;
render: (item: T) => ReactNode;
className?: string;
/** If true, this column will be emphasized in mobile card view (shown first/larger) */
primary?: boolean;
/** If true, this column will be hidden in mobile card view */
hideOnMobile?: boolean;
}
interface DataTableProps<T> {
@ -18,6 +23,8 @@ interface DataTableProps<T> {
};
onRowClick?: (item: T) => void;
className?: string;
/** Force table view even on mobile (not recommended for UX) */
forceTableView?: boolean;
}
export function DataTable<T extends { id: number | string }>({
@ -26,6 +33,7 @@ export function DataTable<T extends { id: number | string }>({
emptyState,
onRowClick,
className = "",
forceTableView = false,
}: DataTableProps<T>) {
if (data.length === 0 && emptyState) {
return (
@ -38,43 +46,120 @@ export function DataTable<T extends { id: number | string }>({
);
}
// Separate primary column for mobile card header
const primaryColumn = columns.find(col => col.primary);
const mobileColumns = columns.filter(col => !col.hideOnMobile && !col.primary);
return (
<div className="overflow-x-auto">
<table className={`min-w-full divide-y divide-border ${className}`}>
<thead className="bg-muted/50">
<tr>
{columns.map(column => (
<th
key={column.key}
className={`px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider ${
column.className || ""
}`}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{data.map(item => (
<tr
<>
{/* Mobile Card View - Hidden on md and up */}
{!forceTableView && (
<div className="md:hidden space-y-3">
{data.map((item, index) => (
<div
key={item.id}
className={`hover:bg-muted/30 transition-colors duration-[var(--cp-transition-fast)] ${onRowClick ? "cursor-pointer" : ""}`}
className={`
bg-card border border-border rounded-xl p-4
shadow-[var(--cp-shadow-1)]
transition-all duration-[var(--cp-duration-fast)]
active:scale-[0.98] active:shadow-none
${onRowClick ? "cursor-pointer active:bg-muted/50" : ""}
`}
onClick={() => onRowClick?.(item)}
role={onRowClick ? "button" : undefined}
tabIndex={onRowClick ? 0 : undefined}
onKeyDown={e => {
if (onRowClick && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
onRowClick(item);
}
}}
style={{
animationDelay: `${index * 50}ms`,
}}
>
{/* Primary content row with chevron if clickable */}
<div className="flex items-center justify-between gap-3 mb-3">
<div className="min-w-0 flex-1">
{primaryColumn ? (
<div className="font-semibold text-foreground">
{primaryColumn.render(item)}
</div>
) : (
<div className="font-semibold text-foreground">
{columns[0]?.render(item)}
</div>
)}
</div>
{onRowClick && (
<ChevronRightIcon className="h-5 w-5 text-muted-foreground/50 flex-shrink-0" />
)}
</div>
{/* Secondary columns as label-value pairs */}
<div className="space-y-2">
{mobileColumns.map((column, colIndex) => {
// Skip the first column if no primary is defined (already shown above)
if (!primaryColumn && colIndex === 0 && column === columns[0]) return null;
return (
<div
key={column.key}
className="flex items-center justify-between gap-4 text-sm"
>
<span className="text-muted-foreground font-medium flex-shrink-0">
{column.header}
</span>
<span className="text-foreground text-right min-w-0 truncate">
{column.render(item)}
</span>
</div>
);
})}
</div>
</div>
))}
</div>
)}
{/* Desktop Table View - Hidden below md */}
<div className={`${forceTableView ? "" : "hidden md:block"} overflow-x-auto`}>
<table className={`min-w-full divide-y divide-border ${className}`}>
<thead className="bg-muted/50">
<tr>
{columns.map(column => (
<td
<th
key={column.key}
className={`px-6 py-4 whitespace-nowrap ${column.className || ""}`}
className={`px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider ${
column.className || ""
}`}
>
{column.render(item)}
</td>
{column.header}
</th>
))}
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody className="bg-card divide-y divide-border">
{data.map(item => (
<tr
key={item.id}
className={`hover:bg-muted/30 transition-colors duration-[var(--cp-transition-fast)] ${onRowClick ? "cursor-pointer" : ""}`}
onClick={() => onRowClick?.(item)}
>
{columns.map(column => (
<td
key={column.key}
className={`px-6 py-4 whitespace-nowrap ${column.className || ""}`}
>
{column.render(item)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</>
);
}

View File

@ -40,10 +40,11 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
return (
<div className="relative z-40 bg-header border-b border-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-2 sm:gap-3 px-3 sm:px-6">
{/* Mobile menu button - 44px minimum touch target */}
<button
type="button"
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"
className="md:hidden flex items-center justify-center w-11 h-11 -ml-1 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted/80 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
onClick={onMenuClick}
aria-label="Open navigation"
>
@ -52,27 +53,35 @@ export const Header = memo(function Header({ onMenuClick, user, profileReady }:
<div className="flex-1" />
<div className="flex items-center gap-1">
{/* Right side actions */}
<div className="flex items-center gap-1 sm:gap-2">
{/* Notification bell */}
<NotificationBell />
{/* Help link - visible on larger screens */}
<Link
href="/account/support"
prefetch
aria-label="Help"
className="hidden sm:inline-flex p-2.5 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-all duration-200"
className="hidden sm:inline-flex items-center justify-center w-11 h-11 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted/80 transition-all duration-200"
title="Support Center"
>
<QuestionMarkCircleIcon className="h-5 w-5" />
</Link>
{/* Profile link - enhanced for mobile */}
<Link
href="/account/settings"
prefetch
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"
className="group flex items-center gap-2 sm:gap-2.5 px-2 sm:px-3 py-1.5 min-h-[44px] text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted/80 rounded-xl transition-all duration-200"
>
<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">
<div className="h-8 w-8 sm:h-7 sm: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 group-hover:shadow-md transition-shadow">
{initials}
</div>
{/* Show truncated name on mobile, full name on larger screens */}
<span className="hidden xs:inline sm:hidden max-w-[80px] truncate text-sm">
{displayName.split(" ")[0]}
</span>
<span className="hidden sm:inline">{displayName}</span>
</Link>
</div>

View File

@ -33,34 +33,41 @@ export function PageLayout({
children,
}: PageLayoutProps) {
return (
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-2xl)]" suppressHydrationWarning>
<div
className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)] md:py-[var(--cp-space-2xl)]"
suppressHydrationWarning
>
<div
className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-lg)] sm:px-[var(--cp-page-padding)] md:px-8"
className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8"
suppressHydrationWarning
>
{/* Breadcrumbs */}
{/* Breadcrumbs - scrollable on mobile */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav className="mb-[var(--cp-space-lg)]" aria-label="Breadcrumb" suppressHydrationWarning>
<nav
className="mb-[var(--cp-space-md)] sm:mb-[var(--cp-space-lg)] -mx-[var(--cp-space-md)] px-[var(--cp-space-md)] sm:mx-0 sm:px-0 overflow-x-auto scrollbar-none"
aria-label="Breadcrumb"
suppressHydrationWarning
>
<ol
className="flex items-center space-x-2 text-sm text-muted-foreground"
className="flex items-center space-x-1 sm:space-x-2 text-sm text-muted-foreground whitespace-nowrap"
suppressHydrationWarning
>
{breadcrumbs.map((item, index) => (
<li key={index} className="flex items-center" suppressHydrationWarning>
<li key={index} className="flex items-center flex-shrink-0" suppressHydrationWarning>
{index > 0 && (
<ChevronRightIcon className="h-4 w-4 mx-2 text-muted-foreground/50" />
<ChevronRightIcon className="h-4 w-4 mx-1 sm:mx-2 text-muted-foreground/50 flex-shrink-0" />
)}
{item.href ? (
<Link
href={item.href}
className="hover:text-foreground transition-colors duration-200 truncate"
className="hover:text-foreground transition-colors duration-200 py-1 px-0.5 -mx-0.5 rounded"
suppressHydrationWarning
>
{item.label}
</Link>
) : (
<span
className="text-foreground font-medium truncate"
className="text-foreground font-medium py-1"
aria-current="page"
suppressHydrationWarning
>
@ -75,17 +82,18 @@ export function PageLayout({
{/* Header */}
<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"
className="mb-[var(--cp-space-lg)] sm:mb-[var(--cp-space-xl)] md:mb-[var(--cp-space-2xl)] pb-[var(--cp-space-lg)] sm:pb-[var(--cp-space-xl)] md:pb-[var(--cp-space-2xl)] border-b border-border"
suppressHydrationWarning
>
<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)]"
className="flex flex-col gap-[var(--cp-space-md)] sm:gap-[var(--cp-space-lg)]"
suppressHydrationWarning
>
{/* Title row */}
<div className="flex items-start min-w-0 flex-1" suppressHydrationWarning>
{icon && (
<div
className="h-8 w-8 text-primary mr-[var(--cp-space-lg)] flex-shrink-0 mt-1"
className="h-7 w-7 sm:h-8 sm:w-8 text-primary mr-[var(--cp-space-md)] sm:mr-[var(--cp-space-lg)] flex-shrink-0 mt-0.5"
suppressHydrationWarning
>
{icon}
@ -93,14 +101,14 @@ export function PageLayout({
)}
<div className="min-w-0 flex-1" suppressHydrationWarning>
<h1
className="text-2xl sm:text-3xl font-bold text-foreground leading-tight"
className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight"
suppressHydrationWarning
>
{title}
</h1>
{description && (
<p
className="text-sm sm:text-base text-muted-foreground mt-1 leading-relaxed"
className="text-sm text-muted-foreground mt-1 leading-relaxed line-clamp-2 sm:line-clamp-none"
suppressHydrationWarning
>
{description}
@ -108,8 +116,13 @@ export function PageLayout({
)}
</div>
</div>
{/* Actions - full width on mobile, stacks buttons */}
{actions && (
<div className="flex-shrink-0 w-full sm:w-auto" suppressHydrationWarning>
<div
className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 w-full sm:w-auto [&>*]:w-full [&>*]:sm:w-auto"
suppressHydrationWarning
>
{actions}
</div>
)}
@ -117,7 +130,7 @@ export function PageLayout({
</div>
{/* Content with loading and error states */}
<div className="space-y-[var(--cp-space-2xl)]" suppressHydrationWarning>
<div className="space-y-[var(--cp-space-xl)] sm:space-y-[var(--cp-space-2xl)]" suppressHydrationWarning>
{renderPageContent(loading, error ?? undefined, children, onRetry)}
</div>
</div>
@ -142,20 +155,20 @@ function renderPageContent(
function PageLoadingState() {
return (
<div className="py-[var(--cp-space-3xl)]">
<div className="space-y-6">
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-3xl)]">
<div className="space-y-4 sm:space-y-6">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
<Skeleton className="h-6 w-36 sm:w-48" />
<Skeleton className="h-4 w-48 sm:w-64" />
</div>
</div>
<div className="space-y-4">
<div className="space-y-3 sm:space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="bg-card border border-border rounded-lg p-4 shadow-[var(--cp-shadow-1)]"
className="bg-card border border-border rounded-lg p-3 sm:p-4 shadow-[var(--cp-shadow-1)]"
>
<Skeleton className="h-4 w-1/2 mb-2" />
<Skeleton className="h-3 w-3/4" />
@ -176,7 +189,7 @@ function PageErrorState({ error, onRetry }: PageErrorStateProps) {
const errorMessage = typeof error === "string" ? error : error.message;
return (
<div className="py-[var(--cp-space-3xl)]">
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-3xl)]">
<ErrorState
title="Unable to load page"
message={errorMessage}

View File

@ -157,8 +157,8 @@ export function PublicShell({ children }: PublicShellProps) {
<div className="fixed inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/5 via-background to-background" />
<header className="sticky top-0 z-40 border-b border-border/40 bg-background/95 backdrop-blur-md supports-[backdrop-filter]:bg-background/80">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] h-16 grid grid-cols-[auto_1fr_auto] items-center gap-4">
<Link href="/" className="inline-flex items-center gap-2.5 min-w-0 group">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-page-padding)] h-16 grid grid-cols-[auto_1fr_auto] items-center gap-3 sm:gap-4">
<Link href="/" className="inline-flex items-center gap-2 sm:gap-2.5 min-w-0 group">
<span className="inline-flex items-center justify-center h-9 w-9 rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary/15">
<Logo size={20} />
</span>
@ -169,7 +169,7 @@ export function PublicShell({ children }: PublicShellProps) {
</span>
</Link>
{/* Desktop Navigation */}
{/* Desktop Navigation - hidden on mobile, nav content shows on md+ */}
<nav className="hidden md:flex items-center justify-center gap-1 sm:gap-3 text-sm font-semibold text-muted-foreground">
<div
ref={servicesDropdownRef}
@ -263,20 +263,18 @@ export function PublicShell({ children }: PublicShellProps) {
</Link>
</nav>
{/* Mobile: Language indicator + hamburger */}
<div className="flex md:hidden items-center justify-center">
<div className="flex items-center gap-1 text-sm text-muted-foreground mr-2">
<Globe className="h-4 w-4" />
<span className="font-medium">EN</span>
</div>
</div>
{/* Spacer for mobile only - takes center column when nav is hidden */}
<div className="md:hidden" />
<div className="flex items-center gap-3 justify-self-end">
{/* Language Selector - Desktop */}
<div className="hidden md:flex items-center gap-1 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5 sm:gap-3 justify-self-end">
{/* Language Selector - All screens */}
<button
type="button"
className="inline-flex items-center gap-1 px-2 sm:px-2.5 py-1.5 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
>
<Globe className="h-4 w-4" />
<span className="font-medium">EN</span>
</div>
</button>
{/* Auth Buttons - Desktop */}
{isAuthenticated ? (
@ -303,11 +301,28 @@ export function PublicShell({ children }: PublicShellProps) {
</div>
)}
{/* Mobile Menu Button */}
{/* Auth Button - Mobile (visible in header) */}
{isAuthenticated ? (
<Link
href="/account"
className="md:hidden inline-flex items-center justify-center h-9 px-3 rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 active:bg-primary/80 transition-colors"
>
Account
</Link>
) : (
<Link
href="/auth/login"
className="md:hidden inline-flex items-center justify-center h-9 px-3 rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow hover:bg-primary/90 active:bg-primary/80 transition-colors"
>
Sign in
</Link>
)}
{/* Mobile Menu Button - 44px touch target */}
<button
type="button"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-md hover:bg-muted/50 transition-colors"
className="md:hidden inline-flex items-center justify-center w-11 h-11 rounded-xl hover:bg-muted/50 active:bg-muted/70 transition-colors"
aria-expanded={mobileMenuOpen}
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
>
@ -317,92 +332,116 @@ export function PublicShell({ children }: PublicShellProps) {
</div>
</header>
{/* Mobile Menu Overlay - Rendered outside header to avoid stacking context issues */}
{/* Mobile Menu Overlay - Full screen with slide animation */}
{mobileMenuOpen && (
<div className="md:hidden fixed inset-0 top-16 z-50 bg-white animate-in fade-in duration-200 overflow-y-auto">
<nav className="flex flex-col p-6 space-y-2">
<div className="space-y-1">
<div
className="md:hidden fixed inset-0 top-16 z-50 bg-background animate-in slide-in-from-right-full duration-300 ease-out overflow-hidden"
style={{
// iOS safe area support
paddingBottom: "env(safe-area-inset-bottom, 0px)",
}}
>
<nav className="flex flex-col h-full overflow-y-auto overscroll-contain">
{/* Services section */}
<div className="px-4 pt-4 pb-2">
<p className="text-xs font-semibold text-primary uppercase tracking-wider px-3 py-2">
Services
</p>
{serviceItems.map(item => (
<Link
key={item.href}
href={item.href}
onClick={closeMobileMenu}
className="flex items-center gap-3 px-3 py-3 rounded-lg hover:bg-muted/50 transition-colors"
>
<div
className={`flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-lg ${item.color}`}
<div className="space-y-1">
{serviceItems.map((item, index) => (
<Link
key={item.href}
href={item.href}
onClick={closeMobileMenu}
className="flex items-center gap-3 px-3 py-3.5 rounded-xl hover:bg-muted/50 active:bg-muted/70 transition-colors"
style={{
animationDelay: `${index * 50}ms`,
}}
>
{item.icon}
</div>
<div>
<p className="text-sm font-semibold text-foreground">{item.label}</p>
<p className="text-xs text-muted-foreground">{item.desc}</p>
</div>
</Link>
))}
<div
className={`flex-shrink-0 flex items-center justify-center w-11 h-11 rounded-xl ${item.color}`}
>
{item.icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-base font-semibold text-foreground">{item.label}</p>
<p className="text-sm text-muted-foreground">{item.desc}</p>
</div>
</Link>
))}
</div>
</div>
<div className="border-t border-border/40 pt-4 space-y-1">
<Link
href="/about"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
>
About
</Link>
<Link
href="/blog"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
>
Blog
</Link>
<Link
href="/contact"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3 rounded-lg text-base font-semibold text-foreground hover:bg-muted/50 transition-colors"
>
Support
</Link>
{/* Other nav links */}
<div className="px-4 py-2 border-t border-border/40">
<div className="space-y-1">
<Link
href="/about"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3.5 rounded-xl text-base font-semibold text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
>
About
</Link>
<Link
href="/blog"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3.5 rounded-xl text-base font-semibold text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
>
Blog
</Link>
<Link
href="/contact"
onClick={closeMobileMenu}
className="flex items-center px-3 py-3.5 rounded-xl text-base font-semibold text-foreground hover:bg-muted/50 active:bg-muted/70 transition-colors"
>
Support
</Link>
</div>
</div>
<div className="border-t border-border/40 pt-4 space-y-3">
{isAuthenticated ? (
<Link
href="/account"
onClick={closeMobileMenu}
className="flex items-center justify-center rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors"
>
My Account
</Link>
) : (
<>
{/* Auth buttons - sticky at bottom */}
<div
className="mt-auto px-4 py-4 border-t border-border/40 bg-background"
style={{
// Extra bottom padding for iOS home indicator
paddingBottom: "calc(1rem + env(safe-area-inset-bottom, 0px))",
}}
>
<div className="space-y-3">
{isAuthenticated ? (
<Link
href="/auth/get-started"
href="/account"
onClick={closeMobileMenu}
className="flex items-center justify-center rounded-full bg-primary px-6 py-3 text-base font-semibold text-primary-foreground shadow hover:bg-primary/90 transition-colors"
className="flex items-center justify-center rounded-full bg-primary px-6 py-3.5 text-base font-semibold text-primary-foreground shadow-lg hover:bg-primary/90 active:bg-primary/80 transition-colors"
>
Get Started
My Account
</Link>
<Link
href="/auth/login"
onClick={closeMobileMenu}
className="flex items-center justify-center rounded-full border border-input bg-background px-6 py-3 text-base font-semibold text-foreground shadow-sm hover:bg-accent transition-colors"
>
Sign in
</Link>
</>
)}
) : (
<>
<Link
href="/auth/get-started"
onClick={closeMobileMenu}
className="flex items-center justify-center rounded-full bg-primary px-6 py-3.5 text-base font-semibold text-primary-foreground shadow-lg hover:bg-primary/90 active:bg-primary/80 transition-colors"
>
Get Started
</Link>
<Link
href="/auth/login"
onClick={closeMobileMenu}
className="flex items-center justify-center rounded-full border border-input bg-background px-6 py-3.5 text-base font-semibold text-foreground shadow-sm hover:bg-accent active:bg-accent/80 transition-colors"
>
Sign in
</Link>
</>
)}
</div>
</div>
</nav>
</div>
)}
<main id="main-content" className="flex-1">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] pt-0 pb-0">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-page-padding)] pt-0 pb-0">
{children}
</div>
</main>

View File

@ -23,18 +23,6 @@ import {
* and mission statement for Assist Solutions.
*/
export function AboutUsView() {
// Sample company logos for the trusted by carousel
const trustedCompanies = [
{ name: "Company 1", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 2", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 3", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 4", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 5", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 6", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 7", logo: "/assets/images/placeholder-logo.png" },
{ name: "Company 8", logo: "/assets/images/placeholder-logo.png" },
];
const values = [
{
text: "Make technology accessible for everyone, regardless of language barriers.",
@ -170,79 +158,8 @@ export function AboutUsView() {
</div>
</div>
</div>
</section>
{/* Trusted By Section - Infinite Carousel */}
<section className="relative left-1/2 right-1/2 w-screen -translate-x-1/2 bg-white py-10 sm:py-12 overflow-hidden">
{/* Gradient fade to next section */}
{/* Gradient fade to Business Solutions section */}
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-b from-transparent to-[#e8f5ff] pointer-events-none z-10" />
<div className="max-w-6xl mx-auto px-6 sm:px-8 mb-8">
<h2 className="text-center text-lg sm:text-xl font-semibold text-muted-foreground">
Trusted by Leading Companies
</h2>
</div>
{/* Infinite Carousel */}
<div className="relative">
{/* Gradient masks for fade effect on edges */}
<div className="absolute left-0 top-0 bottom-0 w-24 sm:w-32 bg-gradient-to-r from-white to-transparent z-10 pointer-events-none" />
<div className="absolute right-0 top-0 bottom-0 w-24 sm:w-32 bg-gradient-to-l from-white to-transparent z-10 pointer-events-none" />
{/* Scrolling container */}
<div className="flex overflow-hidden">
<div className="flex animate-scroll-infinite gap-12 sm:gap-16">
{/* First set of logos */}
{trustedCompanies.map((company, index) => (
<div
key={`logo-1-${index}`}
className="flex-shrink-0 w-28 h-16 sm:w-36 sm:h-20 flex items-center justify-center grayscale hover:grayscale-0 opacity-60 hover:opacity-100 transition-all duration-300"
>
<Image
src={company.logo}
alt={company.name}
width={120}
height={60}
className="object-contain max-h-full"
/>
</div>
))}
{/* Duplicate set for seamless loop */}
{trustedCompanies.map((company, index) => (
<div
key={`logo-2-${index}`}
className="flex-shrink-0 w-28 h-16 sm:w-36 sm:h-20 flex items-center justify-center grayscale hover:grayscale-0 opacity-60 hover:opacity-100 transition-all duration-300"
>
<Image
src={company.logo}
alt={company.name}
width={120}
height={60}
className="object-contain max-h-full"
/>
</div>
))}
</div>
</div>
</div>
{/* CSS for infinite scroll animation */}
<style jsx>{`
@keyframes scroll-infinite {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-scroll-infinite {
animation: scroll-infinite 30s linear infinite;
}
.animate-scroll-infinite:hover {
animation-play-state: paused;
}
`}</style>
</section>
{/* Business Solutions Carousel */}