import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, Copy, Link2, Lock, Trash2, UserPlus, Eye } from "lucide-react";
import clsx from "clsx";
import Modal from "./Modal";
import { useConfirm } from "./ConfirmProvider";
import { notify } from "../lib/notifications";
import { api, type DownloadJob, type ShareLink } from "../lib/api";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
const btn = "px-3 py-1.5 rounded-lg text-sm font-medium transition disabled:opacity-50";
const EXPIRY_DAYS = [null, 1, 7, 30] as const;
// --- share with a registered user (ACL) -----------------------------------------------------
function UserShare({ job }: { job: DownloadJob }) {
const { t } = useTranslation();
const [q, setQ] = useState("");
const [open, setOpen] = useState(false);
const recipients = useQuery({ queryKey: ["share-recipients"], queryFn: api.shareRecipients });
const list = recipients.data ?? [];
const filtered = useMemo(() => {
const s = q.trim().toLowerCase();
if (!s) return list.slice(0, 8);
return list
.filter((u) => u.email.toLowerCase().includes(s) || (u.display_name ?? "").toLowerCase().includes(s))
.slice(0, 8);
}, [q, list]);
const share = useMutation({
mutationFn: (email: string) => api.shareDownload(job.id, email),
onSuccess: (r) => {
notify({ level: "success", message: t("downloads.share.done", { email: r.shared_with }) });
setQ("");
setOpen(false);
},
onError: () => notify({ level: "error", message: t("downloads.share.failed") }),
});
return (
{t("downloads.share.userSection")}
{t("downloads.share.userHint")}
{
setQ(e.target.value);
setOpen(true);
}}
onFocus={() => setOpen(true)}
// Close on blur, but after a tick so a click on a dropdown row still registers.
onBlur={() => setTimeout(() => setOpen(false), 150)}
placeholder={t("downloads.share.pickPlaceholder")}
className={inputCls}
/>
{open && filtered.length > 0 && (
{filtered.map((u) => (
share.mutate(u.email)}
disabled={share.isPending}
className="w-full text-left px-3 py-2 text-sm hover:bg-surface transition flex flex-col"
>
{u.display_name || u.email}
{u.display_name && {u.email} }
))}
)}
{recipients.data && list.length === 0 && (
{t("downloads.share.noUsers")}
)}
);
}
// --- share by public link -------------------------------------------------------------------
function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }) {
const { t } = useTranslation();
const confirm = useConfirm();
const [copied, setCopied] = useState(false);
const fullUrl = window.location.origin + link.url;
const copy = async () => {
try {
await navigator.clipboard.writeText(fullUrl);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
notify({ level: "error", message: t("downloads.share.copyFailed") });
}
};
const toggle = useMutation({
mutationFn: () => api.updateDownloadLink(link.id, { allow_download: !link.allow_download }),
onSuccess: onChanged,
});
const revoke = useMutation({
mutationFn: () => api.revokeDownloadLink(link.id),
onSuccess: onChanged,
});
const badges: string[] = [];
if (link.has_password) badges.push(t("downloads.share.hasPassword"));
if (link.allow_download) badges.push(t("downloads.share.downloadable"));
if (link.expires_at) badges.push(t("downloads.share.expiresOn", { date: new Date(link.expires_at).toLocaleDateString() }));
return (
);
}
function LinkShare({ job }: { job: DownloadJob }) {
const { t } = useTranslation();
const qc = useQueryClient();
const links = useQuery({ queryKey: ["download-links", job.id], queryFn: () => api.downloadLinks(job.id) });
const [creating, setCreating] = useState(false);
const [allowDownload, setAllowDownload] = useState(false);
const [expiryDays, setExpiryDays] = useState(null);
const [password, setPassword] = useState("");
const invalidate = () => qc.invalidateQueries({ queryKey: ["download-links", job.id] });
const create = useMutation({
mutationFn: () =>
api.createDownloadLink(job.id, {
allow_download: allowDownload,
expires_days: expiryDays,
password: password.trim() || undefined,
}),
onSuccess: () => {
invalidate();
setCreating(false);
setAllowDownload(false);
setExpiryDays(null);
setPassword("");
},
});
return (
{t("downloads.share.linkSection")}
{t("downloads.share.linkHint")}
{(links.data ?? []).map((l) => (
))}
{links.data && links.data.length === 0 && !creating && (
{t("downloads.share.noLinks")}
)}
{creating ? (
) : (
setCreating(true)}
className={clsx(btn, "mt-2 border border-border text-muted hover:text-fg hover:border-muted inline-flex items-center gap-1.5")}
>
{t("downloads.share.newLink")}
)}
);
}
export default function ShareDialog({ job, onClose }: { job: DownloadJob; onClose: () => void }) {
const { t } = useTranslation();
return (
);
}