diff --git a/apps/portal/src/components/organisms/AppShell/AppShell.tsx b/apps/portal/src/components/organisms/AppShell/AppShell.tsx index f8d3c6df..538e76ee 100644 --- a/apps/portal/src/components/organisms/AppShell/AppShell.tsx +++ b/apps/portal/src/components/organisms/AppShell/AppShell.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; import { usePathname, useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { useAuthStore, useAuthSession } from "@/features/auth/stores/auth.store"; @@ -125,6 +126,29 @@ function useSidebarExpansion(pathname: string) { return { expandedItems, toggleExpanded }; } +/** + * Portals the NotificationBell into the PageLayout header slot (#page-header-end). + * Lives in AppShell so it never remounts on page navigation, preserving polling timers. + * Re-targets when the DOM element changes (page navigation causes PageLayout remount). + */ +function PortaledNotificationBell() { + const [target, setTarget] = useState(null); + + useEffect(() => { + const sync = () => { + const el = document.getElementById("page-header-end"); + setTarget(prev => (prev === el ? prev : el)); + }; + sync(); + const observer = new MutationObserver(sync); + observer.observe(document.body, { childList: true, subtree: true }); + return () => observer.disconnect(); + }, []); + + if (!target) return null; + return createPortal(, target); +} + function AuthLoadingSkeleton() { return (
@@ -231,22 +255,19 @@ export function AppShell({ children }: AppShellProps) { {/* Main content */}
- {/* Header bar */} -
- {/* Mobile hamburger + logo */} + {/* Mobile-only hamburger bar */} +
-
+
-
-
{/* Main content area */} @@ -257,7 +278,8 @@ export function AppShell({ children }: AppShellProps) {
- {/* Global utilities are mounted in RootLayout */} + {/* Persistent notification bell — portaled into PageLayout header */} + {isAuthReady && } ); } diff --git a/apps/portal/src/components/templates/PageLayout/PageLayout.tsx b/apps/portal/src/components/templates/PageLayout/PageLayout.tsx index e054f0a6..fc00c487 100644 --- a/apps/portal/src/components/templates/PageLayout/PageLayout.tsx +++ b/apps/portal/src/components/templates/PageLayout/PageLayout.tsx @@ -33,48 +33,34 @@ export function PageLayout({ }: PageLayoutProps) { return (
- {/* Page header */} -
-
- {backLink && ( -
- - - {backLink.label} - -
- )} -
-
-
- {icon && ( -
- {icon} -
- )} -
-
-

- {title} -

- {statusPill} -
- {description && ( -

- {description} -

- )} -
-
- {actions && ( -
- {actions} -
+ {/* Page header — h-16 matches sidebar logo area */} +
+
+
+
+ {backLink && ( + + + {backLink.label} + + )} + {icon &&
{icon}
} +

{title}

+ {statusPill &&
{statusPill}
} + {description && ( +

+ {description} +

)}
+ {actions && ( +
{actions}
+ )} + {/* NotificationBell is rendered by AppShell via #page-header-end portal */} +
diff --git a/apps/portal/src/features/billing/components/PaymentMethodCard.tsx b/apps/portal/src/features/billing/components/PaymentMethodCard.tsx index 2cd789b9..67884c7d 100644 --- a/apps/portal/src/features/billing/components/PaymentMethodCard.tsx +++ b/apps/portal/src/features/billing/components/PaymentMethodCard.tsx @@ -38,12 +38,12 @@ const getBrandColor = (brand?: string) => { const getMethodIcon = (type: PaymentMethod["type"], brand?: string) => { const baseClasses = - "w-12 h-12 bg-gradient-to-br rounded-xl flex items-center justify-center shadow-sm"; + "w-9 h-9 bg-gradient-to-br rounded-lg flex items-center justify-center shadow-sm"; if (isBankAccount(type)) { return (
- +
); } @@ -55,7 +55,7 @@ const getMethodIcon = (type: PaymentMethod["type"], brand?: string) => { return (
- +
); }; @@ -82,16 +82,16 @@ export function PaymentMethodCard({ return (
-
+
{icon}
-
-

{cardDisplay}

+
+

{cardDisplay}

{paymentMethod.isDefault && (
@@ -100,7 +100,7 @@ export function PaymentMethodCard({ )}
-
+
{cardBrand && {cardBrand}} {expiry && ( <> diff --git a/apps/portal/src/features/billing/views/BillingOverview.tsx b/apps/portal/src/features/billing/views/BillingOverview.tsx index 7faf78c8..17c9b0fb 100644 --- a/apps/portal/src/features/billing/views/BillingOverview.tsx +++ b/apps/portal/src/features/billing/views/BillingOverview.tsx @@ -77,8 +77,8 @@ function PaymentMethodsSection({
{hasMethods && ( -
-
+
+
{paymentMethodsData.paymentMethods.map(paymentMethod => ( ))} diff --git a/apps/portal/src/features/dashboard/views/DashboardView.tsx b/apps/portal/src/features/dashboard/views/DashboardView.tsx index abffda6f..404edeaf 100644 --- a/apps/portal/src/features/dashboard/views/DashboardView.tsx +++ b/apps/portal/src/features/dashboard/views/DashboardView.tsx @@ -65,21 +65,16 @@ function DashboardGreeting({ }) { return (
- - Welcome back, {displayName} - - {taskCount > 0 ? ( - +

+ Welcome back, {displayName} +

+ {taskCount > 0 ? ( } {taskCount === 1 ? "1 task needs attention" : `${taskCount} tasks need attention`} -
- ) : ( - - Everything is up to date - - )} + ) : ( + Everything is up to date + )} +
); } diff --git a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx index 8dd085d7..d7f649f8 100644 --- a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx +++ b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx @@ -311,6 +311,11 @@ function SimTabSwitcher({ ); } +const cardVariants = { + hidden: { opacity: 0, y: 16 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: "easeOut" as const } }, +}; + function SimPlansGrid({ regularPlans, familyPlans, @@ -344,15 +349,28 @@ function SimPlansGrid({ {regularPlans.length > 0 && (
{regularPlans.map(plan => ( - + + + ))}
)} @@ -365,7 +383,9 @@ function SimPlansGrid({
{familyPlans.map(plan => ( - + + + ))}
diff --git a/apps/portal/src/features/services/views/PublicInternetPlans.tsx b/apps/portal/src/features/services/views/PublicInternetPlans.tsx index de8b898a..8e578856 100644 --- a/apps/portal/src/features/services/views/PublicInternetPlans.tsx +++ b/apps/portal/src/features/services/views/PublicInternetPlans.tsx @@ -115,6 +115,24 @@ function getOfferingTypeId(offeringType: string | undefined): string { return "home1g"; } +const cardVariants = { + hidden: { opacity: 0, y: 16 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: "easeOut" as const } }, +}; + +const tierContainerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.08 }, + }, + exit: { + opacity: 0, + x: -24, + transition: { duration: 0.2 }, + }, +}; + // ─── Unified Internet Card ──────────────────────────────────────────────────── const OFFERING_DESCRIPTIONS: Record = { @@ -307,13 +325,15 @@ function UnifiedInternetCard({ {displayTiers.map(tier => ( - + + + ))} @@ -653,13 +673,23 @@ function FaqItem({ question, answer }: { question: string; answer: string }) { className="w-full flex items-start justify-between gap-3 p-4 text-left hover:bg-muted/50 transition-colors" > {question} - + + + - {isOpen && ( -
{answer}
- )} + + {isOpen && ( + +
{answer}
+
+ )} +
); } diff --git a/apps/portal/src/features/subscriptions/components/sim/ChangePlanModal.tsx b/apps/portal/src/features/subscriptions/components/sim/ChangePlanModal.tsx index 432b3f9e..364efc12 100644 --- a/apps/portal/src/features/subscriptions/components/sim/ChangePlanModal.tsx +++ b/apps/portal/src/features/subscriptions/components/sim/ChangePlanModal.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState } from "react"; +import { motion } from "framer-motion"; import { apiClient } from "@/core/api"; import { XMarkIcon } from "@heroicons/react/24/outline"; import { mapToSimplifiedFormat } from "../../utils/plan"; @@ -84,14 +85,21 @@ export function ChangePlanModal({ return (
- + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + /> -
+
@@ -129,7 +137,7 @@ export function ChangePlanModal({ Back
-
+
); diff --git a/apps/portal/src/features/subscriptions/components/sim/SimFeatureToggles.tsx b/apps/portal/src/features/subscriptions/components/sim/SimFeatureToggles.tsx index 0a055f53..935e1e4b 100644 --- a/apps/portal/src/features/subscriptions/components/sim/SimFeatureToggles.tsx +++ b/apps/portal/src/features/subscriptions/components/sim/SimFeatureToggles.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { apiClient } from "@/core/api"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { Button } from "@/components/atoms/button"; @@ -11,9 +12,7 @@ const TOGGLE_BASE = const TOGGLE_ACTIVE = "bg-primary"; const TOGGLE_INACTIVE = "bg-muted"; const TOGGLE_KNOB_BASE = - "pointer-events-none inline-block h-5 w-5 transform rounded-full bg-background shadow ring-0 transition duration-[var(--cp-duration-normal)] ease-in-out"; -const TOGGLE_KNOB_ON = "translate-x-5"; -const TOGGLE_KNOB_OFF = "translate-x-0"; + "pointer-events-none inline-block h-5 w-5 rounded-full bg-background shadow ring-0"; interface SimFeatureTogglesProps { subscriptionId: number; @@ -211,7 +210,6 @@ function FeatureToggleRow({ onChange, }: FeatureToggleRowProps): React.ReactElement { const toggleBgClass = checked ? TOGGLE_ACTIVE : TOGGLE_INACTIVE; - const knobPositionClass = checked ? TOGGLE_KNOB_ON : TOGGLE_KNOB_OFF; return (
@@ -226,7 +224,11 @@ function FeatureToggleRow({ onClick={onChange} className={`${TOGGLE_BASE} ${toggleBgClass}`} > - +
); @@ -238,6 +240,8 @@ interface NetworkTypeSelectorProps { } function NetworkTypeSelector({ nt, setNt }: NetworkTypeSelectorProps): React.ReactElement { + const options: Array<"4G" | "5G"> = ["4G", "5G"]; + return (
@@ -248,48 +252,40 @@ function NetworkTypeSelector({ nt, setNt }: NetworkTypeSelectorProps): React.Rea changed another option, you may need to wait before submitting.
-
- - +
+ {options.map(value => ( + + ))}

5G connectivity for enhanced speeds

); } -interface NetworkRadioOptionProps { - id: string; - value: "4G" | "5G"; - label: string; - checked: boolean; - onChange: (value: "4G" | "5G") => void; -} - -function NetworkRadioOption({ - id, - value, - label, - checked, - onChange, -}: NetworkRadioOptionProps): React.ReactElement { - return ( -
- onChange(value)} - className="h-4 w-4 text-primary focus:ring-ring border-input" - /> - -
- ); -} - interface NotesAndActionsSectionProps { embedded: boolean; success: string | null; @@ -326,21 +322,36 @@ function NotesAndActionsSection({ - {success && ( -
- - {success} - -
- )} - - {error && ( -
- - {error} - -
- )} + + {success && ( + + + {success} + + + )} + {error && ( + + + {error} + + + )} +