Skip to content
Merged
23 changes: 17 additions & 6 deletions frontend/src/components/dashboard/TopContentList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OpenContentItem } from '@/types';
import { ExternalLink } from 'lucide-react';
import { useNavigate } from 'react-router-dom';

interface TopContentListProps {
heading: string;
Expand All @@ -8,12 +9,22 @@ interface TopContentListProps {
}

function ContentRow({ item }: { item: OpenContentItem }) {
const navigate = useNavigate();

const handleClick = () => {
if (item.content_type === 'video') {
navigate(`/viewer/videos/${item.content_id}`);
} else if (item.content_type === 'library') {
navigate(`/viewer/libraries/${item.content_id}`);
} else {
window.open(item.url, '_blank', 'noopener,noreferrer');
}
};

return (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-3 rounded-lg border border-border bg-card hover:shadow-md transition-shadow"
<div
onClick={handleClick}
className="flex items-center gap-3 p-3 rounded-lg border border-border bg-card hover:shadow-md transition-shadow cursor-pointer"
Comment on lines +25 to +27
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find frontend -name "TopContentList.tsx" -type f

Repository: UnlockedLabs/UnlockEdv2

Length of output: 120


🏁 Script executed:

cat -n frontend/src/components/dashboard/TopContentList.tsx

Repository: UnlockedLabs/UnlockEdv2

Length of output: 3262


🏁 Script executed:

rg "onClick.*handleClick" frontend/src/components/dashboard/ -A 3 -B 1

Repository: UnlockedLabs/UnlockEdv2

Length of output: 566


🏁 Script executed:

rg "<div[^>]*onClick" frontend/src/components/ --type tsx --type ts -c

Repository: UnlockedLabs/UnlockEdv2

Length of output: 95


🏁 Script executed:

rg "<div[^>]*onClick" frontend/src/components/ -c

Repository: UnlockedLabs/UnlockEdv2

Length of output: 49


🏁 Script executed:

rg "onKeyDown|onKeyPress|onKeyUp" frontend/src/components/dashboard/TopContentList.tsx

Repository: UnlockedLabs/UnlockEdv2

Length of output: 49


Replace interactive div with semantic button element for keyboard accessibility.

Line 25 uses a non-semantic <div> for interactive content, which is not keyboard-focusable by default and blocks keyboard users from accessing this core navigation feature. Since the component already uses semantic buttons elsewhere (line 71), replace this div with a <button type="button"> element to provide proper keyboard and assistive technology support.

Suggested fix
-        <div
+        <button
+            type="button"
             onClick={handleClick}
-            className="flex items-center gap-3 p-3 rounded-lg border border-border bg-card hover:shadow-md transition-shadow cursor-pointer"
+            className="w-full text-left flex items-center gap-3 p-3 rounded-lg border border-border bg-card hover:shadow-md transition-shadow cursor-pointer"
         >
@@
-        </div>
+        </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/dashboard/TopContentList.tsx` around lines 25 - 27,
Replace the interactive <div> wrapper in the TopContentList component with a
semantic <button type="button"> to restore keyboard focus and assistive tech
support: change the element that currently uses onClick={handleClick} and the
className "flex items-center gap-3 p-3 rounded-lg border border-border bg-card
hover:shadow-md transition-shadow cursor-pointer" to a <button type="button">
keeping the same className and onClick handler (ensure it is not nested inside
another button), and remove any non-button-only attributes if present so the
element behaves like a proper button for keyboard users.

>
{item.thumbnail_url ? (
<img
Expand All @@ -34,7 +45,7 @@ function ContentRow({ item }: { item: OpenContentItem }) {
</p>
)}
</div>
</a>
</div>
);
}

Expand Down
19 changes: 2 additions & 17 deletions frontend/src/components/schedule/RescheduleSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { toDateInput, toTimeInput, formatDurationStr } from '@/lib/formatters';

interface RescheduleSessionModalProps {
open: boolean;
Expand All @@ -24,22 +25,6 @@ interface RescheduleSessionModalProps {
onSuccess: () => void;
}

function toDateInput(d: Date): string {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

function toTimeInput(d: Date): string {
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

function formatDuration(startTime: string, endTime: string): string {
const [sh, sm] = startTime.split(':').map(Number);
const [eh, em] = endTime.split(':').map(Number);
const totalMin = (eh ?? 0) * 60 + (em ?? 0) - ((sh ?? 0) * 60 + (sm ?? 0));
if (totalMin <= 0) return '0h0m0s';
return `${Math.floor(totalMin / 60)}h${totalMin % 60}m0s`;
}

export function RescheduleSessionModal({
open,
onOpenChange,
Expand Down Expand Up @@ -76,7 +61,7 @@ export function RescheduleSessionModal({

const effectiveStart = startTime || toTimeInput(event.start);
const effectiveEnd = endTime || toTimeInput(event.end);
const duration = formatDuration(effectiveStart, effectiveEnd);
const duration = formatDurationStr(effectiveStart, effectiveEnd);
if (duration === '0h0m0s') {
toast.error('End time must be after start time');
return;
Expand Down
97 changes: 97 additions & 0 deletions frontend/src/components/schedule/SessionDetailActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { CalendarClock, CalendarOff, CheckCircle, MapPin, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';

interface SessionDetailActionsProps {
canModify: boolean;
showTakeAttendance: boolean;
isCancelled: boolean;
onTakeAttendance?: () => void;
onRescheduleClick: () => void;
onCancelClick: () => void;
onChangeInstructorClick: () => void;
onChangeRoomClick: () => void;
onViewClassDetails?: () => void;
}

export function SessionDetailActions({
canModify,
showTakeAttendance,
isCancelled,
onTakeAttendance,
onRescheduleClick,
onCancelClick,
onChangeInstructorClick,
onChangeRoomClick,
onViewClassDetails
}: SessionDetailActionsProps) {
return (
<>
{(canModify || showTakeAttendance) && (
<div className="pt-6 border-t border-gray-200">
<h4 className="text-sm text-gray-700 mb-3">
Actions
</h4>
<div className="space-y-2">
{showTakeAttendance && onTakeAttendance && (
<Button
className="w-full justify-start bg-[#556830] hover:bg-[#203622] text-white"
onClick={onTakeAttendance}
>
<CheckCircle className="size-4 mr-2" />
Take Attendance
</Button>
)}
{canModify && (
<>
<Button
variant="outline"
onClick={onRescheduleClick}
className="w-full justify-start border-gray-300 hover:bg-gray-50"
>
<CalendarClock className="size-4 mr-2" />
Reschedule This Class
</Button>
<Button
variant="outline"
onClick={onCancelClick}
className="w-full justify-start border-gray-300 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-200"
>
<CalendarOff className="size-4 mr-2" />
Cancel This Class
</Button>
<Button
variant="outline"
onClick={onChangeInstructorClick}
className="w-full justify-start border-gray-300 hover:bg-gray-50"
>
<Users className="size-4 mr-2" />
Change Instructor
</Button>
<Button
variant="outline"
onClick={onChangeRoomClick}
className="w-full justify-start border-gray-300 hover:bg-gray-50"
>
<MapPin className="size-4 mr-2" />
Change Room
</Button>
</>
)}
</div>
</div>
)}

{onViewClassDetails && !isCancelled && (
<div className="pt-6 border-t border-gray-200">
<Button
variant="outline"
className="w-full justify-start"
onClick={onViewClassDetails}
>
View Full Class Details →
</Button>
</div>
)}
</>
);
}
103 changes: 103 additions & 0 deletions frontend/src/components/schedule/SessionDetailClassDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Calendar, Clock, MapPin, Users } from 'lucide-react';
import { formatClassTimeRange } from '@/lib/formatters';

interface SessionDetailClassDetailsProps {
className: string;
programName?: string;
classTime: string;
room: string;
originalRoom?: string;
instructorName?: string;
originalInstructorName?: string;
isCancelled: boolean;
isRescheduledFrom: boolean;
isCancelledReschedule: boolean;
}

export function SessionDetailClassDetails({
className,
programName,
classTime,
room,
originalRoom,
instructorName,
originalInstructorName,
isCancelled,
isRescheduledFrom,
isCancelledReschedule
}: SessionDetailClassDetailsProps) {
return (
<div>
<h4 className="text-sm text-gray-700 mb-3">
Class Details
</h4>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Calendar className="size-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-600 mb-0.5">
Class
</div>
<div className="text-[#203622]">
{className}
</div>
{programName && (
<div className="text-sm text-gray-500">
{programName}
</div>
)}
</div>
</div>
<div className="flex items-start gap-3">
<Clock className="size-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-600 mb-0.5">
Time
</div>
<div
className={`text-[#203622] ${isCancelled || isRescheduledFrom || isCancelledReschedule ? 'line-through' : ''}`}
>
{formatClassTimeRange(classTime)}
</div>
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="size-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-600 mb-0.5">
Room
</div>
<div
className={`text-[#203622] ${
!!originalRoom || isCancelled || isRescheduledFrom || isCancelledReschedule
? 'line-through'
: ''
}`}
>
{originalRoom ?? room}
</div>
</div>
</div>
{(originalInstructorName ?? instructorName) && (
<div className="flex items-start gap-3">
<Users className="size-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm text-gray-600 mb-0.5">
Instructor
</div>
<div
className={`text-[#203622] ${
!!originalInstructorName || isCancelled || isRescheduledFrom || isCancelledReschedule
? 'line-through'
: ''
}`}
>
{originalInstructorName ?? instructorName}
</div>
</div>
</div>
)}
</div>
</div>
);
}
Loading
Loading