Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 59 additions & 13 deletions src/app/dashboard/courses/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ import SummaryTabContent from "@/components/dashboard/SummaryTabContent";
import ResourcesTabContent from "@/components/dashboard/ResourcesTabContent";
import TaskTabContent from "@/components/dashboard/TaskTabContent";
import CourseContentTrackerSidebar from "@/components/dashboard/CourseContentTrackerSidebar";
import CourseLearningHeader from "@/components/dashboard/CourseLearningHeader";
import CourseVideoPlayer from "@/components/dashboard/CourseVideoPlayer";
import { Checkbox } from "@/components/ui/Checkbox";
import image1 from "../../../../../public/Image (1).png";

type TabId = "overview" | "resources" | "tasks" | "summary";

const LESSONS = [
{ id: 1, title: "Lesson 1: Intro to Digital Technology", duration: "5 min" },
{ id: 2, title: "Lesson 2: Blockchain Basics", duration: "5 min" },
{ id: 3, title: "Lesson 3: Smart Contracts", duration: "5 min" },
{ id: 4, title: "Lesson 4: DeFi Fundamentals", duration: "5 min" },
{ id: 5, title: "Lesson 5: Web3 Wallets", duration: "5 min" },
{ id: 6, title: "Lesson 6: dApp Development", duration: "5 min" },
{ id: 7, title: "Lesson 7: Token Standards", duration: "5 min" },
];

interface Params {
id: string;
}
Expand Down Expand Up @@ -106,6 +117,8 @@ const COURSE_DATA: Record<
export default function CourseDetailsPage({ params }: { params: Params }) {
const courseData = COURSE_DATA[params.id];
const [activeTab, setActiveTab] = useState<TabId>("overview");
const [currentLesson, setCurrentLesson] = useState(0);
const [completed, setCompleted] = useState(false);
Comment on lines 117 to +121
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
# Check how other page components handle params in this codebase
rg -n "params.*:\s*\{.*params:" --type=ts --type=tsx -g '*/page.tsx' -A 2 || \
rg -n "params\}" src/app --include="*.tsx" -A 2

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 544


🏁 Script executed:

# First, let's look at the actual file mentioned in the review
fd -e tsx -path '*/dashboard/courses/*/page.tsx' -x head -n 130 {}

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 243


🏁 Script executed:

# Check how other page.tsx components handle params in the codebase
fd -e tsx -path '*/page.tsx' -x grep -n "export default function.*params" {} +

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 243


🌐 Web query:

Next.js 15 breaking changes params Promise handling

💡 Result:

In Next.js 15, a breaking change makes params (and searchParams) passed to page, layout, generateMetadata, route handlers, and similar functions in the App Router asynchronous Promises instead of synchronous objects. This supports better streaming, parallel data fetching, and prerendering optimizations by distinguishing request-specific data. Previously (Next.js 14 and earlier): export default function Page({ params }: { params: { slug: string } }) { const slug = params.slug; } Now (Next.js 15+): export default async function Page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; } Key impacts: - Affects dynamic routes like app/blog/[slug]/page.tsx, layouts, route handlers (app/api/[id]/route.ts), generateMetadata. - searchParams in pages is also Promise<{ [key: string]: string | string[] | undefined }> - Client components use useParams which returns a synchronous object (no change). - Synchronous access still works temporarily for migration (with dev/prod warnings), but will be removed in next major version. Migration: - Run codemod: npx @next/codemod@latest next-async-request-api . - Make functions async and await params. - For client components receiving promised params as props: use React.use(params). - Route handlers: export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; } This change is part of broader "Async Request APIs" update including cookies, headers, etc.

Citations:


🏁 Script executed:

# Let me check the actual page file correctly
find src/app -name "page.tsx" | head -20

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 723


🏁 Script executed:

# Now check the specific file mentioned in the review
cat src/app/dashboard/courses/\[id\]/page.tsx | head -130

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 5176


🏁 Script executed:

# Check other dynamic route page.tsx files for params handling
grep -n "export default function.*params" src/app/dashboard/notifications/questions/\[id\]/page.tsx
grep -n "params" src/app/dashboard/notifications/questions/\[id\]/page.tsx | head -5

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 157


🏁 Script executed:

# Check all page.tsx files with dynamic routes for params pattern
find src/app -name "page.tsx" -path "*\[*\]*" -exec grep -l "params" {} \;

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 170


🏁 Script executed:

