This commit is contained in:
2026-04-17 18:01:19 +07:00
parent 5b6b59dc01
commit 9d35fd3653
6 changed files with 467 additions and 258 deletions

View File

@@ -4,11 +4,19 @@ import { setSelectedApplication } from "@/store/features/userSlice";
import { useRouter } from "next/navigation";
import { useDispatch } from "react-redux";
import { URL_MEDIA } from "../../../api";
import { statusConfig } from "@/service/handler";
const formatFullDateTime = (dateString: string) => {
const date = new Date(dateString);
const time = date.toLocaleTimeString("vi-VN", { hour: "2-digit", minute: "2-digit" });
const day = date.toLocaleDateString("vi-VN", { day: "2-digit", month: "2-digit", year: "numeric" });
const time = date.toLocaleTimeString("vi-VN", {
hour: "2-digit",
minute: "2-digit",
});
const day = date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
return `${time} ${day}`;
};
@@ -20,18 +28,29 @@ const processMedia = (mediaArray: any[]) => {
return isImageMime || isImageExt;
});
const docFiles = mediaArray.filter((file) => {
const isImage = file.mime_type?.startsWith("image/") || /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
const isImage =
file.mime_type?.startsWith("image/") ||
/\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
return !isImage;
});
if (imageFiles.length > 0) return { type: "image", src: `${URL_MEDIA}${imageFiles[0].storage_key}` };
if (imageFiles.length > 0)
return { type: "image", src: `${URL_MEDIA}${imageFiles[0].storage_key}` };
if (docFiles.length > 0) {
const extensions = docFiles.map((file) => file.mime_type ? file.mime_type.split("/")[1] : file.storage_key.split(".").pop() || "file");
const extensions = docFiles.map((file) =>
file.mime_type
? file.mime_type.split("/")[1]
: file.storage_key.split(".").pop() || "file",
);
return { type: "documents", extensions };
}
return { type: "empty" };
};
export default function ApplicationSquareCardList({ applications }: { applications: any[] }) {
export default function ApplicationSquareCardList({
applications,
}: {
applications: any[];
}) {
const router = useRouter();
const dispatch = useDispatch();
@@ -45,16 +64,21 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
<h4 className="text-lg font-bold text-zinc-800 dark:text-white/90 mb-5 tracking-tight">
Applications CV
</h4>
<div className="flex flex-wrap gap-4">
{applications?.map((app) => {
const mediaState = processMedia(app.media);
// --- LOGIC STATUS NẰM TRONG VÒNG LẶP ---
const config = statusConfig[app.status] || statusConfig.PENDING;
return (
<div
key={app.id}
onClick={() => handleViewDetail(app)}
className="group relative h-40 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 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"
>
{/* BACKGROUND */}
<div className="absolute inset-0 z-0">
{mediaState.type === "image" ? (
<img
@@ -67,7 +91,10 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
{mediaState.type === "documents" ? (
<div className="flex flex-wrap gap-1 justify-center">
{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">
<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"
>
.{ext}
</span>
))}
@@ -79,15 +106,24 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
)}
</div>
{/* OVERLAY */}
<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" />
{/* TOP INFO: STATUS & FILE COUNT */}
<div className="absolute top-2 left-2 right-2 z-20 flex justify-between items-start">
<div className={`flex items-center gap-1.5 rounded-md backdrop-blur-md border border-white/10 ${
app.status === "PENDING" ? "bg-amber-500/20 text-amber-400" : "bg-emerald-500/20 text-emerald-400"
}`}>
<span className={`w-2 h-2 rounded-full animate-pulse ${app.status === "PENDING" ? "bg-amber-500" : "bg-emerald-500"}`} />
<div
className={`flex items-center p-1 rounded-full border ${config.container}`}
>
<span
className={`w-2 h-2 rounded-full ${config.dot} ${
app.status === "PENDING" ? "animate-pulse" : ""
}`}
/>
{/* <span className="text-[9px] font-bold uppercase tracking-wider">
{app.status}
</span> */}
</div>
{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
@@ -95,13 +131,14 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
)}
</div>
{/* BOTTOM INFO */}
<div className="absolute bottom-2 left-2 right-2 z-20 text-white">
<div className="mb-1">
<p className="text-[10px] font-black tracking-tighter truncate">
<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-white/50 truncate">
<p className="text-[9px] font-medium text-blue-400 truncate">
By: {app.reviewer.display_name}
</p>
)}
@@ -112,8 +149,18 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
{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">
<svg className="w-4 h-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
<svg
className="w-4 h-4 text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
</div>
</div>
@@ -124,4 +171,4 @@ export default function ApplicationSquareCardList({ applications }: { applicatio
</div>
</div>
);
}
}