feat: Add LTI launch, lesson preview, course progress, bookmarks, and asset management features.
This commit is contained in:
@@ -8,9 +8,9 @@ import { cmsApi, Asset } from "@/lib/api";
|
||||
interface AssetPickerModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
courseId: string;
|
||||
courseId?: string;
|
||||
onSelect: (asset: Asset) => void;
|
||||
filterType?: "image" | "file" | "all";
|
||||
filterType?: "image" | "file" | "video" | "all";
|
||||
}
|
||||
|
||||
export default function AssetPickerModal({
|
||||
@@ -23,34 +23,46 @@ export default function AssetPickerModal({
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [viewMode, setViewMode] = useState<"course" | "global">(courseId ? "course" : "global");
|
||||
|
||||
const loadAssets = useCallback(async () => {
|
||||
if (!isOpen) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await cmsApi.getCourseAssets(courseId);
|
||||
const filters: any = {};
|
||||
if (viewMode === "course" && courseId) {
|
||||
filters.course_id = courseId;
|
||||
}
|
||||
if (searchTerm) {
|
||||
filters.search = searchTerm;
|
||||
}
|
||||
|
||||
const data = await cmsApi.getAssets(filters);
|
||||
let filtered = data;
|
||||
|
||||
if (filterType === "image") {
|
||||
filtered = data.filter(a => a.mimetype.startsWith("image/"));
|
||||
} else if (filterType === "file") {
|
||||
filtered = data.filter(a => !a.mimetype.startsWith("image/"));
|
||||
filtered = data.filter(a => !a.mimetype.startsWith("image/") && !a.mimetype.startsWith("video/"));
|
||||
} else if (filterType === "video") {
|
||||
filtered = data.filter(a => a.mimetype.startsWith("video/"));
|
||||
}
|
||||
|
||||
setAssets(filtered);
|
||||
} catch (error) {
|
||||
console.error("Failed to load assets for picker:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [courseId, isOpen, filterType]);
|
||||
}, [courseId, isOpen, filterType, viewMode, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAssets();
|
||||
const timer = setTimeout(() => {
|
||||
loadAssets();
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [loadAssets]);
|
||||
|
||||
const filteredAssets = assets.filter(asset =>
|
||||
asset.filename.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const getIcon = (mimetype: string) => {
|
||||
if (mimetype.startsWith('image/')) return <ImageIcon className="w-5 h-5 text-blue-400" />;
|
||||
if (mimetype.startsWith('video/')) return <Film className="w-5 h-5 text-purple-400" />;
|
||||
@@ -62,47 +74,78 @@ export default function AssetPickerModal({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={filterType === "image" ? "Select Image" : filterType === "file" ? "Select File" : "Select Asset"}
|
||||
title={filterType === "image" ? "Select Image" : filterType === "video" ? "Select Video" : "Select Asset"}
|
||||
>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-hidden flex flex-col">
|
||||
<div className="space-y-4 max-h-[70vh] overflow-hidden flex flex-col">
|
||||
<div className="flex gap-2 p-1 bg-white/5 rounded-xl border border-white/10">
|
||||
{courseId && (
|
||||
<button
|
||||
onClick={() => setViewMode("course")}
|
||||
className={`flex-1 py-2 text-[10px] uppercase font-bold tracking-widest rounded-lg transition-all ${viewMode === "course" ? "bg-blue-500 text-white shadow-lg shadow-blue-500/20" : "text-gray-400 hover:text-gray-200"}`}
|
||||
>
|
||||
This Course
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setViewMode("global")}
|
||||
className={`flex-1 py-2 text-[10px] uppercase font-bold tracking-widest rounded-lg transition-all ${viewMode === "global" ? "bg-blue-500 text-white shadow-lg shadow-blue-500/20" : "text-gray-400 hover:text-gray-200"}`}
|
||||
>
|
||||
Global Library
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search files..."
|
||||
placeholder="Search assets by filename..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-10 pr-4 py-2 text-sm focus:outline-none focus:border-blue-500/50"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-10 pr-4 py-2.5 text-sm font-medium text-gray-200 placeholder:text-gray-500 focus:outline-none focus:border-blue-500/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2 min-h-[300px] pr-2 custom-scrollbar">
|
||||
<div className="flex-1 overflow-y-auto space-y-2 min-h-[350px] pr-2 custom-scrollbar">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-500 gap-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||
<span className="text-xs uppercase font-bold tracking-widest">Loading assets...</span>
|
||||
<div className="flex flex-col items-center justify-center py-24 text-gray-500 gap-4">
|
||||
<Loader2 className="w-10 h-10 animate-spin text-blue-500" />
|
||||
<span className="text-[10px] uppercase font-black tracking-[0.2em] opacity-50">Synchronizing...</span>
|
||||
</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="text-center py-20 text-gray-500 text-sm">
|
||||
{searchTerm ? "No files match your search." : "No assets found for this course."}
|
||||
) : assets.length === 0 ? (
|
||||
<div className="text-center py-24 flex flex-col items-center gap-4">
|
||||
<div className="p-4 bg-white/5 rounded-2xl border border-white/5">
|
||||
<FileIcon className="w-8 h-8 text-gray-600" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-bold text-gray-400">Empty Collection</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-widest">
|
||||
{searchTerm ? "No results found" : "No assets uploaded yet"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filteredAssets.map((asset) => (
|
||||
assets.map((asset) => (
|
||||
<button
|
||||
key={asset.id}
|
||||
onClick={() => {
|
||||
onSelect(asset);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl bg-white/5 border border-transparent hover:border-white/10 hover:bg-white/10 transition-all text-left group"
|
||||
className="w-full flex items-center gap-4 p-3.5 rounded-2xl bg-white/5 border border-white/5 hover:border-blue-500/30 hover:bg-blue-500/5 transition-all text-left group"
|
||||
>
|
||||
<div className="p-2 bg-white/5 rounded-lg group-hover:bg-white/10 transition-colors">
|
||||
<div className="w-12 h-12 flex items-center justify-center bg-white/5 rounded-xl group-hover:bg-blue-500/10 transition-colors shrink-0">
|
||||
{getIcon(asset.mimetype)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-bold text-gray-200 truncate">{asset.filename}</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-wider">
|
||||
{(asset.size_bytes / 1024 / 1024).toFixed(2)} MB • {asset.mimetype.split('/')[1]}
|
||||
<div className="text-sm font-bold text-gray-200 truncate group-hover:text-blue-400 transition-colors">{asset.filename}</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="text-[10px] text-gray-500 uppercase font-bold tracking-wider">
|
||||
{(asset.size_bytes / 1024 / 1024).toFixed(2)} MB
|
||||
</div>
|
||||
<div className="w-1 h-1 rounded-full bg-gray-700" />
|
||||
<div className="text-[10px] text-gray-500 uppercase font-bold tracking-wider truncate">
|
||||
{asset.mimetype.split('/')[1]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -110,10 +153,10 @@ export default function AssetPickerModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/5 text-center">
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-widest">
|
||||
Only assets uploaded to this course are shown.
|
||||
</p>
|
||||
<div className="pt-4 border-t border-white/10 flex items-center justify-between">
|
||||
<div className="text-[10px] text-gray-500 uppercase font-bold tracking-[0.1em]">
|
||||
{assets.length} Assets Found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user