- Deleted loading and page components for invoices and payment methods to streamline the billing section. - Updated AnimatedContainer, InlineToast, and other components to utilize framer-motion for improved animations. - Refactored AppShell and Sidebar components to enhance layout and integrate new animation features. - Adjusted various sections across the portal to ensure consistent animation behavior and visual appeal.
14 KiB
Sidebar Navigation Consolidation
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Simplify sidebar by removing expandable sub-menus — make Support and Billing flat links, creating a combined Billing page.
Architecture: Remove children from navigation items, create a new combined Billing page view that composes existing PaymentMethods and InvoicesList components, update route structure to add /account/billing page.
Tech Stack: Next.js 15, React 19, Tailwind CSS, existing billing/support feature components
Task 1: Make Support a flat sidebar link
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/navigation.ts:46-49
Step 1: Update navigation config
Change the Support entry from expandable (with children) to a flat link:
// Before (lines 46-49):
{
name: "Support",
icon: ChatBubbleLeftRightIcon,
children: [
{ name: "Cases", href: "/account/support" },
{ name: "New Case", href: "/account/support/new" },
],
},
// After:
{ name: "Support", href: "/account/support", icon: ChatBubbleLeftRightIcon },
Step 2: Remove auto-expand logic for Support in AppShell
File: apps/portal/src/components/organisms/AppShell/AppShell.tsx:112
Remove the line:
if (pathname.startsWith("/account/support")) next.add("Support");
Step 3: Verify
Run: pnpm type-check
Expected: PASS — no type errors
Step 4: Commit
git add apps/portal/src/components/organisms/AppShell/navigation.ts apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "refactor: make Support a flat sidebar link"
Task 2: Create combined Billing page view
Files:
- Create:
apps/portal/src/features/billing/views/BillingOverview.tsx
Step 1: Create the combined billing view
This view composes existing PaymentMethodsContainer content and InvoicesList into one page. We reuse the existing components directly — payment methods section on top, invoices below.
"use client";
import { useState } from "react";
import { CreditCardIcon } from "@heroicons/react/24/outline";
import { PageLayout } from "@/components/templates/PageLayout";
import { ErrorBoundary } from "@/components/molecules";
import { useSession } from "@/features/auth/hooks";
import { useAuthStore } from "@/features/auth/stores/auth.store";
import { isApiError } from "@/core/api";
import { openSsoLink } from "@/features/billing/utils/sso";
import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
import {
PaymentMethodCard,
usePaymentMethods,
useCreatePaymentMethodsSsoLink,
} from "@/features/billing";
import type { PaymentMethodList } from "@customer-portal/domain/payments";
import { InlineToast } from "@/components/atoms/inline-toast";
import { Button } from "@/components/atoms/button";
import { Skeleton } from "@/components/atoms/loading-skeleton";
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
import { logger } from "@/core/logger";
function PaymentMethodsSkeleton() {
return (
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-6">
<div className="flex items-center justify-between mb-6">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-10 w-32" />
</div>
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="bg-muted rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-24" />
</div>
</div>
<Skeleton className="h-9 w-28" />
</div>
</div>
))}
</div>
</div>
);
}
function PaymentMethodsSection({
paymentMethodsData,
onManage,
isPending,
}: {
paymentMethodsData: PaymentMethodList;
onManage: () => void;
isPending: boolean;
}) {
const hasMethods = paymentMethodsData.paymentMethods.length > 0;
return (
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="px-6 py-5 border-b border-border">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-foreground">Payment Methods</h2>
<p className="text-sm text-muted-foreground mt-0.5">
{hasMethods
? `${paymentMethodsData.paymentMethods.length} payment method${paymentMethodsData.paymentMethods.length === 1 ? "" : "s"} on file`
: "No payment methods on file"}
</p>
</div>
<Button onClick={onManage} disabled={isPending} size="default">
{isPending ? "Opening..." : "Manage Cards"}
</Button>
</div>
</div>
{hasMethods && (
<div className="p-6">
<div className="space-y-4">
{paymentMethodsData.paymentMethods.map(paymentMethod => (
<PaymentMethodCard key={paymentMethod.id} paymentMethod={paymentMethod} />
))}
</div>
</div>
)}
</div>
);
}
export function BillingOverview() {
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useSession();
const paymentMethodsQuery = usePaymentMethods();
const {
data: paymentMethodsData,
isLoading: isLoadingPaymentMethods,
isFetching: isFetchingPaymentMethods,
error: paymentMethodsError,
} = paymentMethodsQuery;
const createPaymentMethodsSsoLink = useCreatePaymentMethodsSsoLink();
const { hasCheckedAuth } = useAuthStore();
const paymentRefresh = usePaymentRefresh({
refetch: async () => {
const result = await paymentMethodsQuery.refetch();
return { data: result.data };
},
hasMethods: data => Boolean(data && (data.totalCount > 0 || data.paymentMethods.length > 0)),
attachFocusListeners: true,
});
const openPaymentMethods = async () => {
if (!isAuthenticated) {
setError("Please log in to access payment methods.");
return;
}
setError(null);
try {
const ssoLink = await createPaymentMethodsSsoLink.mutateAsync();
openSsoLink(ssoLink.url, { newTab: true });
} catch (err: unknown) {
logger.error("Failed to open payment methods", err);
if (
isApiError(err) &&
"response" in err &&
typeof err.response === "object" &&
err.response !== null &&
"status" in err.response &&
err.response.status === 401
) {
setError("Authentication failed. Please log in again.");
} else {
setError("Unable to access payment methods. Please try again later.");
}
}
};
const isPaymentLoading = !hasCheckedAuth || isLoadingPaymentMethods || isFetchingPaymentMethods;
const combinedError = error
? new Error(error)
: paymentMethodsError instanceof Error
? paymentMethodsError
: paymentMethodsError
? new Error(String(paymentMethodsError))
: null;
return (
<PageLayout icon={<CreditCardIcon />} title="Billing" error={combinedError}>
<ErrorBoundary>
<InlineToast
visible={paymentRefresh.toast.visible}
text={paymentRefresh.toast.text}
tone={paymentRefresh.toast.tone}
/>
<div className="space-y-8">
{/* Payment Methods Section */}
{isPaymentLoading && <PaymentMethodsSkeleton />}
{!isPaymentLoading && paymentMethodsData && (
<PaymentMethodsSection
paymentMethodsData={paymentMethodsData}
onManage={() => void openPaymentMethods()}
isPending={createPaymentMethodsSsoLink.isPending}
/>
)}
{/* Invoices Section */}
<div>
<h2 className="text-lg font-semibold text-foreground mb-4">Invoices</h2>
<InvoicesList />
</div>
</div>
</ErrorBoundary>
</PageLayout>
);
}
export default BillingOverview;
Step 2: Verify
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/portal/src/features/billing/views/BillingOverview.tsx
git commit -m "feat: create combined BillingOverview view"
Task 3: Add /account/billing route and make sidebar flat
Files:
- Create:
apps/portal/src/app/account/billing/page.tsx - Modify:
apps/portal/src/components/organisms/AppShell/navigation.ts:29-36 - Modify:
apps/portal/src/components/organisms/AppShell/AppShell.tsx:111
Step 1: Create the billing page
import { BillingOverview } from "@/features/billing/views/BillingOverview";
export default function AccountBillingPage() {
return <BillingOverview />;
}
Step 2: Update navigation config
Change Billing from expandable to flat:
// Before (lines 29-36):
{
name: "Billing",
icon: CreditCardIcon,
children: [
{ name: "Invoices", href: "/account/billing/invoices" },
{ name: "Payment Methods", href: "/account/billing/payments" },
],
},
// After:
{ name: "Billing", href: "/account/billing", icon: CreditCardIcon },
Step 3: Remove auto-expand logic for Billing in AppShell
File: apps/portal/src/components/organisms/AppShell/AppShell.tsx
Remove the line:
if (pathname.startsWith("/account/billing")) next.add("Billing");
Step 4: Verify
Run: pnpm type-check
Expected: PASS
Step 5: Commit
git add apps/portal/src/app/account/billing/page.tsx apps/portal/src/components/organisms/AppShell/navigation.ts apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "refactor: make Billing a flat sidebar link with combined page"
Task 4: Update Sidebar active-state matching for flat Billing and Support
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/Sidebar.tsx:327
The current SimpleNavItem uses exact match (pathname === item.href) which won't highlight Billing when on /account/billing/invoices/123. Change to startsWith for path-based matching:
// Before (line 327):
const isActive = item.href ? pathname === item.href : false;
// After:
const isActive = item.href
? item.href === "/account"
? pathname === item.href
: pathname.startsWith(item.href)
: false;
This ensures:
- Dashboard (
/account) still uses exact match (doesn't highlight for every/account/*page) - Billing (
/account/billing) highlights on/account/billing,/account/billing/invoices/123, etc. - Support (
/account/support) highlights on/account/support,/account/support/new,/account/support/123, etc.
Step 1: Update Sidebar active matching
Apply the change above.
Step 2: Verify
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/portal/src/components/organisms/AppShell/Sidebar.tsx
git commit -m "fix: use startsWith for sidebar active state on nested routes"
Task 5: Update backLink references in InvoiceDetail
Files:
- Modify:
apps/portal/src/features/billing/views/InvoiceDetail.tsx:84,99
Update the "Back to Invoices" links to point to the combined billing page:
// Before:
backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
// After:
backLink={{ label: "Back to Billing", href: "/account/billing" }}
Apply this on both lines 84 and 99.
Step 1: Apply changes
Step 2: Verify
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/portal/src/features/billing/views/InvoiceDetail.tsx
git commit -m "fix: update InvoiceDetail backLink to point to combined billing page"
Task 6: Update remaining billing route references
Files:
- Modify:
apps/portal/src/features/dashboard/utils/dashboard.utils.ts:43— change/account/billing/invoices/${activity.relatedId}to keep as-is (invoice detail pages still live at/account/billing/invoices/[id]) - Modify:
apps/portal/src/features/dashboard/components/TaskList.tsx:66— changehref="/account/billing/invoices"tohref="/account/billing" - Modify:
apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx:208— changehref="/account/billing/invoices"tohref="/account/billing" - Modify:
apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx:160,187— changehref="/account/billing/invoices"tohref="/account/billing"
Note: Keep dashboard.utils.ts:43 unchanged — it links to a specific invoice detail page which still exists at /account/billing/invoices/[id].
Note: Keep InvoiceTable.tsx:276 unchanged — it navigates to individual invoice detail pages.
Step 1: Apply changes to the 3 files listed above
Step 2: Verify
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/portal/src/features/dashboard/components/TaskList.tsx apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx
git commit -m "fix: update billing route references to use combined billing page"
Task 7: Clean up unused imports in navigation.ts
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/navigation.ts
After removing children from both Billing and Support, the NavigationChild type and children-related interfaces may still be needed by other code (the Sidebar still supports expandable items generically). Check if NavigationChild is still used — if Subscriptions or any other item still has children, keep it. If no items have children anymore, remove unused types.
Step 1: Check if any navigation item still uses children
After our changes, review baseNavigation — none will have children. But NavigationChild and children field on NavigationItem are still referenced by Sidebar.tsx (the ExpandableNavItem component). These can stay for now since they're part of the generic nav system — removing the component is a larger cleanup.
Step 2: Verify full build
Run: pnpm type-check && pnpm lint
Expected: PASS
Step 3: Final commit if any cleanup was needed
git add -A
git commit -m "chore: sidebar consolidation cleanup"