# Check the [...not-found] catch-all route
cat src/app/\[...not-found\]/page.tsx | head -20

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 228


🏁 Script executed:

# Let's see the full function signature of the notifications/questions page
cat src/app/dashboard/notifications/questions/\[id\]/page.tsx | head -50

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 1520


Next.js 15 breaking change: params is now a Promise and must be unwrapped.

In Next.js 15, params passed to page components became asynchronous (Promise<Params>). The current code directly accesses params.id without unwrapping, which will fail at runtime.

Since this file uses "use client", the function cannot be made async. Instead, use React.use() to unwrap the promise:

🔧 Fix using React.use()
 "use client";

-import React, { useState } from "react";
+import React, { useState, use } from "react";
 import OverviewTabContent from "@/components/dashboard/OverviewTabContent";
 // ... other imports

-export default function CourseDetailsPage({ params }: { params: Params }) {
+export default function CourseDetailsPage({ params }: { params: Promise<Params> }) {
+  const { id } = use(params);
-  const courseData = COURSE_DATA[params.id];
+  const courseData = COURSE_DATA[id];

Note: The codebase already has the correct pattern in src/app/dashboard/notifications/questions/[id]/page.tsx using async/await (without "use client"). This file should follow the same pattern or apply the React.use() workaround shown above.

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

In `@src/app/dashboard/courses/`[id]/page.tsx around lines 114 - 118,
CourseDetailsPage is using params directly even though in Next.js 15 params is a
Promise and this is a "use client" component so you cannot make the component
async; update the component to unwrap the params promise with React.use (e.g.,
call React.use(params) to get resolvedParams and then use resolvedParams.id)
before accessing COURSE_DATA, while keeping the rest of the state hooks
(activeTab, currentLesson, completed) intact; locate CourseDetailsPage and
replace direct uses of params.id with the unwrapped value from
React.use(params).


if (!courseData) {
return (
Expand All @@ -115,28 +128,49 @@ export default function CourseDetailsPage({ params }: { params: Params }) {
);
}

const handlePrevious = () =>
setCurrentLesson((prev) => Math.max(0, prev - 1));
const handleNext = () =>
setCurrentLesson((prev) => Math.min(LESSONS.length - 1, prev + 1));

return (
<div className="space-y-6 pb-12">
{/* Breadcrumb */}
<div className="bg-[#1A1520] border border-[#1D1D1C] rounded-xl px-5 py-3 flex items-center gap-3 text-sm">
<span className="text-white/60 truncate">{courseData.title}</span>
<span className="text-white/30 shrink-0">|</span>
<span className="text-white font-medium truncate">
{LESSONS[currentLesson].title}
</span>
</div>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: video + tabs */}
<div className="lg:col-span-2 space-y-4">
<CourseLearningHeader
courseTitle={courseData.title}
currentLesson={courseData.currentLesson}
<CourseVideoPlayer
thumbnailSrc={courseData.image}
thumbnailAlt={LESSONS[currentLesson].title}
onPrevious={handlePrevious}
onNext={handleNext}
/>
Comment on lines +150 to +155
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

Play button has no handler—clicking does nothing.

The onPlay callback is not passed to CourseVideoPlayer, so clicking the centered play button has no effect. This may confuse users expecting video playback or navigation.

If this is intentional for the UI-only scope of issue #82, consider adding a comment or a placeholder handler (e.g., console.log or a toast) to clarify the button isn't broken.

💡 Optional: Add placeholder handler
 <CourseVideoPlayer
   thumbnailSrc={courseData.image}
   thumbnailAlt={LESSONS[currentLesson].title}
   onPrevious={handlePrevious}
   onNext={handleNext}
+  onPlay={() => {
+    // TODO: Implement video playback
+    console.log("Play video clicked");
+  }}
 />
📝 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
<CourseVideoPlayer
thumbnailSrc={courseData.image}
thumbnailAlt={LESSONS[currentLesson].title}
onPrevious={handlePrevious}
onNext={handleNext}
/>
<CourseVideoPlayer
thumbnailSrc={courseData.image}
thumbnailAlt={LESSONS[currentLesson].title}
onPrevious={handlePrevious}
onNext={handleNext}
onPlay={() => {
// TODO: Implement video playback
console.log("Play video clicked");
}}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/courses/`[id]/page.tsx around lines 147 - 152,
CourseVideoPlayer is rendered without an onPlay prop so the centered play button
is unhandled; add and pass an onPlay handler (e.g., a new handlePlay function or
an inline placeholder) to CourseVideoPlayer where it's instantiated, wiring it
to either start playback or call a placeholder like console.log/toast; update
the parent component (the page that renders CourseVideoPlayer) to define
handlePlay and pass onPlay={handlePlay} alongside thumbnailSrc, thumbnailAlt,
onPrevious, and onNext so clicks on the play button produce an observable
action.


<div className="relative w-full h-64 rounded-lg overflow-hidden bg-gray-900">
<img
src={courseData.image}
alt={courseData.title}
className="w-full h-full object-cover"
{/* Completed checkbox */}
<label className="flex items-center gap-2 cursor-pointer w-fit">
<Checkbox
checked={completed}
onCheckedChange={(val) => setCompleted(Boolean(val))}
shape="square"
className="border-white/30 data-[state=checked]:bg-purple-600 data-[state=checked]:border-purple-600"
/>
</div>
<span className="text-sm text-white/80 select-none">Completed</span>
</label>

<div className="flex gap-2 border-b border-[#1D1D1C] overflow-x-auto mt-2">
{/* Tabs */}
<div className="flex gap-2 border-b border-[#1D1D1C] overflow-x-auto">
{[
{ id: "overview" as const, label: "Overview" },
{ id: "resources" as const, label: "Resources" },
{ id: "tasks" as const, label: "Tasks" },
{ id: "tasks" as const, label: "Task" },
{ id: "summary" as const, label: "Summary" },
].map((tab) => (
<button
Expand Down Expand Up @@ -173,8 +207,20 @@ export default function CourseDetailsPage({ params }: { params: Params }) {
</div>
</div>

{/* Right: tutor + content tracker */}
<div className="lg:col-span-1">
<CourseContentTrackerSidebar />
<CourseContentTrackerSidebar
lessons={LESSONS.map((l) => ({
id: l.id,
title: l.title,
duration: l.duration,
}))}
tutorInfo={{
name: "Satoshi Nakamoto",
role: "Front-End Developer",
avatar: "/avatarPlaceholder1.jpg",
}}
/>
</div>
</div>
</div>
Expand Down
76 changes: 76 additions & 0 deletions src/components/dashboard/CourseVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";

import React, { useState } from "react";
import { Play, SkipBack, SkipForward } from "lucide-react";

interface CourseVideoPlayerProps {
thumbnailSrc: string;
thumbnailAlt?: string;
onPrevious?: () => void;
onNext?: () => void;
onPlay?: () => void;
}

export default function CourseVideoPlayer({
thumbnailSrc,
thumbnailAlt = "Course video thumbnail",
onPrevious,
onNext,
onPlay,
}: CourseVideoPlayerProps) {
const [isPlayHovered, setIsPlayHovered] = useState(false);

return (
<div className="relative w-full aspect-video rounded-xl overflow-hidden bg-[#0B0113]">
{/* Thumbnail */}
<img
src={thumbnailSrc}
alt={thumbnailAlt}
className="w-full h-full object-cover"
/>

{/* Dark overlay */}
<div className="absolute inset-0 bg-black/40" />

{/* Side nav: Previous */}
<button
onClick={onPrevious}
className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center w-10 h-10 rounded-full bg-black/50 border border-white/10 text-white hover:bg-black/70 hover:border-white/30 transition-all duration-200 backdrop-blur-sm"
aria-label="Previous lesson"
>
<SkipBack size={18} />
</button>

{/* Center play button */}
<button
onClick={onPlay}
onMouseEnter={() => setIsPlayHovered(true)}
onMouseLeave={() => setIsPlayHovered(false)}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center transition-all duration-200"
aria-label="Play video"
>
<span
className={`flex items-center justify-center w-16 h-16 rounded-full border-2 border-white bg-white/10 backdrop-blur-sm transition-all duration-200 ${
isPlayHovered
? "bg-white/25 scale-110 shadow-[0_0_32px_rgba(255,255,255,0.25)]"
: ""
}`}
>
<Play
size={28}
className="text-white fill-white translate-x-0.5"
/>
</span>
</button>

{/* Side nav: Next */}
<button
onClick={onNext}
className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center justify-center w-10 h-10 rounded-full bg-black/50 border border-white/10 text-white hover:bg-black/70 hover:border-white/30 transition-all duration-200 backdrop-blur-sm"
aria-label="Next lesson"
>
<SkipForward size={18} />
</button>
</div>
);
}
Loading