JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState } from "react";4import { Share2, Download, Eye, Link2, Copy, Check, Folder } from "lucide-react";5import toast from "react-hot-toast";6import Modal from "../ui/Modal";7import Button from "../ui/Button";89export interface ShareTarget {10 type: "file" | "folder";11 id: string;12 name: string;13}1415interface ShareModalProps {16 isOpen: boolean;17 onClose: () => void;18 target: ShareTarget;19}2021export async function copyToClipboard(text: string): Promise<boolean> {22 try {23 await navigator.clipboard.writeText(text);24 return true;25 } catch {26 const textarea = document.createElement("textarea");27 textarea.value = text;28 textarea.style.position = "fixed";29 textarea.style.opacity = "0";30 document.body.appendChild(textarea);31 textarea.select();32 try {33 document.execCommand("copy");34 return true;35 } catch {36 return false;37 } finally {38 document.body.removeChild(textarea);39 }40 }41}4243/** Création d'un lien de partage public (fichier ou dossier complet). */44export default function ShareModal({ isOpen, onClose, target }: ShareModalProps) {45 const [password, setPassword] = useState("");46 const [expiry, setExpiry] = useState("");47 const [mode, setMode] = useState<"DOWNLOAD" | "PREVIEW">("DOWNLOAD");48 const [loading, setLoading] = useState(false);49 const [link, setLink] = useState<string | null>(null);50 const [copied, setCopied] = useState(false);5152 const close = () => { setLink(null); setPassword(""); setExpiry(""); onClose(); };5354 const create = async () => {55 setLoading(true);56 try {57 const res = await fetch("/api/shares", {58 method: "POST",59 headers: { "Content-Type": "application/json" },60 body: JSON.stringify({61 [target.type === "folder" ? "folderId" : "fileId"]: target.id,62 password: password || undefined,63 expiresAt: expiry ? new Date(expiry).toISOString() : undefined,64 mode,65 }),66 });67 const data = await res.json().catch(() => ({}));68 if (res.ok) {69 const url = `${window.location.origin}/shared/${data.token}`;70 setLink(url);71 const ok = await copyToClipboard(url);72 toast.success(ok ? "Lien copié dans le presse-papiers" : "Lien de partage créé");73 } else {74 toast.error(data.error || "Erreur lors du partage");75 }76 } catch (err) {77 toast.error("Erreur réseau : " + (err as Error).message);78 } finally {79 setLoading(false);80 }81 };8283 const isFolder = target.type === "folder";8485 return (86 <Modal isOpen={isOpen} onClose={close} title={isFolder ? "Partager le dossier" : "Partager"} description={target.name}>87 {link ? (88 <div className="space-y-4">89 <div className="flex items-center gap-2 text-sm text-emerald-600 dark:text-emerald-400 font-medium">90 <Check className="w-4 h-4" /> Lien de partage créé91 </div>92 <div className="flex items-center gap-2">93 <div className="relative flex-1">94 <Link2 className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />95 <input type="text" value={link} readOnly className="field pl-9 text-xs font-mono" onClick={(e) => (e.target as HTMLInputElement).select()} />96 </div>97 <Button98 size="sm"99 variant={copied ? "secondary" : "accent"}100 onClick={async () => { await copyToClipboard(link); setCopied(true); toast.success("Lien copié"); setTimeout(() => setCopied(false), 1500); }}101 >102 {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}103 {copied ? "Copié" : "Copier"}104 </Button>105 </div>106 {isFolder && (107 <p className="text-[12.5px] text-gray-500 dark:text-gray-400 flex items-start gap-2">108 <Folder className="w-4 h-4 shrink-0 mt-0.5 text-blue-500" />109 Les visiteurs verront le contenu du dossier et de ses sous-dossiers (fichiers ajoutés plus tard inclus) et pourront tout télécharger en ZIP.110 </p>111 )}112 <Button variant="ghost" onClick={close} className="w-full">Fermer</Button>113 </div>114 ) : (115 <div className="space-y-4">116 <div>117 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">Mode</label>118 <div className="grid grid-cols-2 gap-2">119 {([["DOWNLOAD", "Téléchargement", Download], ["PREVIEW", "Aperçu seulement", Eye]] as const).map(([v, label, Icon]) => (120 <button121 key={v}122 type="button"123 onClick={() => setMode(v)}124 className={`flex items-center gap-2 px-3 h-11 rounded-xl border text-sm transition-colors ${125 mode === v126 ? "border-blue-500 bg-blue-500/10 text-blue-700 dark:text-blue-300 font-medium"127 : "border-line dark:border-white/10 text-gray-600 dark:text-gray-300 hover:border-gray-300 dark:hover:border-white/20"128 }`}129 >130 <Icon className="w-4 h-4" /> {label}131 </button>132 ))}133 </div>134 </div>135 <div>136 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">Mot de passe (optionnel)</label>137 <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Laisser vide pour aucun mot de passe" className="field" autoComplete="new-password" />138 </div>139 <div>140 <label className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">Expiration (optionnel)</label>141 <input type="datetime-local" value={expiry} onChange={(e) => setExpiry(e.target.value)} className="field" min={new Date(Date.now() + 60000).toISOString().slice(0, 16)} />142 </div>143 <div className="flex gap-2 justify-end pt-1">144 <Button variant="ghost" onClick={close}>Annuler</Button>145 <Button variant="accent" onClick={create} loading={loading}><Share2 className="w-4 h-4" /> Créer le lien</Button>146 </div>147 </div>148 )}149 </Modal>150 );151}152