Assist_Design/docs/plans/2026-03-06-sidebar-consolidation.md
barsa 7502068ea9 refactor: remove unused billing and payment components, enhance animation capabilities
- 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.
2026-03-06 14:48:34 +09:00

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


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"

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 — change href="/account/billing/invoices" to href="/account/billing"
  • Modify: apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx:208 — change href="/account/billing/invoices" to href="/account/billing"
  • Modify: apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx:160,187 — change href="/account/billing/invoices" to href="/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"