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) => ( ))}
)}
{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 (
{t("downloads.share.views", { n: link.view_count })} {badges.map((b) => ( {b === t("downloads.share.hasPassword") && } {b} ))}
); } 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 ? (
{t("downloads.share.expiry")}: {EXPIRY_DAYS.map((d) => ( ))}
setPassword(e.target.value)} placeholder={t("downloads.share.password")} className={inputCls} autoComplete="new-password" />
) : ( )}
); } export default function ShareDialog({ job, onClose }: { job: DownloadJob; onClose: () => void }) { const { t } = useTranslation(); return (
); }