diff --git a/apps/portal/src/components/atoms/animated-container.tsx b/apps/portal/src/components/atoms/animated-container.tsx new file mode 100644 index 00000000..8b16dd47 --- /dev/null +++ b/apps/portal/src/components/atoms/animated-container.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { cn } from "@/shared/utils"; + +interface AnimatedContainerProps { + children: React.ReactNode; + className?: string; + /** Animation type */ + animation?: "fade-up" | "fade-scale" | "slide-left" | "none"; + /** Whether to stagger children animations */ + stagger?: boolean; + /** Delay before animation starts in ms */ + delay?: number; +} + +/** + * Reusable animation wrapper component + * Provides consistent entrance animations for page content + */ +export function AnimatedContainer({ + children, + className, + animation = "fade-up", + stagger = false, + delay = 0, +}: AnimatedContainerProps) { + const animationClass = { + "fade-up": "cp-animate-in", + "fade-scale": "cp-animate-scale-in", + "slide-left": "cp-animate-slide-left", + none: "", + }[animation]; + + return ( +
0 ? { animationDelay: `${delay}ms` } : undefined} + > + {children} +
+ ); +} diff --git a/apps/portal/src/components/atoms/loading-overlay.tsx b/apps/portal/src/components/atoms/loading-overlay.tsx new file mode 100644 index 00000000..53d64e7c --- /dev/null +++ b/apps/portal/src/components/atoms/loading-overlay.tsx @@ -0,0 +1,41 @@ +import { Spinner } from "./spinner"; + +interface LoadingOverlayProps { + /** Whether the overlay is visible */ + isVisible: boolean; + /** Main loading message */ + title: string; + /** Optional subtitle/description */ + subtitle?: string; + /** Spinner size */ + spinnerSize?: "xs" | "sm" | "md" | "lg" | "xl"; + /** Custom spinner color */ + spinnerClassName?: string; + /** Custom overlay background */ + overlayClassName?: string; +} + +export function LoadingOverlay({ + isVisible, + title, + subtitle, + spinnerSize = "xl", + spinnerClassName = "text-primary", + overlayClassName = "bg-background/80 backdrop-blur-sm", +}: LoadingOverlayProps) { + if (!isVisible) { + return null; + } + + return ( +
+
+
+ +
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ ); +} diff --git a/apps/portal/src/components/atoms/skeleton.tsx b/apps/portal/src/components/atoms/skeleton.tsx new file mode 100644 index 00000000..eff285b3 --- /dev/null +++ b/apps/portal/src/components/atoms/skeleton.tsx @@ -0,0 +1,20 @@ +import { cn } from "@/shared/utils"; + +interface SkeletonProps { + className?: string; + animate?: boolean; +} + +/** + * Base skeleton atom for loading states. + * A simple shimmer box primitive that can be composed into loading patterns. + * + * For composed loading skeletons, use: + * - LoadingCard, LoadingTable, LoadingStats from molecules/LoadingSkeletons + * - Feature-specific skeletons from features/[feature]/components/skeletons + */ +export function Skeleton({ className, animate = true }: SkeletonProps) { + return ( +
+ ); +} diff --git a/apps/portal/src/components/atoms/spinner.tsx b/apps/portal/src/components/atoms/spinner.tsx new file mode 100644 index 00000000..e2e1afda --- /dev/null +++ b/apps/portal/src/components/atoms/spinner.tsx @@ -0,0 +1,31 @@ +import { cn } from "@/shared/utils"; + +interface SpinnerProps { + size?: "xs" | "sm" | "md" | "lg" | "xl"; + className?: string; +} + +const sizeClasses = { + xs: "h-3 w-3", + sm: "h-4 w-4", + md: "h-6 w-6", + lg: "h-8 w-8", + xl: "h-10 w-10", +}; + +export function Spinner({ size = "sm", className }: SpinnerProps) { + return ( + + + + + ); +} diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/index.ts b/apps/portal/src/components/molecules/LoadingSkeletons/index.ts new file mode 100644 index 00000000..4ce945d8 --- /dev/null +++ b/apps/portal/src/components/molecules/LoadingSkeletons/index.ts @@ -0,0 +1,8 @@ +/** + * Loading Skeleton Molecules + * Generic, reusable loading skeleton components for common UI patterns. + */ + +export { LoadingCard } from "./loading-card"; +export { LoadingTable } from "./loading-table"; +export { LoadingStats } from "./loading-stats"; diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx new file mode 100644 index 00000000..c7018902 --- /dev/null +++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx @@ -0,0 +1,36 @@ +import { cn } from "@/shared/utils"; +import { Skeleton } from "@/components/atoms"; + +interface LoadingCardProps { + className?: string; +} + +/** + * Generic loading skeleton for card-like content. + * Shows a header with avatar and two lines, plus body text lines. + */ +export function LoadingCard({ className }: LoadingCardProps) { + return ( +
+
+
+ +
+ + +
+
+
+ + + +
+
+
+ ); +} diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx new file mode 100644 index 00000000..32de0cdb --- /dev/null +++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from "@/components/atoms"; + +interface LoadingStatsProps { + /** Number of stat cards to display */ + count?: number; +} + +/** + * Generic loading skeleton for stats/metrics cards. + * Shows a grid of stat cards with icon and two value lines. + */ +export function LoadingStats({ count = 4 }: LoadingStatsProps) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( +
+
+ +
+ + +
+
+
+ ))} +
+ ); +} diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx new file mode 100644 index 00000000..33ab9ca2 --- /dev/null +++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx @@ -0,0 +1,40 @@ +import { Skeleton } from "@/components/atoms"; + +interface LoadingTableProps { + /** Number of rows to display */ + rows?: number; + /** Number of columns to display */ + columns?: number; +} + +/** + * Generic loading skeleton for table-like content. + * Shows a header row and configurable number of body rows. + */ +export function LoadingTable({ rows = 5, columns = 4 }: LoadingTableProps) { + return ( +
+ {/* Header */} +
+
+ {Array.from({ length: columns }).map((_, i) => ( + + ))} +
+
+ + {/* Rows */} +
+ {Array.from({ length: rows }).map((_, rowIndex) => ( +
+
+ {Array.from({ length: columns }).map((_, colIndex) => ( + + ))} +
+
+ ))} +
+
+ ); +} diff --git a/apps/portal/src/components/molecules/StatusBadge/index.ts b/apps/portal/src/components/molecules/StatusBadge/index.ts new file mode 100644 index 00000000..48a80b9e --- /dev/null +++ b/apps/portal/src/components/molecules/StatusBadge/index.ts @@ -0,0 +1,9 @@ +/** + * StatusBadge Molecule + * + * A configurable status display component that wraps StatusPill + * with domain-specific configuration maps. + */ + +export { StatusBadge } from "./status-badge"; +export type { StatusBadgeProps, StatusConfig, StatusConfigMap } from "./types"; diff --git a/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx b/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx new file mode 100644 index 00000000..ecf6c4fa --- /dev/null +++ b/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { forwardRef } from "react"; +import { StatusPill } from "@/components/atoms/status-pill"; +import type { StatusBadgeProps, StatusConfig } from "./types"; + +const DEFAULT_CONFIG: StatusConfig = { + variant: "neutral", +}; + +/** + * StatusBadge - A configurable status display component. + * + * Wraps the StatusPill atom with a configuration-driven approach, + * allowing domain-specific status mappings to be passed in. + * + * @example + * ```tsx + * // Simple usage with inline config + * + * + * // Or create a domain-specific wrapper + * const ORDER_STATUS_CONFIG = { + * pending: { variant: "warning", icon: , label: "Pending" }, + * completed: { variant: "success", icon: , label: "Completed" }, + * }; + * + * function OrderStatusBadge({ status }) { + * return ; + * } + * ``` + */ +export const StatusBadge = forwardRef( + ({ status, configMap, showIcon = true, defaultConfig = DEFAULT_CONFIG, ...props }, ref) => { + // Normalize status to lowercase for config lookup + const normalizedStatus = status.toLowerCase(); + + // Look up config, fall back to default + const config = configMap?.[normalizedStatus as keyof typeof configMap] ?? defaultConfig; + + // Determine the label (config label, or original status) + const label = config.label ?? status; + + return ( + + ); + } +); + +StatusBadge.displayName = "StatusBadge"; diff --git a/apps/portal/src/components/molecules/StatusBadge/types.ts b/apps/portal/src/components/molecules/StatusBadge/types.ts new file mode 100644 index 00000000..b5efbb78 --- /dev/null +++ b/apps/portal/src/components/molecules/StatusBadge/types.ts @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; +import type { StatusPillProps } from "@/components/atoms/status-pill"; + +/** + * Status configuration for a single status value. + * Maps a status string to its visual representation. + */ +export interface StatusConfig { + /** The visual variant/color for this status */ + variant: StatusPillProps["variant"]; + /** Optional icon to display */ + icon?: ReactNode; + /** Display label (defaults to the status key if not provided) */ + label?: string; +} + +/** + * A map of status values to their configurations. + * Keys should be lowercase for case-insensitive matching. + */ +export type StatusConfigMap = Partial< + Record, StatusConfig> +>; + +/** + * Props for the StatusBadge molecule. + */ +export interface StatusBadgeProps extends Omit< + StatusPillProps, + "variant" | "icon" | "label" +> { + /** The status value to display */ + status: T; + /** Configuration map for status values */ + configMap?: StatusConfigMap; + /** Whether to show the icon (default: true) */ + showIcon?: boolean; + /** Default config for unknown statuses */ + defaultConfig?: StatusConfig; +} diff --git a/apps/portal/src/features/billing/components/skeletons/index.ts b/apps/portal/src/features/billing/components/skeletons/index.ts new file mode 100644 index 00000000..4e14fad2 --- /dev/null +++ b/apps/portal/src/features/billing/components/skeletons/index.ts @@ -0,0 +1,6 @@ +/** + * Billing Loading Skeletons + * Feature-specific skeletons for consistent loading states. + */ + +export { InvoiceListSkeleton } from "./invoice-list-skeleton"; diff --git a/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx b/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx new file mode 100644 index 00000000..3f7e5ff4 --- /dev/null +++ b/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from "@/components/atoms"; + +interface InvoiceListSkeletonProps { + /** Number of rows to show */ + rows?: number; +} + +/** + * List skeleton for invoices/billing. + * Shows a heading and list items with left content and right price. + */ +export function InvoiceListSkeleton({ rows = 5 }: InvoiceListSkeletonProps) { + return ( +
+ +
+
+ {Array.from({ length: rows }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+
+
+ ); +} diff --git a/apps/portal/src/features/services/views/AccountServicesOverview.tsx b/apps/portal/src/features/services/views/AccountServicesOverview.tsx new file mode 100644 index 00000000..5b8cbab3 --- /dev/null +++ b/apps/portal/src/features/services/views/AccountServicesOverview.tsx @@ -0,0 +1,28 @@ +import { ServicesGrid } from "@/features/services/components/common/ServicesGrid"; + +/** + * AccountServicesOverview - Authenticated user's services landing page. + * + * Shows available services for the logged-in user with a header + * and services grid linking to account-specific service pages. + */ +export function AccountServicesOverview() { + return ( +
+
+ {/* Header */} +
+

+ Our Services +

+

+ From high-speed internet to onsite support, we provide comprehensive solutions for your + home and business. +

+
+ + +
+
+ ); +} diff --git a/apps/portal/src/features/services/views/PublicServicesOverview.tsx b/apps/portal/src/features/services/views/PublicServicesOverview.tsx new file mode 100644 index 00000000..71401ec1 --- /dev/null +++ b/apps/portal/src/features/services/views/PublicServicesOverview.tsx @@ -0,0 +1,157 @@ +import Link from "next/link"; +import { + Wifi, + Smartphone, + ShieldCheck, + ArrowRight, + Phone, + CheckCircle2, + Globe, + Headphones, + Building2, + Wrench, + Tv, +} from "lucide-react"; +import { ServiceCard } from "@/components/molecules/ServiceCard"; + +/** + * PublicServicesOverview - Public-facing services landing page. + * + * Shows all available services with hero section, value propositions, + * service cards grid, and contact CTA. + */ +export function PublicServicesOverview() { + return ( +
+ {/* Hero */} +
+
+ + + Full English Support + +
+ +

+ Our Services +

+ +

+ Connectivity and support solutions for Japan's international community. +

+
+ + {/* Value Props - Compact */} +
+
+ + One provider, all services +
+
+ + English support +
+
+ + No hidden fees +
+
+ + {/* All Services - Clean Grid with staggered animations */} +
+ } + title="Internet" + description="NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation." + price="¥3,200/mo" + accentColor="blue" + /> + + } + title="SIM & eSIM" + description="Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation." + price="¥1,100/mo" + badge="1st month free" + accentColor="green" + /> + + } + title="VPN Router" + description="Access US & UK streaming content with a pre-configured router. Simple plug-and-play." + price="¥2,500/mo" + accentColor="purple" + /> + + } + title="Business" + description="Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs." + accentColor="orange" + /> + + } + title="Onsite Support" + description="Professional technicians visit your location for setup, troubleshooting, and maintenance." + accentColor="cyan" + /> + + } + title="TV" + description="Streaming TV packages with international channels. Watch content from home countries." + accentColor="pink" + /> +
+ + {/* CTA */} +
+

