manage current user's media
This commit is contained in:
@@ -8,6 +8,7 @@ import { fullDataUser } from "@/interface/admin";
|
||||
import { UserMetaCardProps } from "@/interface/user";
|
||||
import { apiGetCurrentUser, apiLogout } from "@/service/auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ListIcon } from "@/icons";
|
||||
|
||||
export default function UserDropdown() {
|
||||
const router = useRouter();
|
||||
@@ -118,7 +119,7 @@ export default function UserDropdown() {
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
href="/profile"
|
||||
href="/account"
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
@@ -136,35 +137,23 @@ export default function UserDropdown() {
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Edit profile
|
||||
Tài Khoản
|
||||
</DropdownItem>
|
||||
</li>
|
||||
{/* <li>
|
||||
<li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
href="/profile"
|
||||
href="/role-upgrade"
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.4858 3.5L13.5182 3.5C13.9233 3.5 14.2518 3.82851 14.2518 4.23377C14.2518 5.9529 16.1129 7.02795 17.602 6.1682C17.9528 5.96567 18.4014 6.08586 18.6039 6.43667L20.1203 9.0631C20.3229 9.41407 20.2027 9.86286 19.8517 10.0655C18.3625 10.9253 18.3625 13.0747 19.8517 13.9345C20.2026 14.1372 20.3229 14.5859 20.1203 14.9369L18.6039 17.5634C18.4013 17.9142 17.9528 18.0344 17.602 17.8318C16.1129 16.9721 14.2518 18.0471 14.2518 19.7663C14.2518 20.1715 13.9233 20.5 13.5182 20.5H10.4858C10.0804 20.5 9.75182 20.1714 9.75182 19.766C9.75182 18.0461 7.88983 16.9717 6.40067 17.8314C6.04945 18.0342 5.60037 17.9139 5.39767 17.5628L3.88167 14.937C3.67903 14.586 3.79928 14.1372 4.15026 13.9346C5.63949 13.0748 5.63946 10.9253 4.15025 10.0655C3.79926 9.86282 3.67901 9.41401 3.88165 9.06303L5.39764 6.43725C5.60034 6.08617 6.04943 5.96581 6.40065 6.16858C7.88982 7.02836 9.75182 5.9539 9.75182 4.23399C9.75182 3.82862 10.0804 3.5 10.4858 3.5ZM13.5182 2L10.4858 2C9.25201 2 8.25182 3.00019 8.25182 4.23399C8.25182 4.79884 7.64013 5.15215 7.15065 4.86955C6.08213 4.25263 4.71559 4.61859 4.0986 5.68725L2.58261 8.31303C1.96575 9.38146 2.33183 10.7477 3.40025 11.3645C3.88948 11.647 3.88947 12.3531 3.40026 12.6355C2.33184 13.2524 1.96578 14.6186 2.58263 15.687L4.09863 18.3128C4.71562 19.3814 6.08215 19.7474 7.15067 19.1305C7.64015 18.8479 8.25182 19.2012 8.25182 19.766C8.25182 20.9998 9.25201 22 10.4858 22H13.5182C14.7519 22 15.7518 20.9998 15.7518 19.7663C15.7518 19.2015 16.3632 18.8487 16.852 19.1309C17.9202 19.7476 19.2862 19.3816 19.9029 18.3134L21.4193 15.6869C22.0361 14.6185 21.6701 13.2523 20.6017 12.6355C20.1125 12.3531 20.1125 11.647 20.6017 11.3645C21.6701 10.7477 22.0362 9.38152 21.4193 8.3131L19.903 5.68667C19.2862 4.61842 17.9202 4.25241 16.852 4.86917C16.3632 5.15138 15.7518 4.79856 15.7518 4.23377C15.7518 3.00024 14.7519 2 13.5182 2ZM9.6659 11.9999C9.6659 10.7103 10.7113 9.66493 12.0009 9.66493C13.2905 9.66493 14.3359 10.7103 14.3359 11.9999C14.3359 13.2895 13.2905 14.3349 12.0009 14.3349C10.7113 14.3349 9.6659 13.2895 9.6659 11.9999ZM12.0009 8.16493C9.88289 8.16493 8.1659 9.88191 8.1659 11.9999C8.1659 14.1179 9.88289 15.8349 12.0009 15.8349C14.1189 15.8349 15.8359 14.1179 15.8359 11.9999C15.8359 9.88191 14.1189 8.16493 12.0009 8.16493Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Account settings
|
||||
<span className="menu-item-icon">
|
||||
<ListIcon />
|
||||
</span>
|
||||
Nhà Sử Học
|
||||
</DropdownItem>
|
||||
</li> */}
|
||||
<li>
|
||||
</li>
|
||||
{/* <li>
|
||||
<DropdownItem
|
||||
onItemClick={closeDropdown}
|
||||
tag="a"
|
||||
@@ -186,9 +175,9 @@ export default function UserDropdown() {
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Support
|
||||
Hỗ Trợ
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</li> */}
|
||||
</ul>
|
||||
<Link
|
||||
onClick={handleLogout}
|
||||
@@ -210,7 +199,7 @@ export default function UserDropdown() {
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
Sign out
|
||||
Đăng xuất
|
||||
</Link>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@ const processMedia = (mediaArray: any[]) => {
|
||||
return { type: "empty" };
|
||||
};
|
||||
|
||||
export default function ApplicationSquareCardList({
|
||||
export default function ApplicationList({
|
||||
applications,
|
||||
}: {
|
||||
applications: any[];
|
||||
@@ -56,9 +56,9 @@ export default function ApplicationSquareCardList({
|
||||
|
||||
const handleViewDetail = (app: any) => {
|
||||
dispatch(setSelectedApplication(app));
|
||||
router.push(`/profile/applications`);
|
||||
router.push(`/account/applications`);
|
||||
};
|
||||
// Tạo object mapping để render icon dựa trên status
|
||||
|
||||
const StatusIcons: Record<string, React.ReactNode> = {
|
||||
APPROVED: (
|
||||
<svg
|
||||
@@ -111,12 +111,21 @@ export default function ApplicationSquareCardList({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5 border rounded-xl dark:border-zinc-800 lg:p-6 bg-white dark:bg-zinc-950">
|
||||
<h4 className="text-lg font-bold text-zinc-800 dark:text-white/90 mb-5 tracking-tight">
|
||||
Applications CV
|
||||
</h4>
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||
<div className="mb-8 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-b border-gray-100 pb-5 dark:border-zinc-800">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-white/90">
|
||||
Hồ sơ{" "}
|
||||
<span className="ml-2 text-sm font-normal text-gray-500">
|
||||
({applications.length} tệp)
|
||||
</span>
|
||||
</h3>
|
||||
{/* <div className="text-sm text-gray-500">
|
||||
Cập nhật lần cuối: {new Date().toLocaleDateString("vi-VN")}
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{/* CHỈ SỬA DÒNG NÀY: Tăng số lượng cột (grid-cols) để các thẻ nhỏ lại */}
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{applications?.map((app) => {
|
||||
const mediaState = processMedia(app.media);
|
||||
const config = statusConfig[app.status] || statusConfig.PENDING;
|
||||
@@ -125,77 +134,69 @@ export default function ApplicationSquareCardList({
|
||||
<div
|
||||
key={app.id}
|
||||
onClick={() => handleViewDetail(app)}
|
||||
className="group relative h-60 aspect-square border dark:border-zinc-800 rounded-xl cursor-pointer overflow-hidden transition-all duration-300 hover:ring-2 hover:ring-blue-500/50"
|
||||
className="group relative flex aspect-square w-full cursor-pointer flex-col overflow-hidden rounded-xl border border-gray-200 bg-gray-50 transition-all duration-300 hover:ring-2 hover:ring-blue-500/50 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
{/* Media Layer */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
{mediaState.type === "image" ? (
|
||||
<img
|
||||
src={mediaState.src}
|
||||
alt=""
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-zinc-100 dark:bg-zinc-900 flex items-center justify-center p-4">
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-100 p-4 dark:bg-zinc-800/80">
|
||||
{mediaState.type === "documents" ? (
|
||||
<div className="flex flex-wrap gap-1 justify-center">
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{mediaState.extensions?.slice(0, 3).map((ext, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-[9px] font-black px-1.5 py-0.5 bg-white dark:bg-zinc-800 rounded border dark:border-zinc-700 uppercase"
|
||||
className="rounded bg-white px-2 py-1 text-xs font-bold uppercase text-gray-600 border border-gray-200 dark:border-zinc-600 dark:bg-zinc-700 dark:text-gray-200"
|
||||
>
|
||||
.{ext}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gradient-to-br from-zinc-400 to-zinc-600 opacity-20" />
|
||||
<div className="h-full w-full bg-gradient-to-br from-gray-200 to-gray-300 opacity-50 dark:from-zinc-600 dark:to-zinc-800" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Overlay Gradient */}
|
||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/20 to-transparent transition-opacity group-hover:opacity-90" />
|
||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-80 transition-opacity duration-300 group-hover:opacity-100" />
|
||||
|
||||
{/* TOP INFO: FILE COUNT & STATUS ICON */}
|
||||
<div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-center">
|
||||
<div className="absolute left-3 right-3 top-3 z-20 flex items-start justify-between">
|
||||
{app.media?.length > 0 ? (
|
||||
<span className="text-[9px] font-bold text-white/60 bg-black/40 px-1.5 py-0.5 rounded-md backdrop-blur-sm">
|
||||
{app.media.length} FILE
|
||||
<span className="rounded-md bg-black/60 px-2 py-1 text-[10px] font-bold tracking-wider text-white backdrop-blur-md">
|
||||
{app.media.length} TỆP
|
||||
</span>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{/* Status Icon Wrapper */}
|
||||
<div
|
||||
className={`${config.container} rounded-full`}
|
||||
>
|
||||
<div className={`${config.container} rounded-full `}>
|
||||
{StatusIcons[app.status] || StatusIcons.PENDING}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Content */}
|
||||
<div className="absolute bottom-2 left-2 right-2 z-20 text-white">
|
||||
<div className="mb-1">
|
||||
<p className="text-[10px] font-bold uppercase opacity-80 truncate">
|
||||
{app.verify_type || "VERIFY"}
|
||||
</p>
|
||||
{app?.reviewer?.display_name && (
|
||||
<p className="text-[9px] font-medium text-blue-400 truncate">
|
||||
By: {app.reviewer.display_name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20 p-4 text-white">
|
||||
<p className="mb-1 truncate text-xs font-bold uppercase tracking-wider text-gray-300">
|
||||
{app.verify_type || "VERIFY"}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-1 border-t border-white/10">
|
||||
<p className="text-[9px] font-bold text-white/70">
|
||||
{app?.reviewer?.display_name && (
|
||||
<p className="mb-3 truncate text-sm font-medium text-blue-300">
|
||||
Người duyệt: {app.reviewer.display_name}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t border-white/20 pt-2">
|
||||
<p className="text-xs font-semibold text-gray-300">
|
||||
{formatFullDateTime(app.created_at)}
|
||||
</p>
|
||||
<div className="transform translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300">
|
||||
|
||||
<div className="flex h-6 w-6 -translate-x-2 items-center justify-center rounded-full bg-white/20 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
|
||||
<svg
|
||||
className="w-4 h-4 text-blue-400"
|
||||
className="h-3 w-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -204,7 +205,7 @@ export default function ApplicationSquareCardList({
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -216,4 +217,4 @@ export default function ApplicationSquareCardList({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,396 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
import Swal from "sweetalert2";
|
||||
import { MediaDto } from "@/interface/media";
|
||||
import { URL_MEDIA } from "../../../api";
|
||||
|
||||
export default function MediaCard({ data }: { data: MediaDto }) {
|
||||
const [index, setIndex] = useState(-1);
|
||||
const listMedia = data?.data || [];
|
||||
import { deleteMedia } from "@/service/mediaService";
|
||||
import { INITIAL_LIMIT } from "../../../constant";
|
||||
|
||||
const slides = listMedia.map((item) => ({
|
||||
export default function MediaLibrary({
|
||||
data,
|
||||
onRefresh,
|
||||
}: {
|
||||
data: MediaDto;
|
||||
onRefresh?: () => void;
|
||||
}) {
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
const [showAllImages, setShowAllImages] = useState(false);
|
||||
const [showAllDocs, setShowAllDocs] = useState(false);
|
||||
|
||||
const [localMedia, setLocalMedia] = useState(data?.data || []);
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalMedia(data?.data || []);
|
||||
}, [data]);
|
||||
|
||||
const isImageFile = (file: any) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
|
||||
return isImageMime || isImageExt;
|
||||
};
|
||||
|
||||
const imageFiles = localMedia.filter(isImageFile);
|
||||
const documentFiles = localMedia.filter((file) => !isImageFile(file));
|
||||
|
||||
const displayedImages = showAllImages
|
||||
? imageFiles
|
||||
: imageFiles.slice(0, INITIAL_LIMIT);
|
||||
const displayedDocs = showAllDocs
|
||||
? documentFiles
|
||||
: documentFiles.slice(0, INITIAL_LIMIT);
|
||||
|
||||
const imageSlides = imageFiles.map((item) => ({
|
||||
src: `${URL_MEDIA}${item.storage_key}`,
|
||||
title: item.original_name,
|
||||
description: `Size: ${(item.size / 1024).toFixed(2)} KB - Type: ${item.mime_type}`,
|
||||
description: `Kích thước: ${(item.size / 1024).toFixed(2)} KB - Loại: ${item.mime_type}`,
|
||||
}));
|
||||
// console.log("slides", listMedia);
|
||||
return (
|
||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">Media Assets</h3>
|
||||
|
||||
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
|
||||
{listMedia.map((item, idx) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => setIndex(idx)}
|
||||
className="group relative min-w-[150px] h-[150px] cursor-pointer overflow-hidden rounded-lg border border-gray-200"
|
||||
>
|
||||
const toggleSelectionMode = () => {
|
||||
setIsSelectionMode(!isSelectionMode);
|
||||
if (isSelectionMode) {
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleItemSelection = (id: string, e?: React.MouseEvent) => {
|
||||
if (e) e.stopPropagation();
|
||||
|
||||
if (!isSelectionMode) {
|
||||
setIsSelectionMode(true);
|
||||
setSelectedIds([id]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIds.includes(id)) {
|
||||
setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id));
|
||||
} else {
|
||||
setSelectedIds([...selectedIds, id]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemClick = (item: any, idx: number, isImage: boolean) => {
|
||||
if (isSelectionMode) {
|
||||
toggleItemSelection(item.id);
|
||||
} else {
|
||||
if (isImage) {
|
||||
setIndex(idx);
|
||||
} else {
|
||||
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
||||
const googleDocsUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
|
||||
window.open(googleDocsUrl, "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const result = await Swal.fire({
|
||||
title: "Xóa tệp đính kèm?",
|
||||
text: `Bạn chuẩn bị xóa ${selectedIds.length} tệp. Hành động này không thể hoàn tác!`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ef4444",
|
||||
cancelButtonColor: "#6b7280",
|
||||
confirmButtonText: "Xóa vĩnh viễn",
|
||||
cancelButtonText: "Hủy",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await deleteMedia(selectedIds);
|
||||
setLocalMedia((prev) =>
|
||||
prev.filter((item) => !selectedIds.includes(item.id)),
|
||||
);
|
||||
setIsSelectionMode(false);
|
||||
setSelectedIds([]);
|
||||
Swal.fire("Thành công!", "Các tệp đã được xóa.", "success");
|
||||
if (onRefresh) onRefresh();
|
||||
} catch (error) {
|
||||
Swal.fire("Lỗi!", "Không thể xóa tệp, vui lòng thử lại.", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFromLightbox = async () => {
|
||||
const currentImage = imageFiles[index];
|
||||
if (!currentImage) return;
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: "Xóa ảnh này?",
|
||||
text: "Bạn có chắc chắn muốn xóa ảnh này không?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ef4444",
|
||||
cancelButtonColor: "#6b7280",
|
||||
confirmButtonText: "Xóa",
|
||||
cancelButtonText: "Hủy",
|
||||
// customClass: { container: 'z-99999999999999999999999' }
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await deleteMedia([currentImage.id]);
|
||||
setLocalMedia((prev) =>
|
||||
prev.filter((item) => item.id !== currentImage.id),
|
||||
);
|
||||
|
||||
if (imageFiles.length === 1) {
|
||||
setIndex(-1);
|
||||
} else if (index >= imageFiles.length - 1) {
|
||||
setIndex(index - 1);
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Thành công!",
|
||||
text: "Ảnh đã được xóa.",
|
||||
icon: "success",
|
||||
customClass: { container: "z-[9999999999]" },
|
||||
});
|
||||
if (onRefresh) onRefresh();
|
||||
} catch (error) {
|
||||
Swal.fire({
|
||||
title: "Lỗi!",
|
||||
text: "Không thể xóa ảnh.",
|
||||
icon: "error",
|
||||
customClass: { container: "z-[9999999999]" },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItemCard = (item: any, isImage: boolean, idx: number) => {
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => handleItemClick(item, idx, isImage)}
|
||||
className={`group relative aspect-square cursor-pointer overflow-hidden rounded-xl border-2 transition-all ${
|
||||
isSelected
|
||||
? "border-blue-500 ring-2 ring-blue-500/30"
|
||||
: "border-gray-200 hover:ring-2 hover:ring-blue-500 hover:ring-offset-2 dark:border-zinc-700 dark:hover:ring-offset-zinc-900"
|
||||
} ${isImage ? "bg-gray-100 dark:bg-zinc-800" : "bg-gray-50 dark:bg-zinc-800"}`}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => toggleItemSelection(item.id, e)}
|
||||
className={`absolute right-2 top-2 z-30 flex h-6 w-6 items-center justify-center rounded-full border-2 transition-all duration-200 ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-500"
|
||||
: "border-white bg-black/40 opacity-0 group-hover:opacity-100"
|
||||
} ${isSelectionMode && !isSelected ? "opacity-100" : ""}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<svg
|
||||
className="h-3.5 w-3.5 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isImage ? (
|
||||
<>
|
||||
<img
|
||||
src={`${URL_MEDIA}${item.storage_key}`}
|
||||
alt={item.original_name}
|
||||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
className={`h-full w-full object-cover transition-transform duration-500 ${isSelected ? "scale-105 opacity-80" : "group-hover:scale-110"}`}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 flex items-end bg-black/40 p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<p className="w-full truncate text-xs text-white">
|
||||
{item.original_name}
|
||||
</p>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<div className="absolute bottom-0 w-full p-3 text-white">
|
||||
<p className="truncate text-sm font-medium ">
|
||||
{item.original_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-300">
|
||||
{(item.size / 1024).toFixed(0)} KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isSelectionMode && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<span className="rounded-full border border-white/30 bg-white/20 px-3 py-1 text-xs font-bold text-white backdrop-blur-md">
|
||||
Xem ảnh
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex h-full w-full flex-col items-center justify-center p-3 text-center transition-opacity ${isSelected ? "opacity-80" : ""}`}
|
||||
>
|
||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-white text-2xl dark:bg-zinc-700">
|
||||
📄
|
||||
</div>
|
||||
<span className="line-clamp-2 px-2 text-xs font-medium text-gray-600 dark:text-gray-300">
|
||||
{item.original_name}
|
||||
</span>
|
||||
</div>
|
||||
{!isSelectionMode && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<span className="rounded-full border border-white/30 bg-white/20 px-3 py-1 text-xs font-bold text-white backdrop-blur-md">
|
||||
Xem file
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<h3 className="text-xl font-bold text-gray-800 dark:text-white/90">
|
||||
Media Assets{" "}
|
||||
<span className="ml-2 text-sm font-normal text-gray-500">
|
||||
({localMedia.length} tệp)
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{selectedIds.length > 0 && (
|
||||
<button
|
||||
onClick={handleDeleteSelected}
|
||||
className="flex items-center gap-1 rounded-lg bg-red-50 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Xóa ({selectedIds.length})
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={toggleSelectionMode}
|
||||
className={`rounded-lg px-4 py-2 text-sm font-medium transition-all w-17.5 h-10 ${
|
||||
isSelectionMode
|
||||
? "bg-blue-500 text-white shadow-md hover:bg-blue-600"
|
||||
: "border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-200 dark:hover:bg-zinc-700"
|
||||
}`}
|
||||
>
|
||||
{isSelectionMode ? "Hủy" : "Chọn"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-10">
|
||||
{imageFiles.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Hình ảnh ({imageFiles.length})
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8">
|
||||
{" "}
|
||||
{displayedImages.map((item, idx) =>
|
||||
renderItemCard(item, true, idx),
|
||||
)}
|
||||
</div>
|
||||
{imageFiles.length > INITIAL_LIMIT && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<button
|
||||
onClick={() => setShowAllImages(!showAllImages)}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
|
||||
>
|
||||
{showAllImages
|
||||
? "Thu gọn"
|
||||
: `Xem thêm ${imageFiles.length - INITIAL_LIMIT} hình ảnh`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
|
||||
{documentFiles.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Tài liệu ({documentFiles.length})
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8">
|
||||
{" "}
|
||||
{displayedDocs.map((item, idx) =>
|
||||
renderItemCard(item, false, idx),
|
||||
)}
|
||||
</div>
|
||||
{documentFiles.length > INITIAL_LIMIT && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<button
|
||||
onClick={() => setShowAllDocs(!showAllDocs)}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
|
||||
>
|
||||
{showAllDocs
|
||||
? "Thu gọn"
|
||||
: `Xem thêm ${documentFiles.length - INITIAL_LIMIT} tài liệu`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Lightbox
|
||||
index={index}
|
||||
open={index >= 0}
|
||||
close={() => setIndex(-1)}
|
||||
slides={slides}
|
||||
slides={imageSlides}
|
||||
plugins={[Zoom, Captions]}
|
||||
zoom={{
|
||||
maxZoomPixelRatio: 10,
|
||||
zoomInMultiplier: 2,
|
||||
doubleTapDelay: 300,
|
||||
doubleClickDelay: 300,
|
||||
doubleClickMaxStops: 2,
|
||||
keyboardMoveDistance: 50,
|
||||
wheelZoomDistanceFactor: 100,
|
||||
pinchZoomDistanceFactor: 100,
|
||||
toolbar={{
|
||||
buttons: [
|
||||
<button
|
||||
key="delete"
|
||||
type="button"
|
||||
className="yarl__button text-red-500"
|
||||
title="Xóa ảnh này"
|
||||
onClick={handleDeleteFromLightbox}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>,
|
||||
"zoom",
|
||||
"close",
|
||||
],
|
||||
}}
|
||||
zoom={{ maxZoomPixelRatio: 3 }}
|
||||
animation={{ zoom: 200 }}
|
||||
styles={{
|
||||
root: {
|
||||
zIndex: 999999999,
|
||||
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.85)",
|
||||
},
|
||||
root: { zIndex: 99, "--yarl__color_backdrop": "rgba(0, 0, 0, 0.9)" },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user