- 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.
358 lines
9.8 KiB
Markdown
358 lines
9.8 KiB
Markdown
# 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:
|
|
|
|
```tsx
|
|
<div className="bg-muted/40 border-b border-border">
|
|
```
|
|
|
|
to:
|
|
|
|
```tsx
|
|
<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**
|
|
|
|
```bash
|
|
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.
|
|
|
|
```tsx
|
|
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`:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```tsx
|
|
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**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```tsx
|
|
<Sidebar
|
|
navigation={navigation}
|
|
pathname={pathname}
|
|
expandedItems={expandedItems}
|
|
toggleExpanded={toggleExpanded}
|
|
isMobile
|
|
user={user}
|
|
profileReady={!!(user?.firstname || user?.lastname)}
|
|
/>
|
|
```
|
|
|
|
For the desktop sidebar (around line 191):
|
|
|
|
```tsx
|
|
<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:
|
|
|
|
```tsx
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```tsx
|
|
{
|
|
/* 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`:
|
|
|
|
```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`:
|
|
|
|
```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**
|
|
|
|
```bash
|
|
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 `Header` re-exports.
|
|
|
|
**Step 4: Verify build**
|
|
|
|
Run: `pnpm type-check`
|
|
Expected: No type errors.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "fix: lint fixes from layout redesign"
|
|
```
|