-
Notifications
You must be signed in to change notification settings - Fork 25
feat: build course video player UI with play button, side controls, a… #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!courseData) { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Play button has no handler—clicking does nothing. The If this is intentional for the UI-only scope of issue 💡 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <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 | ||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 544
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 243
🏁 Script executed:
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:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 723
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 5176
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 157
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 170
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 228
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 1520
Next.js 15 breaking change:
paramsis now a Promise and must be unwrapped.In Next.js 15,
paramspassed to page components became asynchronous (Promise<Params>). The current code directly accessesparams.idwithout unwrapping, which will fail at runtime.Since this file uses
"use client", the function cannot be madeasync. Instead, useReact.use()to unwrap the promise:🔧 Fix using React.use()
Note: The codebase already has the correct pattern in
src/app/dashboard/notifications/questions/[id]/page.tsxusingasync/await(without"use client"). This file should follow the same pattern or apply the React.use() workaround shown above.🤖 Prompt for AI Agents