mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
212 lines
6.9 KiB
TypeScript
212 lines
6.9 KiB
TypeScript
import type { Publication } from "@edr/types";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { Loader2, UploadCloud } from "lucide-react";
|
|
import { useRef, useState, type ReactNode } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { api } from "@/services/api";
|
|
|
|
export interface EditPublicationDialogProps {
|
|
mode?: "create" | "edit";
|
|
publication?: Publication;
|
|
children: ReactNode;
|
|
}
|
|
|
|
const ACCEPT =
|
|
".pdf,.md,.markdown,.ppt,.pptx,application/pdf,text/markdown,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
|
|
export default function EditPublicationDialog({
|
|
mode = "create",
|
|
publication,
|
|
children,
|
|
}: EditPublicationDialogProps) {
|
|
const isEdit = mode === "edit";
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [open, setOpen] = useState(false);
|
|
const [title, setTitle] = useState(publication?.title ?? "");
|
|
const [description, setDescription] = useState(publication?.description ?? "");
|
|
const [category, setCategory] = useState(publication?.category ?? "");
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [progress, setProgress] = useState<number | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const createMutation = useMutation(api.publications.create.mutationOptions());
|
|
const updateMutation = useMutation(api.publications.update.mutationOptions());
|
|
const replaceFileMutation = useMutation(api.publications.replaceFile.mutationOptions());
|
|
const pending =
|
|
createMutation.isPending || updateMutation.isPending || replaceFileMutation.isPending;
|
|
|
|
const reset = () => {
|
|
setTitle(publication?.title ?? "");
|
|
setDescription(publication?.description ?? "");
|
|
setCategory(publication?.category ?? "");
|
|
setFile(null);
|
|
setProgress(null);
|
|
setError(null);
|
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
setError(null);
|
|
if (!title.trim()) {
|
|
setError("Title is required.");
|
|
return;
|
|
}
|
|
if (!isEdit && !file) {
|
|
setError("Choose a file to upload.");
|
|
return;
|
|
}
|
|
|
|
const meta = {
|
|
title: title.trim(),
|
|
description: description.trim() || undefined,
|
|
category: category.trim() || undefined,
|
|
};
|
|
|
|
try {
|
|
if (isEdit && publication) {
|
|
await updateMutation.mutateAsync({ id: publication.id, dto: meta });
|
|
if (file) {
|
|
await replaceFileMutation.mutateAsync({
|
|
id: publication.id,
|
|
file,
|
|
onProgress: setProgress,
|
|
});
|
|
}
|
|
} else if (file) {
|
|
await createMutation.mutateAsync({ file, meta, onProgress: setProgress });
|
|
}
|
|
setOpen(false);
|
|
if (!isEdit) reset();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Something went wrong. Try again.");
|
|
} finally {
|
|
setProgress(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(next) => {
|
|
setOpen(next);
|
|
if (!next) reset();
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
|
|
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg rounded-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl font-bold">
|
|
{isEdit ? "Edit publication" : "New publication"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{isEdit
|
|
? "Update this document's title, description or category, or replace its file."
|
|
: "Upload a PDF, Markdown or PowerPoint file for the public library."}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="grid gap-5 py-4">
|
|
<div className="space-y-2">
|
|
<Label>Title *</Label>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="e.g. EDR Freight Platform Guide"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Category</Label>
|
|
<Input
|
|
value={category}
|
|
onChange={(e) => setCategory(e.target.value)}
|
|
placeholder="e.g. Guides, Reports"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Description</Label>
|
|
<Textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="What this document covers…"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>{isEdit ? "Replace file (optional)" : "File *"}</Label>
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="cursor-pointer rounded-xl border-2 border-dashed border-slate-300 px-4 py-6 text-center hover:bg-slate-50"
|
|
>
|
|
{progress !== null ? (
|
|
<p className="text-sm text-slate-500">Uploading… {progress}%</p>
|
|
) : file ? (
|
|
<p className="text-sm font-medium text-slate-700">{file.name}</p>
|
|
) : isEdit && publication ? (
|
|
<p className="text-sm text-slate-500">
|
|
Currently <span className="font-medium">{publication.fileName}</span> —
|
|
click to replace
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col items-center gap-1 text-slate-500">
|
|
<UploadCloud className="h-6 w-6" />
|
|
<span className="text-sm">Click to choose a PDF, Markdown or PowerPoint file</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept={ACCEPT}
|
|
hidden
|
|
onChange={(e) => setFile(e.currentTarget.files?.[0] ?? null)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error ? (
|
|
<p className="rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700">{error}</p>
|
|
) : null}
|
|
|
|
<div className="mt-2 flex justify-end gap-3">
|
|
<DialogClose asChild>
|
|
<Button variant="outline" disabled={pending}>
|
|
Cancel
|
|
</Button>
|
|
</DialogClose>
|
|
<Button
|
|
type="button"
|
|
onClick={() => void handleSubmit()}
|
|
disabled={pending}
|
|
className="bg-[#10B981] text-white hover:bg-[#10B981]/90 disabled:opacity-60"
|
|
>
|
|
{pending ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : isEdit ? (
|
|
"Save changes"
|
|
) : (
|
|
"Upload"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|