Need help choosing?

+

+ Our bilingual team can help you find the right solution. +

+ +
+ + Contact Us + + + + +
+
+
+ ); +} diff --git a/apps/portal/src/features/subscriptions/components/skeletons/index.ts b/apps/portal/src/features/subscriptions/components/skeletons/index.ts new file mode 100644 index 00000000..327b73db --- /dev/null +++ b/apps/portal/src/features/subscriptions/components/skeletons/index.ts @@ -0,0 +1,8 @@ +/** + * Subscription Loading Skeletons + * Feature-specific skeletons for consistent loading states. + */ + +export { SubscriptionStatsCardsSkeleton } from "./subscription-stats-cards-skeleton"; +export { SubscriptionTableSkeleton } from "./subscription-table-skeleton"; +export { SubscriptionDetailStatsSkeleton } from "./subscription-detail-stats-skeleton"; diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx new file mode 100644 index 00000000..aa17bfaa --- /dev/null +++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx @@ -0,0 +1,30 @@ +import { Skeleton } from "@/components/atoms"; + +/** + * Stats skeleton for the subscription detail page. + * Shows 4 columns: Status, Amount, Next Due, Registration. + */ +export function SubscriptionDetailStatsSkeleton() { + return ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx new file mode 100644 index 00000000..113c2c47 --- /dev/null +++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx @@ -0,0 +1,23 @@ +import { Skeleton } from "@/components/atoms"; + +/** + * Stats cards skeleton for the subscriptions list page. + * Shows 3 stat cards with icon and value placeholders. + */ +export function SubscriptionStatsCardsSkeleton() { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ + +
+
+
+ ))} +
+ ); +} diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx new file mode 100644 index 00000000..d29df542 --- /dev/null +++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx @@ -0,0 +1,50 @@ +import { Skeleton } from "@/components/atoms"; + +interface SubscriptionTableSkeletonProps { + /** Number of rows to show */ + rows?: number; +} + +/** + * Table skeleton for the subscriptions list. + * Shows 3 columns: Service, Amount, Next Due. + */ +export function SubscriptionTableSkeleton({ rows = 6 }: SubscriptionTableSkeletonProps) { + return ( +
+
+ +
+
+ {/* Header skeleton */} +
+
+ + + +
+
+ {/* Row skeletons */} +
+ {Array.from({ length: rows }).map((_, i) => ( +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ ))} +
+
+
+ ); +}