@@ -1108,7 +1309,7 @@ function WeekAccordionItem({
|
- {item.skill}
+
|
@@ -1134,38 +1335,64 @@ function WeekAccordionItem({
)}
-
- {materials.length > 0 && (
-
+
+ {t("coursePlan.generateHint")}
+
+ >
+ )}
+ {materialSkills.length > 0 && (
+
+ setSkillFilter("all")}
+ >
+ {t("common.all", "All")}
+
+ {materialSkills.map((skill) => (
+ setSkillFilter(skill)}
+ className="capitalize"
+ >
+ {skill}
+
+ ))}
+
)}
-
- {t("coursePlan.generateHint")}
-
{materials.length > 0 && (
- {materials.map((m) => (
-
+ {filteredMaterials.map((m) => (
+
))}
)}
@@ -1174,11 +1401,53 @@ function WeekAccordionItem({
);
}
-function MaterialCard({ material }: { material: CoursePlanMaterial }) {
+function MaterialCard({
+ material,
+ studentPreview,
+}: {
+ material: CoursePlanMaterial;
+ studentPreview: boolean;
+}) {
const { t } = useTranslation();
+ const qc = useQueryClient();
const [drawerOpen, setDrawerOpen] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [title, setTitle] = useState(material.title);
+ const [summary, setSummary] = useState(material.summary || "");
+ const [bodyText, setBodyText] = useState(material.body_text || "");
+ const [shareDate, setShareDate] = useState(material.share_date ?? "");
+ const [isStatic, setIsStatic] = useState(Boolean(material.is_static));
const Icon = SKILL_ICONS[material.skill] ?? ClipboardList;
const mediaCount = material.media?.length ?? 0;
+
+ useEffect(() => {
+ setTitle(material.title);
+ setSummary(material.summary || "");
+ setBodyText(material.body_text || "");
+ setShareDate(material.share_date ?? "");
+ setIsStatic(Boolean(material.is_static));
+ }, [material]);
+
+ const saveMut = useMutation({
+ mutationFn: () =>
+ coursePlanService.updateMaterial(material.id, {
+ title,
+ summary,
+ body_text: bodyText,
+ share_date: shareDate || null,
+ is_static: isStatic,
+ }),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ["course-plan", material.plan_id] });
+ qc.invalidateQueries({
+ queryKey: ["course-plan", material.plan_id, "deliverables"],
+ });
+ toast.success(t("common.saved", "Saved"));
+ setEditing(false);
+ },
+ onError: (err) => toast.error(describeApiError(err, t("common.error", "Error"))),
+ });
+
return (
@@ -1193,6 +1462,7 @@ function MaterialCard({ material }: { material: CoursePlanMaterial }) {
material.material_type,
)}
+
)}
+ {!studentPreview && (
+ setEditing((v) => !v)}
+ >
+ {editing ? t("common.cancel", "Cancel") : t("common.edit", "Edit")}
+
+ )}
{material.summary && {material.summary}}
-
- {material.body_text || JSON.stringify(material.body, null, 2)}
-
+
+ {material.share_date && (
+
+ {t("coursePlan.shareDate", "Share date")}: {material.share_date}
+
+ )}
+ {material.is_static && (
+
+ {t("coursePlan.staticMaterial", "Static material (keep on regenerate)")}
+
+ )}
+
+ {editing && !studentPreview ? (
+
+
+
+ setIsStatic(Boolean(v))}
+ />
+
+
+
+
+
+
+
+
+
+ saveMut.mutate()} disabled={saveMut.isPending}>
+ {saveMut.isPending && }
+ {t("common.save", "Save")}
+
+
+
+ ) : (
+
+ )}
);
@@ -1228,10 +1566,12 @@ function MediaDrawer({
open,
onOpenChange,
material,
+ studentPreview,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
material: CoursePlanMaterial;
+ studentPreview: boolean;
}) {
const { t } = useTranslation();
const qc = useQueryClient();
@@ -1304,47 +1644,49 @@ function MediaDrawer({
-
- audioMut.mutate()}
- disabled={audioMut.isPending}
- >
- {audioMut.isPending ? (
-
- ) : (
-
- )}
- {t("coursePlan.media.generateAudio")}
-
- imageMut.mutate()}
- disabled={imageMut.isPending}
- >
- {imageMut.isPending ? (
-
- ) : (
-
- )}
- {t("coursePlan.media.generateImage")}
-
- videoMut.mutate()}
- disabled={videoMut.isPending}
- >
- {videoMut.isPending ? (
-
- ) : (
-
- )}
- {t("coursePlan.media.generateVideo")}
-
-
+ {!studentPreview && (
+
+ audioMut.mutate()}
+ disabled={audioMut.isPending}
+ >
+ {audioMut.isPending ? (
+
+ ) : (
+
+ )}
+ {t("coursePlan.media.generateAudio")}
+
+ imageMut.mutate()}
+ disabled={imageMut.isPending}
+ >
+ {imageMut.isPending ? (
+
+ ) : (
+
+ )}
+ {t("coursePlan.media.generateImage")}
+
+ videoMut.mutate()}
+ disabled={videoMut.isPending}
+ >
+ {videoMut.isPending ? (
+
+ ) : (
+
+ )}
+ {t("coursePlan.media.generateVideo")}
+
+
+ )}
{items.length === 0 && (
@@ -1358,8 +1700,9 @@ function MediaDrawer({
media={m}
onDelete={() => {
if (!window.confirm(t("common.confirm", "Are you sure?"))) return;
- deleteMut.mutate(m.id);
+ if (!studentPreview) deleteMut.mutate(m.id);
}}
+ canDelete={!studentPreview}
/>
))}
@@ -1371,12 +1714,21 @@ function MediaDrawer({
function MediaTile({
media,
onDelete,
+ canDelete,
}: {
media: CoursePlanMedia;
onDelete: () => void;
+ canDelete: boolean;
}) {
const { t } = useTranslation();
- const url = media.download_url || "";
+ // The streaming endpoint accepts the JWT either via the Authorization
+ // header (REST clients) or via ``?token=…`` (so plain media tags work,
+ // since browsers can't attach custom headers to / |