Skip to content

feat: Dashboard layout#39

Merged
Luluameh merged 2 commits into
LightForgeHub:mainfrom
devchant:dashboard_layout
Feb 20, 2026
Merged

feat: Dashboard layout#39
Luluameh merged 2 commits into
LightForgeHub:mainfrom
devchant:dashboard_layout

Conversation

@devchant
Copy link
Copy Markdown
Contributor

@devchant devchant commented Feb 20, 2026

close: #36

SUMARY

The dashboard_layout branch implements a fully responsive dashboard system for SkillSphere, designed to support both desktop and mobile experiences with a consistent layout structure. The architecture centers on a reusable DashboardShell layout that integrates a sticky desktop sidebar, a responsive header, and a mobile drawer navigation system. The header includes branding, a profile switcher with dropdown behavior, keyboard accessibility (Escape handling), and localStorage persistence for profile state. The sidebar provides structured navigation with active-route highlighting and icon-based links, while maintaining a separate section for support and AI tools.

In addition to the layout infrastructure, eight dashboard sub-pages were created for core platform modules including Home, Learners, Notifications, Courses, Earnings, Support, and AI. Custom SVG icons and an avatar placeholder asset were added to support the UI design system. The implementation follows a mobile-first approach with a dark theme and purple accent styling, supports multi-profile persistence using localStorage, and ensures consistent spacing and styling across content areas to improve scalability and future feature integration.

### Media assets
https://github.com/user-attachments/assets/606c4825-7204-47e2-93a6-9eed16870c5b

LucySkills.mp4

Summary by CodeRabbit

  • New Features
    • Introduced comprehensive dashboard interface with responsive navigation and mobile drawer support.
    • Added profile selection system with persistent storage across sessions.
    • Created new dashboard sections: AI Chat, Courses, Earnings, Learners, Notifications, and Support.
    • Implemented mobile-friendly design with sidebar and header components.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete responsive dashboard UI system with a DashboardShell layout, Header component with profile management, responsive Sidebar with mobile drawer support, seven dashboard route pages, and conditionally hides the app header/footer on dashboard paths.

Changes

Cohort / File(s) Summary
Dashboard Layout Foundation
src/components/dashboard/DashboardShell.tsx, src/app/dashboard/layout.tsx
Introduces DashboardShell (client component managing drawer state) and DashboardLayout wrapper; together provide the structural foundation for dashboard routing with responsive mobile drawer integration.
Dashboard Navigation Components
src/components/dashboard/Header.tsx, src/components/dashboard/Sidebar.tsx
Header component implements top bar with profile selection, localStorage persistence, mobile modal overlay, and keyboard handling; Sidebar provides responsive navigation (mobile dropdown and desktop sidebar) with profile switching, active path-based state, and MobileDrawer component for slide-in mobile navigation.
Dashboard Pages
src/app/dashboard/page.tsx, src/app/dashboard/ai/page.tsx, src/app/dashboard/courses/page.tsx, src/app/dashboard/earnings/page.tsx, src/app/dashboard/learners/page.tsx, src/app/dashboard/notifications/page.tsx, src/app/dashboard/support/page.tsx
Seven straightforward page components (Home, AI Chat, Courses, Earnings, Learners, Notifications, Support) each rendering static placeholder content with headings and descriptions.
App Layout Updates
src/components/layout/AppLayout.tsx
Adds client-side path detection via usePathname() to conditionally render header/footer slots only when not on dashboard routes; adjusts container styling based on route context; makes headerSlot and footerSlot optional.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A dashboard springs to life with care,
Sidebars and headers everywhere!
Mobile drawers slide with grace,
Navigation finds its place,
Hopping through responsive design,
A UI dream, truly divine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Dashboard layout' directly summarizes the main change—implementing a comprehensive dashboard UI with layout components and pages.
Linked Issues check ✅ Passed PR successfully delivers all core requirements from #36: Sidebar with profile section, navigation items (Home, Learners, Notifications, Courses, Earnings), bottom links (Support, AI), active state highlighting, responsive mobile drawer, and clean reusable structure.
Out of Scope Changes check ✅ Passed All changes are scope-aligned with #36 (sidebar/layout implementation). AppLayout.tsx updates are minimal and necessary to integrate the dashboard layout system without introducing unrelated functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (7)
src/app/dashboard/layout.tsx (1)

3-5: Type metadata as Next.js Metadata for stronger type safety.

The metadata export is a plain object literal. Typing it as Metadata from "next" gives you compile-time validation and IDE completions for all supported SEO fields (description, openGraph, etc.).

♻️ Proposed refactor
+import type { Metadata } from "next"

