Update Document Upload module
This commit is contained in:
parent
cab7f25f91
commit
4633e16094
|
|
@ -0,0 +1,321 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { AlertCircle, CalendarDays, Download, FileText, Mail, Phone, Upload } from "lucide-react";
|
||||||
|
import { PanelShell } from "../panels/PanelShell";
|
||||||
|
import { listMedia, uploadMediaFile } from "../../../services/api";
|
||||||
|
import type { LibraryContent } from "../../../services/api/types";
|
||||||
|
|
||||||
|
const SUPPORTED_MIME_TYPES = [
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/bmp",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUPPORTED_EXTENSIONS = [
|
||||||
|
".pdf",
|
||||||
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".xls",
|
||||||
|
".xlsx",
|
||||||
|
".ppt",
|
||||||
|
".pptx",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".bmp",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isSupportedFile(file: File) {
|
||||||
|
const name = file.name.toLowerCase();
|
||||||
|
const hasSupportedExtension = SUPPORTED_EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
|
const hasSupportedMime = SUPPORTED_MIME_TYPES.includes(file.type);
|
||||||
|
return hasSupportedExtension || hasSupportedMime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string) {
|
||||||
|
if (!value) return "—";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "—";
|
||||||
|
return new Intl.DateTimeFormat("en", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploaderName(item: LibraryContent) {
|
||||||
|
if (typeof item.uploadedBy === "object" && item.uploadedBy) {
|
||||||
|
const parts = [item.uploadedBy.firstName, item.uploadedBy.lastName].filter(Boolean);
|
||||||
|
if (parts.length) return parts.join(" ");
|
||||||
|
return item.uploadedBy.email || "Unknown uploader";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploaderEmail(item: LibraryContent) {
|
||||||
|
if (typeof item.uploadedBy === "object" && item.uploadedBy) return item.uploadedBy.email || "—";
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploaderPhone(item: LibraryContent) {
|
||||||
|
if (typeof item.uploadedBy === "object" && item.uploadedBy) return item.uploadedBy.phone || "—";
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventDocumentsPanel({
|
||||||
|
eventId,
|
||||||
|
eventName,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
eventId: string;
|
||||||
|
eventName?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [documents, setDocuments] = useState<LibraryContent[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [success, setSuccess] = useState("");
|
||||||
|
|
||||||
|
const loadDocuments = useCallback(async () => {
|
||||||
|
if (!eventId) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const res = await listMedia({ eventId, limit: 100 });
|
||||||
|
setDocuments(res.media ?? []);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Could not load documents.");
|
||||||
|
setDocuments([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadDocuments();
|
||||||
|
}, [loadDocuments]);
|
||||||
|
|
||||||
|
const acceptAttribute = useMemo(() => SUPPORTED_MIME_TYPES.join(","), []);
|
||||||
|
|
||||||
|
const handleUpload = async (file: File | null) => {
|
||||||
|
if (!file) return;
|
||||||
|
if (!isSupportedFile(file)) {
|
||||||
|
setError("Only PDF, Word, Excel, PowerPoint, and image files are supported.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUploading(true);
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
try {
|
||||||
|
await uploadMediaFile(file, { eventId, module: "event-documents", visibility: "private" });
|
||||||
|
setSuccess("Document uploaded successfully.");
|
||||||
|
await loadDocuments();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Upload failed.");
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelShell
|
||||||
|
onClose={onClose}
|
||||||
|
title="Upload Documents"
|
||||||
|
breadcrumb={eventName ? `Event · ${eventName}` : "Event documents"}
|
||||||
|
description="Upload and manage event documents for this event."
|
||||||
|
completedCount={1}
|
||||||
|
totalSections={1}
|
||||||
|
ctaLabel="Close"
|
||||||
|
hidePrimaryCta
|
||||||
|
onCtaClick={onClose}
|
||||||
|
panelWidth="min(1100px, 100vw)"
|
||||||
|
showProgress={false}
|
||||||
|
>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.15fr) minmax(320px, 0.85fr)", gap: "20px", alignItems: "start" }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||||||
|
<div style={{ display: "flex", paddingLeft: "19px", alignItems: "center", justifyContent: "space-between", gap: "10px" }}>
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: "var(--text-base)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--card-foreground)" }}>
|
||||||
|
Uploaded documents
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
|
||||||
|
Files uploaded by you for this event appear here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px", padding: "10px 12px", paddingLeft: "19px", borderRadius: "var(--radius-button)", background: "color-mix(in srgb, var(--destructive) 10%, transparent)", color: "var(--destructive)", fontSize: "var(--text-sm)" }}>
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px", padding: "10px 12px", paddingLeft: "19px", borderRadius: "var(--radius-button)", background: "color-mix(in srgb, var(--primary) 12%, transparent)", color: "var(--primary)", fontSize: "var(--text-sm)" }}>
|
||||||
|
<FileText size={16} />
|
||||||
|
<span>{success}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p style={{ margin: 0, color: "var(--muted)", fontSize: "var(--text-sm)" }}>Loading documents…</p>
|
||||||
|
) : documents.length === 0 ? (
|
||||||
|
<div style={{ border: "1px dashed var(--border)", borderRadius: "var(--radius-card)", padding: "24px", paddingLeft: "12px", background: "var(--secondary)", textAlign: "center" }}>
|
||||||
|
<p style={{ margin: 0, paddingLeft: "12px", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
|
||||||
|
No documents uploaded yet for this event.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "10px", padding: "14px 14px 8px", }}>
|
||||||
|
{documents.map((item) => (
|
||||||
|
<div key={item._id} style={{ border: "1px solid var(--border)", borderRadius: "var(--radius-card)", padding: "14px", background: "var(--card)" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "10px" }}>
|
||||||
|
{/* <div style={{ display: "flex", alignItems: "flex-start", gap: "10px", minWidth: 0 }}>
|
||||||
|
<div style={{ width: "36px", height: "36px", borderRadius: "50%", background: "color-mix(in srgb, var(--primary) 12%, transparent)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
||||||
|
<FileText size={18} />
|
||||||
|
</div>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<p style={{ margin: 0, fontSize: "var(--text-sm)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--card-foreground)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||||
|
{item.name || "Untitled document"}
|
||||||
|
</p>
|
||||||
|
<span style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", whiteSpace: "nowrap", flexShrink: 0,}}> • {getUploaderName(item) || "Uploaded by you"}
|
||||||
|
</span>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
|
||||||
|
{getUploaderName(item) || "Uploaded by you"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: "10px",
|
||||||
|
minWidth: 0,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* File Name */}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "var(--text-sm)",
|
||||||
|
fontFamily: "var(--font-inter)",
|
||||||
|
fontWeight: "var(--font-weight-medium)",
|
||||||
|
color: "var(--card-foreground)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name || "Untitled document"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Submitted by + Date */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
marginTop: "4px",
|
||||||
|
fontSize: "var(--text-xs)",
|
||||||
|
fontFamily: "var(--font-noto-sans)",
|
||||||
|
color: "var(--muted)",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Submitted by {getUploaderName(item) || "You"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>{formatDateTime(item.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.url ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => window.open(item.url, "_blank", "noopener,noreferrer")}
|
||||||
|
style={{ border: "1px solid var(--border)", background: "var(--card)", color: "var(--card-foreground)", borderRadius: "var(--radius-button)", padding: "6px 10px", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: "6px", flexShrink: 0, marginBottom: 0,}}
|
||||||
|
>
|
||||||
|
<Download size={14} />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: "8px", marginTop: "10px" }}>
|
||||||
|
{
|
||||||
|
// <div style={{ padding: "10px 10px", borderRadius: "var(--radius-button)", background: "var(--secondary)" }}>
|
||||||
|
// <p style={{ margin: 0, fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--muted)" }}>Uploaded by</p>
|
||||||
|
// <p style={{ margin: "4px 0 0", fontSize: "var(--text-sm)", color: "var(--card-foreground)" }}>{getUploaderEmail(item)}</p>
|
||||||
|
// </div>
|
||||||
|
// <div style={{ padding: "10px 10px", borderRadius: "var(--radius-button)", background: "var(--secondary)" }}>
|
||||||
|
// <p style={{ margin: 0, fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--muted)" }}>Submitted</p>
|
||||||
|
// <p style={{ margin: "4px 0 0", fontSize: "var(--text-sm)", color: "var(--card-foreground)" }}>{formatDateTime(item.createdAt)}</p>
|
||||||
|
// </div>
|
||||||
|
// <div style={{ padding: "10px 10px", borderRadius: "var(--radius-button)", background: "var(--secondary)" }}>
|
||||||
|
// <p style={{ margin: 0, fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--muted)" }}>Phone</p>
|
||||||
|
// <p style={{ margin: "4px 0 0", fontSize: "var(--text-sm)", color: "var(--card-foreground)" }}>{getUploaderPhone(item)}</p>
|
||||||
|
// </div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ border: "1px solid var(--border)", borderRadius: "var(--radius-card)", padding: "16px", marginTop: "8px", background: "var(--card)", display: "flex", flexDirection: "column", gap: "12px", width: "calc(100% - 20px)" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
|
<div style={{ width: "36px", height: "36px", borderRadius: "50%", background: "color-mix(in srgb, var(--primary) 12%, transparent)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Upload size={18} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: 0, fontSize: "var(--text-sm)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--card-foreground)" }}>Upload a document</p>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>Allowed: PDF, Word, Excel, PowerPoint, and images.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input ref={fileInputRef} type="file" accept={acceptAttribute} onChange={(e) => { void handleUpload(e.target.files?.[0] ?? null); }} style={{ display: "none" }} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "8px", border: "none", borderRadius: "var(--radius-button)", background: "var(--primary)", color: "var(--primary-foreground)", padding: "10px 12px", cursor: uploading ? "not-allowed" : "pointer", opacity: uploading ? 0.7 : 1 }}
|
||||||
|
>
|
||||||
|
{uploading ? "Uploading…" : "Upload file"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style={{ display: "grid", gap: "8px", fontSize: "var(--text-xs)", color: "var(--muted)", fontFamily: "var(--font-noto-sans)" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}><FileText size={14} /> PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}><CalendarDays size={14} /> JPG, PNG, GIF, BMP, WEBP images</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}><Mail size={14} /> The uploader name, email, phone, and upload date are shown in the list.</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}><Phone size={14} /> Files are stored for this event and available for download.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PanelShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue