feat: Dashboard layout#39
Conversation
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (7)
src/app/dashboard/layout.tsx (1)
3-5: Typemetadataas Next.jsMetadatafor stronger type safety.The
metadataexport is a plain object literal. Typing it asMetadatafrom"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 usingh-screen(ormin-h-0inside a flex parent withoverflow-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 addinginert.When
isOpenisfalse, the backdrop div is still rendered withpointer-events-nonebut remains in the accessibility tree. Addingaria-hidden={!isOpen}is good, but screen readers may still encounter the element. Consider conditionally rendering the entire drawer, or adding theinertattribute 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, andclassNamesare duplicated between Sidebar.tsx and Header.tsx.Extract these into a shared module (e.g.,
src/components/dashboard/constants.tsandsrc/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
encodeURIComponentat 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 ishiddenbelowmd.Consider either rendering
<Sidebar />directly without the wrapping<aside>and sticky<div>, or refactoringSidebarto not emit its own<aside>wrapper when used insideDashboardShell.🤖 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: ConvertingAppLayoutto a client component unnecessarily disables server rendering for the entire app.Adding
"use client"here forces all routes through a client boundary sinceAppLayoutwraps the entire app at the root level. Sincedashboard/layout.tsxalready 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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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).
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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).
| export default function DashboardLayout({ children }: { children: React.ReactNode }) { | ||
| return <DashboardShell>{children}</DashboardShell> | ||
| } |
There was a problem hiding this comment.
🧩 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.
| 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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| 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]) |
There was a problem hiding this comment.
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.
| 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.
| <button | ||
| onClick={() => { | ||
| // open mobile modal only on small screens | ||
| if (typeof window !== "undefined" && window.innerWidth < 768) { | ||
| setMobileProfileModalOpen(true) | ||
| } | ||
| }} |
There was a problem hiding this comment.
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.
| const profiles = [ | ||
| { id: "nora", name: "Miss Nora" }, | ||
| { id: "sam", name: "Mr Sam" }, | ||
| { id: "lulu", name: "Mrs Lulu" }, | ||
| ] |
There was a problem hiding this comment.
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.
| <div className="mt-3"> | ||
| <div className="text-xs text-gray-300 mb-2">Profiles</div> | ||
|
|
||
| </div> |
There was a problem hiding this comment.
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.
| <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.
…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.
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