feat: responsive media grid with lightbox, proper attachment list
- Media (images/videos) now render in a responsive grid with a full-screen lightbox on tap instead of a stacked full-width column. - Multiple attached files (attachments[]) each get their own icon, title, and download button, replacing the single hidden file link. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { LearnProps } from "../../../../types/props";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import useAxios from "../../../../hooks/useAxios";
|
||||
@@ -6,6 +7,12 @@ import { MdErrorOutline } from "react-icons/md";
|
||||
import Button from "../../../../components/button";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import {
|
||||
IoClose,
|
||||
IoDocumentTextOutline,
|
||||
IoDownloadOutline,
|
||||
IoPlayCircleOutline,
|
||||
} from "react-icons/io5";
|
||||
|
||||
const getMediaType = (url: string) => {
|
||||
const extension = url.split(".").pop()?.toLowerCase();
|
||||
@@ -20,6 +27,9 @@ const getMediaType = (url: string) => {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
type Attachment = { id: number | null; title: string; url: string };
|
||||
type LightboxState = { url: string; type: "image" | "video" } | null;
|
||||
|
||||
const Learn = ({ activeTutorial }: LearnProps) => {
|
||||
const axios = useAxios();
|
||||
const { data, isLoading, isFetching, error } = useQuery({
|
||||
@@ -28,8 +38,41 @@ const Learn = ({ activeTutorial }: LearnProps) => {
|
||||
return axios.get(`/home/category/learning/${activeTutorial}`);
|
||||
},
|
||||
});
|
||||
const [lightbox, setLightbox] = useState<LightboxState>(null);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full border-4 flex flex-col justify-center items-center border-solid mt-4 p-4 rounded-md border-gray-800 border-spacing-1">
|
||||
{lightbox && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/90 flex flex-col justify-center items-center p-4"
|
||||
onClick={() => setLightbox(null)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 left-4 text-white text-3xl"
|
||||
onClick={() => setLightbox(null)}
|
||||
>
|
||||
<IoClose />
|
||||
</button>
|
||||
{lightbox.type === "image" ? (
|
||||
<img
|
||||
src={lightbox.url}
|
||||
alt="media"
|
||||
className="max-w-full max-h-[85vh] rounded-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
src={lightbox.url}
|
||||
controls
|
||||
autoPlay
|
||||
className="max-w-full max-h-[85vh] rounded-lg"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading || isFetching ? (
|
||||
<div className="w-full h-full p-5 flex justify-center items-center">
|
||||
<svg
|
||||
@@ -117,67 +160,92 @@ const Learn = ({ activeTutorial }: LearnProps) => {
|
||||
id: string;
|
||||
title: string;
|
||||
medias: string[];
|
||||
file: string | null;
|
||||
attachments?: Attachment[];
|
||||
url_file: string;
|
||||
}) => (
|
||||
<div
|
||||
key={i.id}
|
||||
className="relative w-full h-full flex flex-col justify-center items-center"
|
||||
className="relative w-full h-full flex flex-col justify-center items-center mb-2"
|
||||
>
|
||||
<span className="p-2 bg-gray-800 rounded-md mb-4">
|
||||
{i.title}
|
||||
</span>
|
||||
<div className="relative w-full h-full flex flex-col justify-center items-center">
|
||||
|
||||
{i.medias.length > 0 && (
|
||||
<div className="w-full grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4">
|
||||
{i.medias.map((media, index) => {
|
||||
const mediaType = getMediaType(media);
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
key={index}
|
||||
className="w-full md:w-1/2 mb-3 rounded-lg mx-auto h-full"
|
||||
type="button"
|
||||
className="relative aspect-square rounded-lg overflow-hidden bg-gray-800 flex items-center justify-center"
|
||||
onClick={() =>
|
||||
mediaType === "unknown"
|
||||
? saveAs(media)
|
||||
: setLightbox({
|
||||
url: media,
|
||||
type: mediaType as "image" | "video",
|
||||
})
|
||||
}
|
||||
>
|
||||
{mediaType === "image" ? (
|
||||
<img
|
||||
src={media}
|
||||
alt="media"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : mediaType === "video" ? (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
<>
|
||||
<video
|
||||
src={media}
|
||||
controls
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
className="w-full h-full object-cover"
|
||||
muted
|
||||
/>
|
||||
<Button
|
||||
extra="!py-1 self-center"
|
||||
onClick={() => saveAs(media)}
|
||||
>
|
||||
دانلود ویدیو
|
||||
</Button>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<IoPlayCircleOutline className="text-white w-10 h-10" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col gap-2 justify-center items-center">
|
||||
<p>نوع فایل قابل نمایش نیست</p>
|
||||
<Button extra="!py-1" onClick={() => saveAs(media)}>
|
||||
دانلود فایل
|
||||
</Button>
|
||||
<div className="w-full h-full flex flex-col gap-1 items-center justify-center text-xs p-2">
|
||||
<IoDocumentTextOutline className="w-8 h-8" />
|
||||
<span>دانلود</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{i.file && (
|
||||
<Button extra="!mb-4" onClick={() => saveAs(i.file as string)}>
|
||||
دانلود فایل آموزش
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{i.attachments && i.attachments.length > 0 && (
|
||||
<div className="w-full flex flex-col gap-2 mb-4">
|
||||
{i.attachments.map((att) => (
|
||||
<div
|
||||
key={att.id ?? att.url}
|
||||
className="w-full flex items-center justify-between gap-2 bg-gray-800 rounded-md p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<IoDocumentTextOutline className="shrink-0 w-5 h-5" />
|
||||
<span className="truncate text-xs md:text-sm">
|
||||
{att.title}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
extra="!py-1 !px-2 shrink-0 flex items-center gap-1"
|
||||
onClick={() => saveAs(att.url, att.title)}
|
||||
>
|
||||
<IoDownloadOutline />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{i.url_file && (
|
||||
<>
|
||||
<Button extra="!mb-4" onClick={() => saveAs(i.url_file)}>
|
||||
دانلود برنامه
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<hr className="border-2 w-full rounded-md my-5 border-gray-800" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user