SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
2 h agolast push
JavaScript 56.7% TypeScript 42.6%
6.2 KB · 152 lines tsx
Raw Blame History
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