refactor: enhance AppShell layout and remove Header component
- 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.
This commit is contained in:
parent
b3cb1064d8
commit
be3388cf58
@ -4,9 +4,10 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useAuthStore, useAuthSession } from "@/features/auth/stores/auth.store";
|
||||
import { accountService } from "@/features/account/api/account.api";
|
||||
import { Bars3Icon } from "@heroicons/react/24/outline";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { Header } from "./Header";
|
||||
import { baseNavigation } from "./navigation";
|
||||
import { Logo } from "@/components/atoms/logo";
|
||||
|
||||
interface AppShellProps {
|
||||
children: React.ReactNode;
|
||||
@ -109,7 +110,6 @@ function useSidebarExpansion(pathname: string) {
|
||||
if (pathname.startsWith("/account/subscriptions")) next.add("Subscriptions");
|
||||
if (pathname.startsWith("/account/billing")) next.add("Billing");
|
||||
if (pathname.startsWith("/account/support")) next.add("Support");
|
||||
if (pathname.startsWith("/account/settings")) next.add("Settings");
|
||||
const result = [...next];
|
||||
if (result.length === prev.length && result.every(v => prev.includes(v))) return prev;
|
||||
return result;
|
||||
@ -180,6 +180,16 @@ export function AppShell({ children }: AppShellProps) {
|
||||
expandedItems={expandedItems}
|
||||
toggleExpanded={toggleExpanded}
|
||||
isMobile
|
||||
user={
|
||||
user
|
||||
? {
|
||||
firstName: user.firstname ?? null,
|
||||
lastName: user.lastname ?? null,
|
||||
email: user.email,
|
||||
}
|
||||
: null
|
||||
}
|
||||
profileReady={!!(user?.firstname || user?.lastname)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -193,18 +203,36 @@ export function AppShell({ children }: AppShellProps) {
|
||||
pathname={pathname}
|
||||
expandedItems={expandedItems}
|
||||
toggleExpanded={toggleExpanded}
|
||||
user={
|
||||
user
|
||||
? {
|
||||
firstName: user.firstname ?? null,
|
||||
lastName: user.lastname ?? null,
|
||||
email: user.email,
|
||||
}
|
||||
: null
|
||||
}
|
||||
profileReady={!!(user?.firstname || user?.lastname)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-col w-0 flex-1 overflow-hidden bg-background">
|
||||
{/* Header */}
|
||||
<Header
|
||||
onMenuClick={() => setSidebarOpen(true)}
|
||||
user={user}
|
||||
profileReady={!!(user?.firstname || user?.lastname)}
|
||||
/>
|
||||
{/* Mobile-only hamburger bar */}
|
||||
<div className="md:hidden flex items-center h-16 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>
|
||||
|
||||
{/* Main content area */}
|
||||
<main className="flex-1 relative overflow-y-auto focus:outline-none">
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { memo } from "react";
|
||||
import { Bars3Icon, QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { NotificationBell } from "@/features/notifications";
|
||||
|
||||
interface UserInfo {
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
function getDisplayName(user: UserInfo | null, 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: UserInfo | null, 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();
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
onMenuClick: () => void;
|
||||
user: UserInfo | null;
|
||||
profileReady: boolean;
|
||||
}
|
||||
|
||||
export const Header = memo(function Header({ onMenuClick, user, profileReady }: HeaderProps) {
|
||||
const displayName = getDisplayName(user, profileReady);
|
||||
const initials = getInitials(user, profileReady, displayName);
|
||||
|
||||
return (
|
||||
<div className="relative z-40 bg-header/80 backdrop-blur-xl border-b border-border/40">
|
||||
<div className="flex items-center h-14 gap-2 px-3 sm:px-5">
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
type="button"
|
||||
className="md:hidden 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={onMenuClick}
|
||||
aria-label="Open navigation"
|
||||
>
|
||||
<Bars3Icon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right side actions */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{/* Notification bell */}
|
||||
<NotificationBell />
|
||||
|
||||
{/* Help link */}
|
||||
<Link
|
||||
href="/account/support"
|
||||
prefetch
|
||||
aria-label="Help"
|
||||
className="hidden sm:inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted/60 transition-all duration-200"
|
||||
title="Support Center"
|
||||
>
|
||||
<QuestionMarkCircleIcon className="h-4.5 w-4.5" />
|
||||
</Link>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="hidden sm:block w-px h-5 bg-border/60 mx-1.5" />
|
||||
|
||||
{/* Profile link */}
|
||||
<Link
|
||||
href="/account/settings"
|
||||
prefetch
|
||||
className="group flex items-center gap-2 px-2 py-1 min-h-[40px] text-sm font-medium text-muted-foreground hover:text-foreground rounded-lg transition-all duration-200"
|
||||
>
|
||||
<div className="h-7 w-7 rounded-lg bg-gradient-to-br from-primary to-accent-gradient flex items-center justify-center text-[11px] font-bold text-primary-foreground shadow-sm group-hover:shadow-md transition-shadow">
|
||||
{initials}
|
||||
</div>
|
||||
<span className="hidden sm:inline text-[13px]">{displayName}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -5,6 +5,7 @@ import { memo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuthStore } from "@/features/auth/stores/auth.store";
|
||||
import { Logo } from "@/components/atoms/logo";
|
||||
import { NotificationBell } from "@/features/notifications";
|
||||
import type { NavigationChild, NavigationItem } from "./navigation";
|
||||
import type { ComponentType, SVGProps } from "react";
|
||||
|
||||
@ -48,12 +49,75 @@ function NavIcon({
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarUserInfo {
|
||||
firstName?: string | null | undefined;
|
||||
lastName?: string | null | undefined;
|
||||
email?: string | null | undefined;
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
navigation: NavigationItem[];
|
||||
pathname: string;
|
||||
expandedItems: string[];
|
||||
toggleExpanded: (name: string) => void;
|
||||
isMobile?: boolean;
|
||||
user?: SidebarUserInfo | null;
|
||||
profileReady?: boolean;
|
||||
}
|
||||
|
||||
function getSidebarDisplayName(
|
||||
user: SidebarUserInfo | null | undefined,
|
||||
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 getSidebarInitials(
|
||||
user: SidebarUserInfo | null | undefined,
|
||||
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,
|
||||
}: {
|
||||
user?: SidebarUserInfo | null | undefined;
|
||||
profileReady: boolean;
|
||||
}) {
|
||||
const displayName = getSidebarDisplayName(user, profileReady);
|
||||
const initials = getSidebarInitials(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
|
||||
dropdownPosition="right"
|
||||
className="flex-shrink-0 [&_button]:text-white/70 [&_button]:hover:text-white [&_button]:hover:bg-white/10 [&_button]:p-1.5 [&_button]:rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Sidebar = memo(function Sidebar({
|
||||
@ -61,6 +125,8 @@ export const Sidebar = memo(function Sidebar({
|
||||
pathname,
|
||||
expandedItems,
|
||||
toggleExpanded,
|
||||
user,
|
||||
profileReady = false,
|
||||
}: SidebarProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-0 flex-1 bg-sidebar">
|
||||
@ -76,7 +142,11 @@ export const Sidebar = memo(function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col pt-6 pb-4 overflow-y-auto">
|
||||
<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">
|
||||
{navigation
|
||||
.filter(item => !item.isLogout)
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
CreditCardIcon,
|
||||
ServerIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
UserIcon,
|
||||
ArrowRightStartOnRectangleIcon,
|
||||
Squares2X2Icon,
|
||||
ClipboardDocumentListIcon,
|
||||
@ -49,10 +48,5 @@ export const baseNavigation: NavigationItem[] = [
|
||||
{ name: "New Case", href: "/account/support/new" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/account/settings",
|
||||
icon: UserIcon,
|
||||
},
|
||||
{ name: "Log out", href: "#", icon: ArrowRightStartOnRectangleIcon, isLogout: true },
|
||||
];
|
||||
|
||||
@ -34,7 +34,7 @@ export function PageLayout({
|
||||
return (
|
||||
<div>
|
||||
{/* Page header */}
|
||||
<div className="bg-muted/40 border-b border-border">
|
||||
<div className="bg-muted/40">
|
||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)]">
|
||||
{backLink && (
|
||||
<div className="mb-[var(--cp-space-sm)] sm:mb-[var(--cp-space-md)]">
|
||||
|
||||
@ -8,10 +8,13 @@ import { cn } from "@/shared/utils";
|
||||
|
||||
interface NotificationBellProps {
|
||||
className?: string;
|
||||
/** Position the dropdown to the right of the bell (for sidebar usage) */
|
||||
dropdownPosition?: "below" | "right";
|
||||
}
|
||||
|
||||
export const NotificationBell = memo(function NotificationBell({
|
||||
className,
|
||||
dropdownPosition = "below",
|
||||
}: NotificationBellProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -81,7 +84,7 @@ export const NotificationBell = memo(function NotificationBell({
|
||||
)}
|
||||
</button>
|
||||
|
||||
<NotificationDropdown isOpen={isOpen} onClose={closeDropdown} />
|
||||
<NotificationDropdown isOpen={isOpen} onClose={closeDropdown} position={dropdownPosition} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@ -16,11 +16,13 @@ import { cn } from "@/shared/utils";
|
||||
interface NotificationDropdownProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
position?: "below" | "right";
|
||||
}
|
||||
|
||||
export const NotificationDropdown = memo(function NotificationDropdown({
|
||||
isOpen,
|
||||
onClose,
|
||||
position = "below",
|
||||
}: NotificationDropdownProps) {
|
||||
const { data, isLoading } = useNotifications({
|
||||
limit: 10,
|
||||
@ -40,7 +42,8 @@ export const NotificationDropdown = memo(function NotificationDropdown({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 top-full mt-2 w-80 sm:w-96",
|
||||
"absolute w-80 sm:w-96",
|
||||
position === "right" ? "left-full top-0 ml-2" : "right-0 top-full mt-2",
|
||||
"bg-popover border border-border rounded-xl shadow-lg z-50 overflow-hidden",
|
||||
"animate-in fade-in-0 zoom-in-95 duration-100"
|
||||
)}
|
||||
|
||||
78
docs/plans/2026-03-06-portal-layout-redesign-design.md
Normal file
78
docs/plans/2026-03-06-portal-layout-redesign-design.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Portal Layout Redesign
|
||||
|
||||
**Date:** 2026-03-06
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The portal has a double-header issue: a top Header bar (notifications + profile) and a PageLayout page header (title + actions) stack directly, creating two competing bars with different backgrounds and borders. The sidebar already provides navigation context, making the top header redundant.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Remove the top Header bar entirely on desktop** -- sidebar handles everything
|
||||
2. **Move profile into sidebar top** -- compact profile row below the logo
|
||||
3. **Notification bell in the profile row** -- reuse existing NotificationBell component, styled for sidebar context
|
||||
4. **PageLayout keeps subtle bg, drops the border** -- `bg-muted/40` stays, `border-b` removed
|
||||
5. **Mobile keeps a thin hamburger-only bar** -- opens sidebar overlay with full nav + profile + notifications
|
||||
|
||||
## Design
|
||||
|
||||
### Sidebar Structure
|
||||
|
||||
```
|
||||
+---------------------+
|
||||
| Logo Assist Solns | Logo + brand
|
||||
| Customer Portal |
|
||||
| +--JD--John Doe--B+ | Profile row + bell icon with badge
|
||||
+---------------------+
|
||||
| Dashboard |
|
||||
| Orders |
|
||||
| Billing > |
|
||||
| Subscriptions |
|
||||
| Services |
|
||||
| Support > |
|
||||
| Settings |
|
||||
+---------------------+
|
||||
| Log out |
|
||||
+---------------------+
|
||||
```
|
||||
|
||||
- Profile row: avatar initials + display name (links to /account/settings) + notification bell with badge
|
||||
- Bell icon uses existing NotificationBell component, restyled for white-on-dark sidebar
|
||||
- Separator between profile area and nav list
|
||||
|
||||
### PageLayout
|
||||
|
||||
- Keep: `bg-muted/40` background on the title area
|
||||
- Remove: `border-b border-border`
|
||||
- All existing props stay: icon, title, description, actions, back link, status pill
|
||||
- Content flows directly below without a hard line
|
||||
|
||||
### Mobile
|
||||
|
||||
- Thin top bar with hamburger + logo mark only
|
||||
- Opens sidebar overlay (already exists) which contains profile + bell, full nav, logout
|
||||
- No separate header elements on mobile
|
||||
|
||||
### AppShell Main Content
|
||||
|
||||
- Desktop: no header, content starts immediately with `<main>`
|
||||
- Mobile: thin hamburger bar above `<main>`
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Change |
|
||||
| ---------------- | ------------------------------------------------------------ |
|
||||
| `AppShell.tsx` | Remove `<Header>` on desktop, keep mobile-only hamburger bar |
|
||||
| `Header.tsx` | Simplify to mobile-only hamburger, or inline into AppShell |
|
||||
| `Sidebar.tsx` | Add profile row with bell below logo |
|
||||
| `PageLayout.tsx` | Remove `border-b border-border` from page header |
|
||||
|
||||
## What Stays the Same
|
||||
|
||||
- Sidebar colors, width (220px), collapse behavior
|
||||
- All page content structure
|
||||
- Mobile sidebar overlay animation
|
||||
- Auth flow, prefetching, sidebar expansion persistence
|
||||
- All existing PageLayout props and usage patterns
|
||||
- NotificationBell component logic (just restyled)
|
||||
357
docs/plans/2026-03-06-portal-layout-redesign-plan.md
Normal file
357
docs/plans/2026-03-06-portal-layout-redesign-plan.md
Normal file
@ -0,0 +1,357 @@
|
||||
# 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"
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user