-export const metadata = {
+export const metadata: Metadata = {
   title: "Dashboard - SkillSphere",
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/layout.tsx` around lines 3 - 5, The exported metadata
object should be typed as Next.js Metadata for compile-time checks: add an
import of the Metadata type from "next" (e.g., import type { Metadata } from
"next") and change the declaration of the exported identifier metadata to use
that type (export const metadata: Metadata = { title: "Dashboard - SkillSphere"
}); this will enable proper type validation and IDE completions for the metadata
shape used by Next.js.
src/components/dashboard/Sidebar.tsx (3)

133-133: Desktop sidebar uses a viewport-height percentage (h-[80vh]) which may clip navigation on short screens.

On a short viewport (e.g., 600px), h-[80vh] = 480px, and the sidebar content may overflow without scroll. Consider using h-screen (or min-h-0 inside a flex parent with overflow-y-auto) so the sidebar fills the available space and scrolls when needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Sidebar.tsx` at line 133, The sidebar's fixed class
uses h-[80vh] which can clip content on short viewports; update the Sidebar
component's container (the aside with className starting "hidden md:flex ...")
to use a full-height strategy such as h-screen or make it flex-aware by using
min-h-0 on the flex parent and adding overflow-y-auto on the aside so the
navigation can scroll (adjust the aside's className to replace h-[80vh] with
h-screen or add overflow-y-auto and ensure its parent uses min-h-0).

240-244: Backdrop overlay remains in the DOM when drawer is closed — consider unmounting or adding inert.

When isOpen is false, the backdrop div is still rendered with pointer-events-none but remains in the accessibility tree. Adding aria-hidden={!isOpen} is good, but screen readers may still encounter the element. Consider conditionally rendering the entire drawer, or adding the inert attribute when closed, to fully remove it from the tab order and accessibility tree.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Sidebar.tsx` around lines 240 - 244, The backdrop
div rendered in Sidebar (the element using isOpen and onClose) stays in the DOM
when closed which keeps it accessible; update the Sidebar component so the
backdrop is either conditionally rendered only when isOpen is true or add the
inert attribute when isOpen is false (and keep aria-hidden in sync) to remove it
from the accessibility and tab order; locate the backdrop element that sets
className with "pointer-events-none" and change rendering logic to unmount it
when closed or toggle inert (and ensure onClose remains wired and aria-hidden
reflects the same state).

9-30: profiles, navItems, bottomItems, and classNames are duplicated between Sidebar.tsx and Header.tsx.

Extract these into a shared module (e.g., src/components/dashboard/constants.ts and src/lib/classNames.ts) to keep a single source of truth and reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Sidebar.tsx` around lines 9 - 30, profiles,
navItems, bottomItems and the helper function classNames are duplicated in
Sidebar.tsx and Header.tsx; extract them into shared modules (e.g., a dashboard
constants module exporting profiles, navItems, bottomItems and a separate module
exporting classNames), update Sidebar.tsx and Header.tsx to import these symbols
instead of declaring them locally, and ensure the exported names match exactly
(profiles, navItems, bottomItems, classNames) so existing usages in both
components remain unchanged.
0001-feat-dahoard-layout-with-nav-bar-changes-and-respons.patch (1)

7-8: Asset filenames contain spaces (Avatar Placeholder.png, ai chatbot.svg).

Spaces in public asset filenames require encodeURIComponent at every reference point and are error-prone. Rename to kebab-case (e.g., avatar-placeholder.png, ai-chatbot.svg) for simpler, more reliable URL references.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@0001-feat-dahoard-layout-with-nav-bar-changes-and-respons.patch` around lines
7 - 8, Asset filenames include spaces ("Avatar Placeholder.png", "ai
chatbot.svg") which force encodeURIComponent usage across references; rename
these files to kebab-case (e.g., avatar-placeholder.png, ai-chatbot.svg) and
update all references/imports to match the new names. Specifically, rename the
file entries in the public/assets and public/icons folders and update any usages
in components, CSS, or markup that reference "Avatar Placeholder.png" or "ai
chatbot.svg" so they point to "avatar-placeholder.png" and "ai-chatbot.svg"
respectively. Ensure any bundler/static server references
(src/href/background-image URLs or <img> src) are updated and tested to load
without encoding. Run a quick search for the original filenames to find and
replace every occurrence.
src/components/dashboard/DashboardShell.tsx (1)

17-21: Nested <aside> elements and redundant mobile section inside a desktop-only container.

The outer <aside className="hidden md:block ..."> wraps <Sidebar />, which itself renders an <aside className="hidden md:flex ..."> for desktop and a <div className="md:hidden ..."> for mobile. The nested <aside> is a semantic smell, and the Sidebar's mobile dropdown is dead markup here since the parent is hidden below md.

Consider either rendering <Sidebar /> directly without the wrapping <aside> and sticky <div>, or refactoring Sidebar to not emit its own <aside> wrapper when used inside DashboardShell.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/DashboardShell.tsx` around lines 17 - 21,
DashboardShell currently wraps <Sidebar /> with an outer <aside
className="hidden md:block md:w-64 flex-shrink-0"> and a sticky wrapper, causing
nested <aside> semantics and leaving Sidebar's mobile markup dead; fix by either
removing the outer <aside> and sticky <div> in DashboardShell and render
<Sidebar /> directly (so Sidebar controls its own wrappers), or update Sidebar
to accept a prop (e.g., noWrapper or embedMode) and when that prop is true have
Sidebar omit its own <aside> wrapper and mobile markup; adjust DashboardShell to
pass that prop if you choose the latter and keep the sticky positioning
consistent (move sticky logic into Sidebar or DashboardShell as needed).
src/components/layout/AppLayout.tsx (1)

1-3: Converting AppLayout to a client component unnecessarily disables server rendering for the entire app.

Adding "use client" here forces all routes through a client boundary since AppLayout wraps the entire app at the root level. Since dashboard/layout.tsx already provides its own separate layout, consider using Next.js route groups (e.g., (marketing) and (dashboard)) instead. This allows the header and footer to be defined in a (marketing) layout while keeping the root layout as a server component, preserving SSR benefits for non-dashboard pages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/AppLayout.tsx` around lines 1 - 3, AppLayout is marked
with "use client", which forces a client boundary at the root and disables SSR
for the whole app; remove the "use client" directive from AppLayout and instead
move any client-only logic into dedicated client components (e.g., separate
HeaderClient or FooterClient) or into the dashboard-specific layout
(dashboard/layout.tsx). Refactor by converting AppLayout back to a server
component, create route groups like (marketing) and (dashboard) so Header/Footer
live in the (marketing) layout while dashboard/layout.tsx handles
dashboard-specific client needs, and ensure any hooks requiring a client (e.g.,
usePathname) are only used inside client components referenced from those
layouts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/dashboard/ai/page.tsx`:
- Around line 1-8: The paragraph in the AIChatPage component uses the
low-contrast class "text-slate-600"; update the paragraph's className inside
export default function AIChatPage() to "mt-4 text-slate-400" (replace
"text-slate-600" with "text-slate-400") so the paragraph matches the other
contrast fixes.

In `@src/app/dashboard/courses/page.tsx`:
- Around line 1-8: The paragraph in the CoursesPage component uses a
low-contrast class "text-slate-600"; update the <p> element in the CoursesPage
function to use "text-slate-400" instead of "text-slate-600" to match the other
fixes and improve contrast (locate the paragraph inside the CoursesPage export
default function CoursesPage and change its className accordingly).

In `@src/app/dashboard/earnings/page.tsx`:
- Around line 1-8: The paragraph in the EarningsPage component uses the
low-contrast class "text-slate-600"; update the <p> element inside the
EarningsPage function to use "text-slate-400" instead to match the other pages
and improve contrast (locate the paragraph in the EarningsPage component and
replace the className value accordingly).

In `@src/app/dashboard/layout.tsx`:
- Around line 7-9: The file references the React namespace for the children type
(DashboardLayout({ children }: { children: React.ReactNode })), but React isn't
imported, causing a TypeScript error; fix by adding an explicit type
import—either import type { ReactNode } from 'react' and change the prop type to
{ children: ReactNode } or import React (import React from 'react') so
React.ReactNode resolves; update the DashboardLayout signature accordingly.

In `@src/app/dashboard/learners/page.tsx`:
- Around line 1-8: The paragraph in the LearnersPage component uses the
low-contrast class "text-slate-600"; update the JSX in the LearnersPage function
to use "text-slate-400" for the <p> element so it matches the fix applied in
support/page.tsx and improves contrast (locate the LearnersPage export default
function and change the paragraph's className from "mt-4 text-slate-600" to
"mt-4 text-slate-400").

In `@src/app/dashboard/notifications/page.tsx`:
- Around line 1-8: In the NotificationsPage component, update the paragraph's
tailwind text color from "text-slate-600" to "text-slate-400" to match the
contrast fix used elsewhere; locate the <p> element inside the NotificationsPage
function and replace the className value accordingly.

In `@src/app/dashboard/page.tsx`:
- Around line 1-8: The paragraph in the DashboardHome component uses
"text-slate-600" which has the same contrast issue; update the class on the <p>
element inside the DashboardHome function to use "text-slate-400" instead of
"text-slate-600" so the paragraph matches the corrected contrast level.

In `@src/app/dashboard/support/page.tsx`:
- Line 5: The paragraph in support/page.tsx uses the utility class
text-slate-600 which has insufficient contrast on the DashboardShell background;
update the paragraph's text color to a higher-contrast utility (e.g.,
text-slate-400 or text-slate-300) to meet WCAG AA, and make the same change for
the equivalent <p> in learners/page.tsx, page.tsx, ai/page.tsx,
earnings/page.tsx, courses/page.tsx, and notifications/page.tsx; reference the
DashboardShell background and replace text-slate-600 with text-slate-400 (or
text-slate-300) in those components.

In `@src/components/dashboard/DashboardShell.tsx`:
- Around line 15-21: The layout applies md:pl-72 on the DashboardShell container
while rendering the Sidebar component in-flow, causing double horizontal offset;
fix by removing the md:pl-72 class from the outer div in DashboardShell (or
alternatively make Sidebar positioned fixed/absolute and keep md:pl-72) so that
either the flex layout (with <Sidebar />) controls spacing or the sidebar is
taken out of flow—update the class list in DashboardShell to eliminate the
conflicting md:pl-72 when keeping the in-flow Sidebar.

In `@src/components/dashboard/Header.tsx`:
- Around line 80-86: The profile button's onClick only opens the mobile modal
when window.innerWidth < 768, so on desktop the click does nothing; update the
onClick handler in Header (the button using setMobileProfileModalOpen) to either
open the desktop profile menu/trigger the existing desktop handler or
remove/disable the click affordance on larger viewports: detect viewport
(window.innerWidth) and for >=768 call the desktop profile toggle (or no-op and
hide the chevron) while for <768 call setMobileProfileModalOpen(true); ensure
you reference the same state setter (setMobileProfileModalOpen) and any desktop
toggle function (e.g., openDesktopProfileMenu or similar) so desktop clicks
produce the expected behavior or the visual affordance is removed.
- Line 19: The component defines open and setOpen but never opens the dropdown,
making the Escape-key useEffect no-op; either remove the open state and the
Escape-key useEffect entirely, or wire up open by adding a handler that calls
setOpen(true)/toggle (e.g., on the profile button's onClick) and ensure the
existing useEffect (the Escape key listener) closes via setOpen(false) and
cleans up its event listener; update any JSX that conditionally renders the
dropdown to use open so the Escape handler becomes effective.
- Line 2: The import Link from "next/link" in Header.tsx is unused; remove the
unused symbol to clean up the module. Open the Header component in Header.tsx,
delete the import statement "import Link from 'next/link'" (or replace it with a
used import if you intended to use Next's Link), and ensure there are no
references to the Link symbol elsewhere in that file.
- Around line 22-33: The write effect is clobbering localStorage on mount
because it runs with the initial default profile before the read effect's
setProfile applies; update Header.tsx to avoid writing on initial mount by
adding a mounted/ref guard or by combining the read/write into a single effect:
read localStorage once on mount (using localStorage.getItem and setProfile) and
only enable subsequent writes in the effect that watches profile (using a
mounted boolean or isInitialized flag) so that setItem(JSON.stringify(profile))
is skipped for the initial render where profile === profiles[0]; reference the
existing useEffect blocks, the profile state, and setProfile in your change.

In `@src/components/dashboard/Sidebar.tsx`:
- Around line 22-26: Profile selection is duplicated across Sidebar, Header, and
MobileDrawer because each component independently reads 'dashboard_profile' from
localStorage and maintains its own useState, causing UI divergence; lift the
profile state into DashboardShell (or a new React context) and pass the current
profile and a setter down to Header, Sidebar, and MobileDrawer instead of
reading localStorage in each component: remove localStorage reads and local
useState from Sidebar, Header, and MobileDrawer, initialize the shared state
once in DashboardShell (e.g., useState/get from localStorage on mount) and
update localStorage inside the shared setter so all three components consume the
same profile via props or context and remain synchronized.
- Around line 268-271: The "Profiles" section is missing interactive buttons —
call the existing handleSelectProfile function to let users switch profiles: in
Sidebar/MobileDrawer render a list of profile buttons (map over the profiles
array or prop that the component already receives) under the "Profiles" heading,
wire each button's onClick to handleSelectProfile(profileId or profile), and
visually mark the active profile (using the same active className logic used
elsewhere in Sidebar). Ensure the buttons use the existing profile display data
and close the drawer if MobileDrawer has a close function so mobile UX behaves
like the desktop profile selector.

---

Nitpick comments:
In `@0001-feat-dahoard-layout-with-nav-bar-changes-and-respons.patch`:
- Around line 7-8: Asset filenames include spaces ("Avatar Placeholder.png", "ai
chatbot.svg") which force encodeURIComponent usage across references; rename
these files to kebab-case (e.g., avatar-placeholder.png, ai-chatbot.svg) and
update all references/imports to match the new names. Specifically, rename the
file entries in the public/assets and public/icons folders and update any usages
in components, CSS, or markup that reference "Avatar Placeholder.png" or "ai
chatbot.svg" so they point to "avatar-placeholder.png" and "ai-chatbot.svg"
respectively. Ensure any bundler/static server references
(src/href/background-image URLs or <img> src) are updated and tested to load
without encoding. Run a quick search for the original filenames to find and
replace every occurrence.

In `@src/app/dashboard/layout.tsx`:
- Around line 3-5: The exported metadata object should be typed as Next.js
Metadata for compile-time checks: add an import of the Metadata type from "next"
(e.g., import type { Metadata } from "next") and change the declaration of the
exported identifier metadata to use that type (export const metadata: Metadata =
{ title: "Dashboard - SkillSphere" }); this will enable proper type validation
and IDE completions for the metadata shape used by Next.js.

In `@src/components/dashboard/DashboardShell.tsx`:
- Around line 17-21: DashboardShell currently wraps <Sidebar /> with an outer
<aside className="hidden md:block md:w-64 flex-shrink-0"> and a sticky wrapper,
causing nested <aside> semantics and leaving Sidebar's mobile markup dead; fix
by either removing the outer <aside> and sticky <div> in DashboardShell and
render <Sidebar /> directly (so Sidebar controls its own wrappers), or update
Sidebar to accept a prop (e.g., noWrapper or embedMode) and when that prop is
true have Sidebar omit its own <aside> wrapper and mobile markup; adjust
DashboardShell to pass that prop if you choose the latter and keep the sticky
positioning consistent (move sticky logic into Sidebar or DashboardShell as
needed).

In `@src/components/dashboard/Sidebar.tsx`:
- Line 133: The sidebar's fixed class uses h-[80vh] which can clip content on
short viewports; update the Sidebar component's container (the aside with
className starting "hidden md:flex ...") to use a full-height strategy such as
h-screen or make it flex-aware by using min-h-0 on the flex parent and adding
overflow-y-auto on the aside so the navigation can scroll (adjust the aside's
className to replace h-[80vh] with h-screen or add overflow-y-auto and ensure
its parent uses min-h-0).
- Around line 240-244: The backdrop div rendered in Sidebar (the element using
isOpen and onClose) stays in the DOM when closed which keeps it accessible;
update the Sidebar component so the backdrop is either conditionally rendered
only when isOpen is true or add the inert attribute when isOpen is false (and
keep aria-hidden in sync) to remove it from the accessibility and tab order;
locate the backdrop element that sets className with "pointer-events-none" and
change rendering logic to unmount it when closed or toggle inert (and ensure
onClose remains wired and aria-hidden reflects the same state).
- Around line 9-30: profiles, navItems, bottomItems and the helper function
classNames are duplicated in Sidebar.tsx and Header.tsx; extract them into
shared modules (e.g., a dashboard constants module exporting profiles, navItems,
bottomItems and a separate module exporting classNames), update Sidebar.tsx and
Header.tsx to import these symbols instead of declaring them locally, and ensure
the exported names match exactly (profiles, navItems, bottomItems, classNames)
so existing usages in both components remain unchanged.

In `@src/components/layout/AppLayout.tsx`:
- Around line 1-3: AppLayout is marked with "use client", which forces a client
boundary at the root and disables SSR for the whole app; remove the "use client"
directive from AppLayout and instead move any client-only logic into dedicated
client components (e.g., separate HeaderClient or FooterClient) or into the
dashboard-specific layout (dashboard/layout.tsx). Refactor by converting
AppLayout back to a server component, create route groups like (marketing) and
(dashboard) so Header/Footer live in the (marketing) layout while
dashboard/layout.tsx handles dashboard-specific client needs, and ensure any
hooks requiring a client (e.g., usePathname) are only used inside client
components referenced from those layouts.

Comment on lines +1 to +8
export default function AIChatPage() {
return (
<div>
<h2 className="text-2xl font-semibold">AI Chat Bot</h2>
<p className="mt-4 text-slate-600">Chat with the AI assistant here.</p>
</div>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same text-slate-600 contrast issue.

Apply the same text-slate-400 fix to the paragraph. No other concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/ai/page.tsx` around lines 1 - 8, The paragraph in the
AIChatPage component uses the low-contrast class "text-slate-600"; update the
paragraph's className inside export default function AIChatPage() to "mt-4
text-slate-400" (replace "text-slate-600" with "text-slate-400") so the
paragraph matches the other contrast fixes.

Comment on lines +1 to +8
export default function CoursesPage() {
return (
<div>
<h2 className="text-2xl font-semibold">Courses</h2>
<p className="mt-4 text-slate-600">Create, edit, and manage courses here.</p>
</div>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same text-slate-600 contrast issue.

Apply the same text-slate-400 fix to the paragraph. No other concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/courses/page.tsx` around lines 1 - 8, The paragraph in the
CoursesPage component uses a low-contrast class "text-slate-600"; update the <p>
element in the CoursesPage function to use "text-slate-400" instead of
"text-slate-600" to match the other fixes and improve contrast (locate the
paragraph inside the CoursesPage export default function CoursesPage and change
its className accordingly).

Comment on lines +1 to +8
export default function EarningsPage() {
return (
<div>
<h2 className="text-2xl font-semibold">Earnings</h2>
<p className="mt-4 text-slate-600">View revenue, payouts, and transaction history.</p>
</div>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same text-slate-600 contrast issue.

Apply the same text-slate-400 fix to the paragraph. No other concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/earnings/page.tsx` around lines 1 - 8, The paragraph in the
EarningsPage component uses the low-contrast class "text-slate-600"; update the
<p> element inside the EarningsPage function to use "text-slate-400" instead to
match the other pages and improve contrast (locate the paragraph in the
EarningsPage component and replace the className value accordingly).

Comment on lines +7 to +9
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return <DashboardShell>{children}</DashboardShell>
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Checking if layout.tsx exists and viewing its imports ==="
if [ -f "src/app/dashboard/layout.tsx" ]; then
  echo "File exists. First 15 lines:"
  head -n 15 "src/app/dashboard/layout.tsx"
else
  echo "File not found at src/app/dashboard/layout.tsx"
  echo "Searching for layout.tsx:"
  fd "layout.tsx"
fi

echo ""
echo "=== Checking tsconfig.json for global type declarations ==="
fd "tsconfig.json" -t f --exec head -n 50 {}

echo ""
echo "=== Checking for global.d.ts or next-env.d.ts ==="
fd "global.d.ts" -o "next-env.d.ts" -t f

echo ""
echo "=== Checking DashboardShell.tsx for 'use client' ==="
fd "DashboardShell" --extension tsx --extension ts --exec grep -n "use client" {}

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 1385


🏁 Script executed:

#!/bin/bash
echo "=== Checking .next/types directory for React declarations ==="
if [ -d ".next/types" ]; then
  find .next/types -name "*.ts" -type f | head -10
  echo ""
  echo "Searching for React declarations in .next/types:"
  find .next/types -name "*.ts" -type f -exec grep -l "React" {} \; | head -5
else
  echo ".next/types directory does not exist (build likely hasn't run)"
fi

echo ""
echo "=== Checking node_modules for `@types/react` declarations ==="
if [ -f "node_modules/@types/react/index.d.ts" ]; then
  echo "@types/react exists"
  head -n 5 "node_modules/@types/react/index.d.ts"
else
  echo "Checking what React type declarations are available:"
  find node_modules -path "*/react*.d.ts" -type f 2>/dev/null | head -5
fi

echo ""
echo "=== Full layout.tsx content for complete context ==="
cat -n "src/app/dashboard/layout.tsx"

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 704


React.ReactNode used without importing React — TypeScript compilation error.

Line 7 references the React namespace (React.ReactNode) but the file has no import React from 'react' or import type { ReactNode } from 'react'. With the new JSX transform, React doesn't need to be in scope for JSX syntax, but the React namespace still requires an explicit import for type references like React.ReactNode. With strict: true in tsconfig.json, this causes a Cannot find namespace 'React' TypeScript error.

🛠️ Proposed fix
 import DashboardShell from "@/components/dashboard/DashboardShell"
+import type { ReactNode } from "react"

 export const metadata = {
   title: "Dashboard - SkillSphere",
 }

-export default function DashboardLayout({ children }: { children: React.ReactNode }) {
+export default function DashboardLayout({ children }: { children: ReactNode }) {
   return <DashboardShell>{children}</DashboardShell>
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return <DashboardShell>{children}</DashboardShell>
}
import type { ReactNode } from "react"
export default function DashboardLayout({ children }: { children: ReactNode }) {
return <DashboardShell>{children}</DashboardShell>
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/layout.tsx` around lines 7 - 9, The file references the
React namespace for the children type (DashboardLayout({ children }: { children:
React.ReactNode })), but React isn't imported, causing a TypeScript error; fix
by adding an explicit type import—either import type { ReactNode } from 'react'
and change the prop type to { children: ReactNode } or import React (import
React from 'react') so React.ReactNode resolves; update the DashboardLayout
signature accordingly.

Comment on lines +1 to +8
export default function LearnersPage() {
return (
<div>
<h2 className="text-2xl font-semibold">Learners</h2>
<p className="mt-4 text-slate-600">Manage and review learner activity here.</p>
</div>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same text-slate-600 contrast issue as support/page.tsx.

Apply the same text-slate-400 fix to the paragraph here. No other concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/learners/page.tsx` around lines 1 - 8, The paragraph in the
LearnersPage component uses the low-contrast class "text-slate-600"; update the
JSX in the LearnersPage function to use "text-slate-400" for the <p> element so
it matches the fix applied in support/page.tsx and improves contrast (locate the
LearnersPage export default function and change the paragraph's className from
"mt-4 text-slate-600" to "mt-4 text-slate-400").

onToggleMenu?: () => void
}) {
const [profile, setProfile] = useState(profiles[0])
const [open, setOpen] = useState(false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

open state is never set to true — the Escape-key effect is dead code.

open (line 19) is initialised to false and no code path sets it to true. The useEffect on lines 35-42 that listens for Escape to close a dropdown never activates. This appears to be a leftover from an incomplete desktop profile dropdown. Either wire it up or remove it to avoid confusion.

Also applies to: 35-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Header.tsx` at line 19, The component defines open
and setOpen but never opens the dropdown, making the Escape-key useEffect no-op;
either remove the open state and the Escape-key useEffect entirely, or wire up
open by adding a handler that calls setOpen(true)/toggle (e.g., on the profile
button's onClick) and ensure the existing useEffect (the Escape key listener)
closes via setOpen(false) and cleans up its event listener; update any JSX that
conditionally renders the dropdown to use open so the Escape handler becomes
effective.

Comment on lines +22 to +33
useEffect(() => {
try {
const v = localStorage.getItem("dashboard_profile")
if (v) setProfile(JSON.parse(v))
} catch (e) {}
}, [])

useEffect(() => {
try {
localStorage.setItem("dashboard_profile", JSON.stringify(profile))
} catch (e) {}
}, [profile])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

localStorage write on mount briefly clobbers the persisted profile.

The write effect (lines 29-33) runs during the same mount cycle as the read effect (lines 22-27). On the first render profile is still the default (profiles[0]), so the write effect overwrites localStorage with the default before the read effect's setProfile triggers a re-render. A cross-tab storage listener would briefly see the wrong value.

Guard the write to skip the initial mount, or combine both effects.

Example: guard with a mounted ref
+  const mounted = useRef(false)
+
   useEffect(() => {
     try {
       const v = localStorage.getItem("dashboard_profile")
-      if (v) setProfile(JSON.parse(v))
+      if (v) setProfile(JSON.parse(v))
     } catch (e) {}
+    mounted.current = true
   }, [])

   useEffect(() => {
+    if (!mounted.current) return
     try {
       localStorage.setItem("dashboard_profile", JSON.stringify(profile))
     } catch (e) {}
   }, [profile])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
try {
const v = localStorage.getItem("dashboard_profile")
if (v) setProfile(JSON.parse(v))
} catch (e) {}
}, [])
useEffect(() => {
try {
localStorage.setItem("dashboard_profile", JSON.stringify(profile))
} catch (e) {}
}, [profile])
const mounted = useRef(false)
useEffect(() => {
try {
const v = localStorage.getItem("dashboard_profile")
if (v) setProfile(JSON.parse(v))
} catch (e) {}
mounted.current = true
}, [])
useEffect(() => {
if (!mounted.current) return
try {
localStorage.setItem("dashboard_profile", JSON.stringify(profile))
} catch (e) {}
}, [profile])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Header.tsx` around lines 22 - 33, The write effect
is clobbering localStorage on mount because it runs with the initial default
profile before the read effect's setProfile applies; update Header.tsx to avoid
writing on initial mount by adding a mounted/ref guard or by combining the
read/write into a single effect: read localStorage once on mount (using
localStorage.getItem and setProfile) and only enable subsequent writes in the
effect that watches profile (using a mounted boolean or isInitialized flag) so
that setItem(JSON.stringify(profile)) is skipped for the initial render where
profile === profiles[0]; reference the existing useEffect blocks, the profile
state, and setProfile in your change.

Comment on lines +80 to +86
<button
onClick={() => {
// open mobile modal only on small screens
if (typeof window !== "undefined" && window.innerWidth < 768) {
setMobileProfileModalOpen(true)
}
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Profile button is non-functional on desktop.

The click handler only opens the mobile modal when window.innerWidth < 768. On desktop viewports, clicking the profile button/chevron does nothing. This leaves desktop users with no way to switch profiles from the header (they must use the sidebar dropdown instead). If that's intentional, consider hiding the chevron affordance on desktop to avoid a misleading clickable appearance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Header.tsx` around lines 80 - 86, The profile
button's onClick only opens the mobile modal when window.innerWidth < 768, so on
desktop the click does nothing; update the onClick handler in Header (the button
using setMobileProfileModalOpen) to either open the desktop profile menu/trigger
the existing desktop handler or remove/disable the click affordance on larger
viewports: detect viewport (window.innerWidth) and for >=768 call the desktop
profile toggle (or no-op and hide the chevron) while for <768 call
setMobileProfileModalOpen(true); ensure you reference the same state setter
(setMobileProfileModalOpen) and any desktop toggle function (e.g.,
openDesktopProfileMenu or similar) so desktop clicks produce the expected
behavior or the visual affordance is removed.

Comment on lines +22 to +26
const profiles = [
{ id: "nora", name: "Miss Nora" },
{ id: "sam", name: "Mr Sam" },
{ id: "lulu", name: "Mrs Lulu" },
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Profile state is duplicated across Header, Sidebar, and MobileDrawer — they will diverge within the same tab.

Each component independently reads dashboard_profile from localStorage on mount and maintains its own useState. The storage event only fires in other tabs, so changing the profile in the Sidebar won't update the Header (or vice versa) without a page reload. This leads to inconsistent profile display.

Lift the profile state into DashboardShell (or a shared React context) and pass it down as props/context to Header, Sidebar, and MobileDrawer.

Also applies to: 37-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Sidebar.tsx` around lines 22 - 26, Profile selection
is duplicated across Sidebar, Header, and MobileDrawer because each component
independently reads 'dashboard_profile' from localStorage and maintains its own
useState, causing UI divergence; lift the profile state into DashboardShell (or
a new React context) and pass the current profile and a setter down to Header,
Sidebar, and MobileDrawer instead of reading localStorage in each component:
remove localStorage reads and local useState from Sidebar, Header, and
MobileDrawer, initialize the shared state once in DashboardShell (e.g.,
useState/get from localStorage on mount) and update localStorage inside the
shared setter so all three components consume the same profile via props or
context and remain synchronized.

Comment on lines +268 to +271
<div className="mt-3">
<div className="text-xs text-gray-300 mb-2">Profiles</div>

</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

MobileDrawer's profile selection UI is incomplete — buttons are missing.

The "Profiles" heading is rendered (line 269) but no profile buttons follow. handleSelectProfile (lines 230-236) is defined but never called. Users opening the mobile drawer cannot switch profiles.

Proposed fix: render profile buttons
          <div className="mt-3">
            <div className="text-xs text-gray-300 mb-2">Profiles</div>
-          
+            <div className="space-y-1">
+              {profiles.map((p) => (
+                <button
+                  key={p.id}
+                  onClick={() => handleSelectProfile(p)}
+                  className={classNames(
+                    "w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg text-sm",
+                    p.id === profile.id ? "bg-purple-600/20 text-white" : "text-gray-300 hover:bg-white/5"
+                  )}
+                >
+                  <img src={`/assets/${encodeURIComponent("Avatar Placeholder.png")}`} alt="avatar" className="w-7 h-7 rounded-full object-cover" />
+                  <span>{p.name}</span>
+                </button>
+              ))}
+            </div>
          </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="mt-3">
<div className="text-xs text-gray-300 mb-2">Profiles</div>
</div>
<div className="mt-3">
<div className="text-xs text-gray-300 mb-2">Profiles</div>
<div className="space-y-1">
{profiles.map((p) => (
<button
key={p.id}
onClick={() => handleSelectProfile(p)}
className={classNames(
"w-full text-left flex items-center gap-3 px-3 py-2 rounded-lg text-sm",
p.id === profile.id ? "bg-purple-600/20 text-white" : "text-gray-300 hover:bg-white/5"
)}
>
<img src={`/assets/${encodeURIComponent("Avatar Placeholder.png")}`} alt="avatar" className="w-7 h-7 rounded-full object-cover" />
<span>{p.name}</span>
</button>
))}
</div>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/Sidebar.tsx` around lines 268 - 271, The "Profiles"
section is missing interactive buttons — call the existing handleSelectProfile
function to let users switch profiles: in Sidebar/MobileDrawer render a list of
profile buttons (map over the profiles array or prop that the component already
receives) under the "Profiles" heading, wire each button's onClick to
handleSelectProfile(profileId or profile), and visually mark the active profile
(using the same active className logic used elsewhere in Sidebar). Ensure the
buttons use the existing profile display data and close the drawer if
MobileDrawer has a close function so mobile UX behaves like the desktop profile
selector.

@Luluameh Luluameh merged commit 8e342b8 into LightForgeHub:main Feb 20, 2026
1 check passed
Luluameh pushed a commit that referenced this pull request May 29, 2026
…row, subscriptions

closes #194
closes #195
closes #196
closes #197

#196 — Platform Fee Whitelist for Specific Assets
  - New `AssetFeeBps(Address)` storage + admin setters/clearers.
  - `set_asset_fee_bps` / `clear_asset_fee_bps` / `get_asset_fee_bps`.
  - `internal_settle` and the two new entry points route through
    `platform_fee_for_token`, which prefers the per-asset override
    over the global tiered config.

#197 — Protocol Insurance Fund Allocation
  - New `InsuranceVaultAddress` + per-token `InsuranceVaultBalance`.
  - Admin sets the vault via `set_insurance_vault`.
  - `route_insurance_cut` slices 1% of the platform fee (after
    referral reward) into the per-token vault balance on every
    settlement / approval / collection. Skipped when no vault is
    configured so deployments upgrade gracefully.
  - `withdraw_insurance(token, recipient, amount)` admin-only payout
    decrements the vault balance and transfers from the contract.

#194 — Fixed-Price Escrow for Milestone Tasks
  - New `FixedPriceSession` struct + `FixedPriceStatus` enum stored
    under `DataKey::FixedPriceSession(id)`.
  - `initialize_fixed_price_session(seeker, expert, token, amount,
    metadata_cid)` locks funds.
  - `approve_fixed_price_session(seeker, id)` releases to expert
    using the same fee + insurance routing as streaming sessions.
  - `dispute_fixed_price_session(id, seeker, reason, evidence)`
    moves the session into the existing dispute pathway.
  - No streaming math runs on fixed-price sessions — strictly lock
    then approve / dispute.

#195 — Expert Subscription Retainers
  - New `Subscription` struct keyed by (seeker, expert).
  - `subscribe(seeker, expert, token, monthly_fee, months)`
    prepays `monthly_fee * months` to the contract.
  - `collect_subscription_payment(expert, seeker)` deducts one
    month's fee per period (`SUBSCRIPTION_PERIOD_SECS = 30 days`),
    routes fee + insurance like a settlement, and credits the
    expert's virtual balance.
  - `claim_subscription_balance(expert, seeker)` pays the
    accumulated balance on-chain.

New errors: FixedPriceSessionAlreadyFinalised (#34),
SubscriptionNotFound (#35), SubscriptionAlreadyCollected (#36),
SubscriptionExpired (#37), InsuranceVaultUnset (#38),
InsufficientInsuranceBalance (#39).

Tests cover: asset-fee set/get/clear + applied-at-settlement,
insurance accrual + admin withdraw + skipped-when-unset,
fixed-price lock → approve happy path + dispute + double-approve
panic, and subscription subscribe → collect → period-guard →
advance-and-collect → claim flow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build Creator Dashboard sidebar navigation layout

2 participants