"use client"
import { useEffect, useMemo, useState } from "react"
import { Icon } from "@iconify/react"
import {
Building2,
Copy,
Eye,
Link2,
Loader2,
Mail,
Pencil,
RefreshCw,
Shield,
SlidersHorizontal,
Trash2,
UserRound,
Users,
} from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { useSearchContacts } from "@/lib/api/hooks/use-contact-queries"
import { useDriveShares, useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
import type { DriveFileInfo, DriveShare } from "@/lib/api/types"
import { displayFileName } from "@/lib/drive/display-file-name"
import {
FOLDER_SHARE_PERMISSION_OPTIONS,
folderPermissionsFromRole,
folderPermissionsToBitmask,
type FolderSharePermissionId,
type FolderSharePermissions,
} from "@/lib/drive/drive-share-permissions"
import {
NC_SHARE_TYPE,
SHARE_SECTION_LABELS,
groupSharesBySection,
shareAccessLabel,
shareLinkForCopy,
shareMetaLine,
shareOwnerLabel,
shareRecipientLabel,
type DriveShareMode,
type ShareListSection,
} from "@/lib/drive/drive-share-types"
import { DriveFileTypeIcon } from "@/lib/drive/drive-file-icon"
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
import {
DRIVE_BTN_GHOST,
DRIVE_BTN_PRIMARY,
DRIVE_CARD_ACTIVE,
DRIVE_CARD_IDLE,
DRIVE_DIALOG_CONTENT,
DRIVE_DIALOG_DIVIDER,
DRIVE_DIALOG_FOOTER,
DRIVE_DIALOG_HEADER,
DRIVE_DIALOG_OVERLAY,
DRIVE_FIELD_CLASS,
DRIVE_LABEL_CLASS,
DRIVE_PANEL_MUTED,
DRIVE_TEXT_PRIMARY,
DRIVE_TEXT_SECONDARY,
DRIVE_TEXT_TITLE,
DRIVE_TEXTAREA_CLASS,
} from "@/lib/drive/drive-dialog-styles"
import { cn } from "@/lib/utils"
function shareItemLabel(path: string) {
const trimmed = path.replace(/\/+$/, "")
const base = trimmed.slice(trimmed.lastIndexOf("/") + 1)
return displayFileName(base || path)
}
type SharePermissionMode = "viewer" | "editor" | "advanced"
const PERMISSION_MODE_OPTIONS: {
id: SharePermissionMode
label: string
description: string
icon: typeof Eye
folderOnly?: boolean
}[] = [
{
id: "viewer",
label: "Lecteur",
description: "Consultation uniquement",
icon: Eye,
},
{
id: "editor",
label: "Éditeur",
description: "Peut modifier le contenu",
icon: Pencil,
},
{
id: "advanced",
label: "Avancé",
description: "Définir chaque autorisation",
icon: SlidersHorizontal,
folderOnly: true,
},
]
const MODE_OPTIONS: {
id: DriveShareMode
label: string
description: string
icon: typeof Link2
}[] = [
{
id: "contact",
label: "Personne",
description: "Partage direct par e-mail ou compte",
icon: UserRound,
},
{
id: "internal",
label: "Lien interne",
description: "Réservé aux utilisateurs inscrits connectés",
icon: Users,
},
{
id: "public",
label: "Lien public",
description: "Accessible à toute personne disposant du lien",
icon: Link2,
},
]
function shareSectionIcon(section: ShareListSection) {
if (section === "people") return UserRound
if (section === "groups") return Building2
return Link2
}
function ShareEntryRow({
share,
onDelete,
deleting,
}: {
share: DriveShare
onDelete: () => void
deleting: boolean
}) {
const url = shareLinkForCopy(share)
const recipient = shareRecipientLabel(share)
const owner = shareOwnerLabel(share)
const meta = shareMetaLine(share)
const accessLabel = shareAccessLabel(share)
const isLink = share.share_type === NC_SHARE_TYPE.LINK
const copy = async () => {
if (!url) return
try {
await navigator.clipboard.writeText(url)
toast.success("Lien copié")
} catch {
toast.error("Impossible de copier le lien")
}
}
const primaryLine = recipient ?? (url && isLink ? url : accessLabel)
return (
{accessLabel}
{share.has_password ? (
Mot de passe
) : null}
{primaryLine}
{url && isLink && recipient ? (
{url}
) : null}
{owner ? (
Propriétaire · {owner}
) : null}
{meta}
{share.note?.trim() ? (
« {share.note.trim()} »
) : null}
{url ? (
) : null}
)
}
function ActiveSharesPanel({
shares,
loading,
error,
onRetry,
deletingShareId,
onDeleteShare,
}: {
shares: DriveShare[]
loading: boolean
error: boolean
onRetry: () => void
deletingShareId: string | null
onDeleteShare: (shareId: string) => void
}) {
const grouped = useMemo(() => groupSharesBySection(shares), [shares])
const sectionOrder: ShareListSection[] = ["links", "people", "groups"]
const hasShares = shares.length > 0
return (
Accès existants
{!loading ? (
) : null}
{loading ? (
Chargement des partages…
) : error ? (
Impossible de charger les partages existants.
) : !hasShares ? (
Aucun partage actif pour cet élément. Créez un lien ou invitez une personne ci-dessus.
) : (
{sectionOrder.map((section) => {
const items = grouped[section]
if (items.length === 0) return null
const SectionIcon = shareSectionIcon(section)
return (
{SHARE_SECTION_LABELS[section]}
({items.length})
{items.map((share) => (
onDeleteShare(share.id)}
/>
))}
)
})}
)}
)
}
function SharePermissionsPanel({
isFolder,
permissionMode,
folderPermissions,
onPermissionModeChange,
onFolderPermissionChange,
}: {
isFolder: boolean
permissionMode: SharePermissionMode
folderPermissions: FolderSharePermissions
onPermissionModeChange: (mode: SharePermissionMode) => void
onFolderPermissionChange: (id: FolderSharePermissionId, checked: boolean) => void
}) {
const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions)
const modeOptions = PERMISSION_MODE_OPTIONS.filter((option) => isFolder || !option.folderOnly)
return (
{modeOptions.map((option) => {
const IconComponent = option.icon
const selected = permissionMode === option.id
return (
)
})}
{isFolder && permissionMode === "advanced" ? (
{FOLDER_SHARE_PERMISSION_OPTIONS.map((option) => {
const checked = folderPermissions[option.id]
const checkboxId = `drive-share-perm-${option.id}`
return (
onFolderPermissionChange(option.id, value === true)
}
/>
)
})}
{!folderPermissions.viewContent && folderPermissions.addFiles ? (
Dépôt uniquement : les visiteurs pourront ajouter des fichiers sans voir le contenu
existant du dossier.
) : null}
{advancedPermissionBits === 0 ? (
Sélectionnez au moins une autorisation.
) : null}
) : null}
)
}
export function ShareDialog() {
const path = useDriveUIStore((s) => s.sharePath)
const shareItemType = useDriveUIStore((s) => s.shareItemType)
const setSharePath = useDriveUIStore((s) => s.setSharePath)
const [mode, setMode] = useState("public")
const [permissionMode, setPermissionMode] = useState("viewer")
const [folderPermissions, setFolderPermissions] = useState(
() => folderPermissionsFromRole("viewer")
)
const [contactEmail, setContactEmail] = useState("")
const [contactQuery, setContactQuery] = useState("")
const [contactNote, setContactNote] = useState("")
const [recipientRegistered, setRecipientRegistered] = useState(null)
const [deletingShareId, setDeletingShareId] = useState(null)
const { data, isLoading: sharesLoading, isError: sharesError, refetch: refetchShares } = useDriveShares(
path ?? "",
Boolean(path)
)
const { createShare, deleteShare, lookupShareRecipient } = useDriveMutations()
const lookupRecipientEmail = lookupShareRecipient.mutateAsync
const { data: contactResults = [] } = useSearchContacts(contactQuery)
const itemLabel = useMemo(() => (path ? shareItemLabel(path) : ""), [path])
const isFolder = shareItemType === "directory"
const selectedMode = MODE_OPTIONS.find((m) => m.id === mode) ?? MODE_OPTIONS[0]
const filePreview = useMemo((): DriveFileInfo | null => {
if (!path) return null
return {
path,
name: itemLabel,
type: shareItemType ?? "file",
size: 0,
mime_type: "",
last_modified: "",
etag: "",
is_favorite: false,
}
}, [path, itemLabel, shareItemType])
useEffect(() => {
if (path) {
setMode("public")
setPermissionMode("viewer")
setFolderPermissions(folderPermissionsFromRole("viewer"))
setContactEmail("")
setContactQuery("")
setContactNote("")
setRecipientRegistered(null)
}
}, [path])
useEffect(() => {
const email = contactEmail.trim().toLowerCase()
if (mode !== "contact" || !email.includes("@")) {
setRecipientRegistered(null)
return
}
const timer = window.setTimeout(() => {
lookupRecipientEmail(email)
.then((res) => setRecipientRegistered(res.registered))
.catch(() => setRecipientRegistered(null))
}, 350)
return () => window.clearTimeout(timer)
}, [contactEmail, mode, lookupRecipientEmail])
const advancedPermissionBits = folderPermissionsToBitmask(folderPermissions)
const canCreateShare =
(mode !== "contact" || contactEmail.trim().includes("@")) &&
(!isFolder || permissionMode !== "advanced" || advancedPermissionBits > 0)
const setFolderPermission = (id: FolderSharePermissionId, checked: boolean) => {
setFolderPermissions((prev) => ({ ...prev, [id]: checked }))
}
const onPermissionModeChange = (nextMode: SharePermissionMode) => {
setPermissionMode(nextMode)
if (nextMode === "advanced") {
setFolderPermissions(folderPermissionsFromRole(permissionMode === "editor" ? "editor" : "viewer"))
}
}
const close = () => setSharePath(null, null)
const sharePayload = () => {
const base =
isFolder && permissionMode === "advanced"
? { path: path!, permissions: advancedPermissionBits }
: { path: path!, role: permissionMode === "editor" ? "editor" : "viewer" }
return base
}
const onShare = async () => {
if (!path || !canCreateShare) return
try {
const payload = sharePayload()
const share = await createShare.mutateAsync({
...payload,
mode,
...(mode === "contact"
? {
share_with: contactEmail.trim().toLowerCase(),
note: contactNote.trim() || undefined,
send_mail: true,
}
: {}),
})
if (mode === "contact") {
if (share.access_mode === "user" || share.share_type === NC_SHARE_TYPE.USER) {
toast.success("Partagé — visible dans « Partagés avec moi » du destinataire")
} else {
toast.success("Invitation envoyée par e-mail avec un lien public")
}
setContactEmail("")
setContactNote("")
setContactQuery("")
} else {
const link = shareLinkForCopy(share)
if (link) {
await navigator.clipboard.writeText(link)
toast.success(
mode === "internal"
? "Lien interne copié dans le presse-papiers"
: "Lien public copié dans le presse-papiers"
)
} else {
toast.success("Partage créé")
}
}
void refetchShares()
} catch {
toast.error("Partage impossible")
}
}
const onDeleteShare = async (shareId: string) => {
setDeletingShareId(shareId)
try {
await deleteShare.mutateAsync(shareId)
toast.success("Partage supprimé")
void refetchShares()
} catch {
toast.error("Suppression impossible")
} finally {
setDeletingShareId(null)
}
}
const existingShares = data?.shares ?? []
const actionLabel =
mode === "contact"
? "Partager"
: mode === "internal"
? "Créer le lien interne"
: "Créer le lien public"
return (
)
}