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 { LearnProps } from "../../../../types/props";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import useAxios from "../../../../hooks/useAxios";
|
import useAxios from "../../../../hooks/useAxios";
|
||||||
@@ -6,6 +7,12 @@ import { MdErrorOutline } from "react-icons/md";
|
|||||||
import Button from "../../../../components/button";
|
import Button from "../../../../components/button";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import { Fragment } from "react/jsx-runtime";
|
import { Fragment } from "react/jsx-runtime";
|
||||||
|
import {
|
||||||
|
IoClose,
|
||||||
|
IoDocumentTextOutline,
|
||||||
|
IoDownloadOutline,
|
||||||
|
IoPlayCircleOutline,
|
||||||
|
} from "react-icons/io5";
|
||||||
|
|
||||||
const getMediaType = (url: string) => {
|
const getMediaType = (url: string) => {
|
||||||
const extension = url.split(".").pop()?.toLowerCase();
|
const extension = url.split(".").pop()?.toLowerCase();
|
||||||
@@ -20,6 +27,9 @@ const getMediaType = (url: string) => {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Attachment = { id: number | null; title: string; url: string };
|
||||||
|
type LightboxState = { url: string; type: "image" | "video" } | null;
|
||||||
|
|
||||||
const Learn = ({ activeTutorial }: LearnProps) => {
|
const Learn = ({ activeTutorial }: LearnProps) => {
|
||||||
const axios = useAxios();
|
const axios = useAxios();
|
||||||
const { data, isLoading, isFetching, error } = useQuery({
|
const { data, isLoading, isFetching, error } = useQuery({
|
||||||
@@ -28,8 +38,41 @@ const Learn = ({ activeTutorial }: LearnProps) => {
|
|||||||
return axios.get(`/home/category/learning/${activeTutorial}`);
|
return axios.get(`/home/category/learning/${activeTutorial}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [lightbox, setLightbox] = useState<LightboxState>(null);
|
||||||
|
|
||||||
return (
|
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">
|
<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 ? (
|
{isLoading || isFetching ? (
|
||||||
<div className="w-full h-full p-5 flex justify-center items-center">
|
<div className="w-full h-full p-5 flex justify-center items-center">
|
||||||
<svg
|
<svg
|
||||||
@@ -117,67 +160,92 @@ const Learn = ({ activeTutorial }: LearnProps) => {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
medias: string[];
|
medias: string[];
|
||||||
file: string | null;
|
attachments?: Attachment[];
|
||||||
url_file: string;
|
url_file: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
key={i.id}
|
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">
|
<span className="p-2 bg-gray-800 rounded-md mb-4">
|
||||||
{i.title}
|
{i.title}
|
||||||
</span>
|
</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) => {
|
{i.medias.map((media, index) => {
|
||||||
const mediaType = getMediaType(media);
|
const mediaType = getMediaType(media);
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={index}
|
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" ? (
|
{mediaType === "image" ? (
|
||||||
<img
|
<img
|
||||||
src={media}
|
src={media}
|
||||||
alt="media"
|
alt="media"
|
||||||
className="w-full h-full object-cover rounded-lg"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : mediaType === "video" ? (
|
) : mediaType === "video" ? (
|
||||||
<div className="w-full h-full flex flex-col gap-2">
|
<>
|
||||||
<video
|
<video
|
||||||
src={media}
|
src={media}
|
||||||
controls
|
className="w-full h-full object-cover"
|
||||||
className="w-full h-full object-cover rounded-lg"
|
muted
|
||||||
/>
|
/>
|
||||||
<Button
|
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||||
extra="!py-1 self-center"
|
<IoPlayCircleOutline className="text-white w-10 h-10" />
|
||||||
onClick={() => saveAs(media)}
|
|
||||||
>
|
|
||||||
دانلود ویدیو
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col gap-2 justify-center items-center">
|
<div className="w-full h-full flex flex-col gap-1 items-center justify-center text-xs p-2">
|
||||||
<p>نوع فایل قابل نمایش نیست</p>
|
<IoDocumentTextOutline className="w-8 h-8" />
|
||||||
<Button extra="!py-1" onClick={() => saveAs(media)}>
|
<span>دانلود</span>
|
||||||
دانلود فایل
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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 && (
|
{i.url_file && (
|
||||||
<>
|
|
||||||
<Button extra="!mb-4" onClick={() => saveAs(i.url_file)}>
|
<Button extra="!mb-4" onClick={() => saveAs(i.url_file)}>
|
||||||
دانلود برنامه
|
دانلود برنامه
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<hr className="border-2 w-full rounded-md my-5 border-gray-800" />
|
<hr className="border-2 w-full rounded-md my-5 border-gray-800" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user