- Replaced the Header component with a mobile-only hamburger menu in the AppShell for improved navigation on smaller screens. - Integrated user profile information directly into the Sidebar for better accessibility. - Removed the Settings link from the navigation to streamline the user experience. - Updated Sidebar and NotificationBell components to accommodate new user profile display logic.
9.8 KiB
Portal Layout Redesign Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Remove the top Header bar, move profile + notifications into the sidebar, and simplify the PageLayout header band.
Architecture: The AppShell currently renders a Header component (notifications, profile, help link) above the main content, and each page uses PageLayout which adds a second header band with title/actions. We remove the Header on desktop (keep mobile hamburger), add a profile row + notification bell to the Sidebar, and drop the border from PageLayout's title area.
Tech Stack: Next.js 15, React 19, Tailwind CSS v4, shadcn/ui, Heroicons
Task 1: Remove border from PageLayout header band
Files:
- Modify:
apps/portal/src/components/templates/PageLayout/PageLayout.tsx:37
Step 1: Edit PageLayout
In PageLayout.tsx line 37, change:
<div className="bg-muted/40 border-b border-border">
to:
<div className="bg-muted/40">
Step 2: Verify visually
Run: pnpm --filter @customer-portal/portal dev (if dev server is running, just check the browser)
Expected: Page title areas no longer have a bottom border, content flows smoothly below.
Step 3: Commit
git add apps/portal/src/components/templates/PageLayout/PageLayout.tsx
git commit -m "style: remove border from PageLayout header band"
Task 2: Add profile row to Sidebar
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/Sidebar.tsx
Step 1: Add SidebarProfile component
Add a new component inside Sidebar.tsx, before the Sidebar export. This renders the compact profile row with avatar initials, display name, and the notification bell.
import { NotificationBell } from "@/features/notifications";
interface SidebarProfileProps {
user: { firstName?: string | null; lastName?: string | null; email?: string | null } | null;
profileReady: boolean;
}
function getDisplayName(user: SidebarProfileProps["user"], profileReady: boolean): string {
const fullName = [user?.firstName, user?.lastName].filter(Boolean).join(" ");
const emailPrefix = user?.email?.split("@")[0];
if (profileReady) return fullName || emailPrefix || "Account";
return emailPrefix || "Account";
}
function getInitials(
user: SidebarProfileProps["user"],
profileReady: boolean,
displayName: string
): string {
if (profileReady && user?.firstName && user?.lastName) {
return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();
}
return displayName.slice(0, 2).toUpperCase();
}
function SidebarProfile({ user, profileReady }: SidebarProfileProps) {
const displayName = getDisplayName(user, profileReady);
const initials = getInitials(user, profileReady, displayName);
return (
<div className="px-3 pb-4">
<div className="flex items-center gap-2 px-2 py-2 rounded-lg bg-white/10">
<Link
href="/account/settings"
prefetch
className="flex items-center gap-2 flex-1 min-w-0 group"
>
<div className="h-7 w-7 rounded-lg bg-white/20 flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0">
{initials}
</div>
<span className="text-sm font-medium text-white/90 truncate group-hover:text-white transition-colors">
{displayName}
</span>
</Link>
<NotificationBell className="flex-shrink-0 [&_button]:text-white/70 [&_button]:hover:text-white [&_button]:hover:bg-white/10 [&_button]:p-1.5 [&_button]:rounded-md [&_.absolute]:top-0.5 [&_.absolute]:right-0.5" />
</div>
</div>
);
}
Note: The NotificationBell styling override uses Tailwind's child selector syntax to restyle the button for the dark sidebar context. The dropdown will still render with its normal popover styling since it uses absolute positioning with bg-popover.
Step 2: Update Sidebar props and render the profile row
Add user and profileReady to SidebarProps:
interface SidebarProps {
navigation: NavigationItem[];
pathname: string;
expandedItems: string[];
toggleExpanded: (name: string) => void;
isMobile?: boolean;
user?: { firstName?: string | null; lastName?: string | null; email?: string | null } | null;
profileReady?: boolean;
}
In the Sidebar component, render SidebarProfile between the logo area and the nav:
export const Sidebar = memo(function Sidebar({
navigation,
pathname,
expandedItems,
toggleExpanded,
user,
profileReady = false,
}: SidebarProps) {
return (
<div className="flex flex-col h-0 flex-1 bg-sidebar">
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border">
{/* ... existing logo ... */}
</div>
{/* Profile row */}
<div className="pt-4">
<SidebarProfile user={user} profileReady={profileReady} />
</div>
<div className="flex-1 flex flex-col pb-4 overflow-y-auto">
<nav className="flex-1 px-3 space-y-1">{/* ... existing nav items ... */}</nav>
{/* ... existing logout ... */}
</div>
</div>
);
});
Step 3: Add Link import
Ensure Link from next/link is imported (it already is in the existing file).
Step 4: Verify build
Run: pnpm type-check
Expected: No type errors.
Step 5: Commit
git add apps/portal/src/components/organisms/AppShell/Sidebar.tsx
git commit -m "feat: add profile row with notification bell to sidebar"
Task 3: Pass user data to Sidebar from AppShell
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/AppShell.tsx
Step 1: Pass user and profileReady props to both Sidebar instances
In AppShell.tsx, the user object from useAppShellAuth has firstName, lastName, email fields. Pass them to both the mobile and desktop <Sidebar> instances:
For the mobile sidebar (around line 177):
<Sidebar
navigation={navigation}
pathname={pathname}
expandedItems={expandedItems}
toggleExpanded={toggleExpanded}
isMobile
user={user}
profileReady={!!(user?.firstname || user?.lastname)}
/>
For the desktop sidebar (around line 191):
<Sidebar
navigation={navigation}
pathname={pathname}
expandedItems={expandedItems}
toggleExpanded={toggleExpanded}
user={user}
profileReady={!!(user?.firstname || user?.lastname)}
/>
Note: The auth store uses firstname/lastname (lowercase). The Sidebar's SidebarProfile expects firstName/lastName (camelCase) matching the Header convention. Map in the prop:
user={user ? { firstName: user.firstname, lastName: user.lastname, email: user.email } : null}
Step 2: Verify build
Run: pnpm type-check
Expected: No type errors.
Step 3: Commit
git add apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "feat: pass user data to sidebar for profile row"
Task 4: Remove Header from desktop, keep mobile hamburger
Files:
- Modify:
apps/portal/src/components/organisms/AppShell/AppShell.tsx
Step 1: Replace the Header component with a mobile-only hamburger bar
In AppShell.tsx, replace the <Header> render (around line 203) with:
{
/* Mobile-only hamburger bar */
}
<div className="md:hidden flex items-center h-12 px-3 border-b border-border/40 bg-background">
<button
type="button"
className="flex items-center justify-center w-10 h-10 -ml-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
onClick={() => setSidebarOpen(true)}
aria-label="Open navigation"
>
<Bars3Icon className="h-5 w-5" />
</button>
<div className="ml-2">
<Logo size={20} />
</div>
</div>;
Step 2: Add necessary imports
Add imports at the top of AppShell.tsx:
import { Bars3Icon } from "@heroicons/react/24/outline";
import { Logo } from "@/components/atoms/logo";
Step 3: Remove the Header import
Remove this line from AppShell.tsx:
import { Header } from "./Header";
Step 4: Verify build
Run: pnpm type-check
Expected: No type errors.
Step 5: Verify visually
Check the portal in a browser:
- Desktop: no top bar, content starts immediately. Sidebar shows profile + bell.
- Mobile (resize narrow): thin bar with hamburger + logo. Sidebar overlay has profile + bell.
Step 6: Commit
git add apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "refactor: remove desktop header, add mobile-only hamburger bar"
Task 5: Clean up Header.tsx
Files:
- Modify or delete:
apps/portal/src/components/organisms/AppShell/Header.tsx
Step 1: Check for other imports of Header
Run: pnpm exec grep -r "from.*Header" apps/portal/src/components/organisms/AppShell/ --include="*.tsx" --include="*.ts"
If only AppShell.tsx imported it (and we removed that import in Task 4), the file is unused.
Step 2: Delete Header.tsx
If no other consumers exist, delete the file.
Step 3: Check for barrel exports
Check if Header is exported from any index file:
- Check
apps/portal/src/components/organisms/AppShell/index.ts(if it exists) - Check
apps/portal/src/components/organisms/index.ts - Remove any
Headerre-exports.
Step 4: Verify build
Run: pnpm type-check
Expected: No type errors.
Step 5: Commit
git add -A apps/portal/src/components/organisms/AppShell/Header.tsx
git commit -m "chore: remove unused Header component"
Task 6: Final type-check and lint
Files: None (verification only)
Step 1: Run type-check
Run: pnpm type-check
Expected: Clean pass.
Step 2: Run lint
Run: pnpm lint
Expected: Clean pass (or only pre-existing warnings).
Step 3: Commit any lint fixes if needed
git add .
git commit -m "fix: lint fixes from layout redesign"