From 8f193bf7119ed7bd0fbe7b39d4b27323d5e85efb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:55:28 +0000 Subject: [PATCH 1/2] Initial plan From deee272d9e3bd8b7d747b4348983fc65c69b3751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:10:31 +0000 Subject: [PATCH 2/2] fix: load YouTube thumbnails for demo representations - StudentView: reduce overlay from 80% to 40%, add two-stage error fallback - Read: extract RelatedDemoCard with same two-stage fallback - Demos: add youtube_thumbnail/youtube_id props and thumbnail display Agent-Logs-Url: https://github.com/codewit-us/codewit.us/sessions/24a3bb5e-7767-4b02-8fa9-717448fdb264 Co-authored-by: kbuffardi <8324410+kbuffardi@users.noreply.github.com> --- codewit/client/src/components/demos/Demos.tsx | 33 ++++++- codewit/client/src/pages/Read.tsx | 93 +++++++++++++------ .../client/src/pages/course/StudentView.tsx | 37 ++++++-- 3 files changed, 120 insertions(+), 43 deletions(-) diff --git a/codewit/client/src/components/demos/Demos.tsx b/codewit/client/src/components/demos/Demos.tsx index 3a904c6..afaeb94 100644 --- a/codewit/client/src/components/demos/Demos.tsx +++ b/codewit/client/src/components/demos/Demos.tsx @@ -1,4 +1,5 @@ // codewit/client/src/components/demos/Demos.tsx +import { useState } from "react"; import { Link } from "react-router-dom"; import LoadingIcons from "../loading/LoadingIcon"; import { TrashIcon, PencilSquareIcon, VideoCameraIcon } from "@heroicons/react/24/solid"; @@ -7,13 +8,26 @@ interface VideoProps { title: string; amountExercises: number; uid: number; + youtube_id: string; + youtube_thumbnail: string; isDeleting?: boolean; handleEdit: () => void; handleDelete: () => void; } -const Video = ({ title, uid, amountExercises, isDeleting, handleEdit, handleDelete }: VideoProps): JSX.Element => { - +const Video = ({ title, uid, amountExercises, youtube_id, youtube_thumbnail, isDeleting, handleEdit, handleDelete }: VideoProps): JSX.Element => { + const fallback_src = `https://i.ytimg.com/vi/${youtube_id}/hqdefault.jpg`; + const [imgSrc, setImgSrc] = useState(youtube_thumbnail || fallback_src); + const [imgFailed, setImgFailed] = useState(false); + + const handleImgError = () => { + if (imgSrc !== fallback_src) { + setImgSrc(fallback_src); + } else { + setImgFailed(true); + } + }; + const handleMenuClick = (e: React.MouseEvent, action: string) => { e.stopPropagation(); if (action === 'edit') { @@ -26,8 +40,19 @@ const Video = ({ title, uid, amountExercises, isDeleting, handleEdit, handleDele return (
-
- +
+ {imgFailed ? ( +
+ +
+ ) : ( + {title} + )}
diff --git a/codewit/client/src/pages/Read.tsx b/codewit/client/src/pages/Read.tsx index fc8e2e6..d400555 100644 --- a/codewit/client/src/pages/Read.tsx +++ b/codewit/client/src/pages/Read.tsx @@ -8,6 +8,7 @@ import { PlayIcon, CheckCircleIcon, LinkIcon, + VideoCameraIcon, } from '@heroicons/react/24/solid'; import { Editor } from '@monaco-editor/react'; import { useForm } from "@tanstack/react-form"; @@ -217,6 +218,67 @@ function LeftPanel({info, module_id, course_id}: LeftPanelProps) { ; } +interface RelatedDemoCardProps { + demo: RelatedDemo, + link_path: string, +} + +function RelatedDemoCard({demo, link_path}: RelatedDemoCardProps) { + const fallback_src = `https://i.ytimg.com/vi/${demo.youtube_id}/hqdefault.jpg`; + const [imgSrc, setImgSrc] = useState(demo.youtube_thumbnail || fallback_src); + const [imgFailed, setImgFailed] = useState(false); + + const handleImgError = () => { + if (imgSrc !== fallback_src) { + setImgSrc(fallback_src); + } else { + setImgFailed(true); + } + }; + + let status = null; + + if (demo.completion !== 0 && demo.completion !== 1) { + status = `${(demo.completion * 100).toFixed(0)}%`; + } + + return +
+ {imgFailed ? ( +
+ +
+ ) : ( + {demo.title} + )} +
+ {demo.completion === 1 ? + + : +
+ {status} + +
+ } +
+
+
+

+ {demo.title} +

+
+ ; +} + interface RelatedDemosProps { demos: RelatedDemo[] | null, course_id?: string | null, @@ -246,36 +308,7 @@ function RelatedDemos({demos, course_id, module_id}: RelatedDemosProps) { link_path += `module_id=${module_id}`; } - let status = null; - - if (demo.completion !== 0 && demo.completion !== 1) { - status = `${(demo.completion * 100).toFixed(0)}%`; - } - - return -
- {demo.title} -
- {demo.completion === 1 ? - - : -
- {status} - -
- } -
-
-
-

- {demo.title} -

-
- + return ; })}
diff --git a/codewit/client/src/pages/course/StudentView.tsx b/codewit/client/src/pages/course/StudentView.tsx index c8b024c..7f8db4b 100644 --- a/codewit/client/src/pages/course/StudentView.tsx +++ b/codewit/client/src/pages/course/StudentView.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -import { PlayIcon, CheckCircleIcon } from "@heroicons/react/24/solid"; +import { PlayIcon, CheckCircleIcon, VideoCameraIcon } from "@heroicons/react/24/solid"; import { StudentCourse, StudentModule, StudentDemo } from "@codewit/interfaces"; import { cn } from "../../utils/styles"; @@ -158,6 +158,18 @@ interface CourseModuleDemoProps { } function CourseModuleDemo({course_id, module_id, demo}: CourseModuleDemoProps) { + const fallback_src = `https://i.ytimg.com/vi/${demo.youtube_id}/hqdefault.jpg`; + const [imgSrc, setImgSrc] = useState(demo.youtube_thumbnail || fallback_src); + const [imgFailed, setImgFailed] = useState(false); + + const handleImgError = () => { + if (imgSrc !== fallback_src) { + setImgSrc(fallback_src); + } else { + setImgFailed(true); + } + }; + let status = null; if (demo.completion !== 0 && demo.completion !== 1) { @@ -166,22 +178,29 @@ function CourseModuleDemo({course_id, module_id, demo}: CourseModuleDemoProps) { return
- {demo.title} -
+ {imgFailed ? ( +
+ +
+ ) : ( + {demo.title} + )} +
{demo.completion === 1 ? - + :
{status} - +
}