SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
yesterdaylast push
JavaScript 56.7% TypeScript 42.6%
7.8 KB · 222 lines tsx
Raw Blame History
1"use client";23import { useState, useEffect } from "react";4import { Users, Check, Search } from "lucide-react";5import Modal from "../ui/Modal";6import Button from "../ui/Button";7import toast from "react-hot-toast";8import { useRouter } from "next/navigation";910interface UserOption {11  id: string;12  name: string | null;13  username: string | null;14}1516interface CreateSharedFolderModalProps {17  isOpen: boolean;18  onClose: () => void;19  onCreated: () => void;20}2122const inputClasses = "field";2324export default function CreateSharedFolderModal({25  isOpen,26  onClose,27  onCreated,28}: CreateSharedFolderModalProps) {29  const router = useRouter();30  const [name, setName] = useState("");31  const [users, setUsers] = useState<UserOption[]>([]);32  const [selectedIds, setSelectedIds] = useState<string[]>([]);33  const [userSearch, setUserSearch] = useState("");34  const [loading, setLoading] = useState(false);35  const [loadingUsers, setLoadingUsers] = useState(false);3637  useEffect(() => {38    if (!isOpen) return;39    setLoadingUsers(true);40    fetch("/api/users/public")41      .then((r) => r.ok ? r.json() : [])42      .then((data) => setUsers(Array.isArray(data) ? data : []))43      .finally(() => setLoadingUsers(false));44  }, [isOpen]);4546  const handleClose = () => {47    setName("");48    setSelectedIds([]);49    setUserSearch("");50    onClose();51  };5253  const toggleUser = (id: string) => {54    setSelectedIds((prev) =>55      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]56    );57  };5859  const handleCreate = async () => {60    if (!name.trim()) return;61    setLoading(true);62    try {63      const res = await fetch("/api/shared-spaces", {64        method: "POST",65        headers: { "Content-Type": "application/json" },66        body: JSON.stringify({ name: name.trim(), memberIds: selectedIds }),67      });6869      if (res.ok) {70        const space = await res.json();71        toast.success(`Dossier partagé "${name}" créé`);72        handleClose();73        onCreated();74        // Navigate to the new shared space75        router.push(`/dashboard/shared-space/${space.id}`);76      } else {77        const data = await res.json();78        toast.error(data.error || "Erreur lors de la création");79      }80    } catch {81      toast.error("Erreur réseau");82    } finally {83      setLoading(false);84    }85  };8687  const filteredUsers = users.filter((u) => {88    const q = userSearch.toLowerCase();89    return (90      u.username?.toLowerCase().includes(q) ||91      u.name?.toLowerCase().includes(q)92    );93  });9495  return (96    <Modal isOpen={isOpen} onClose={handleClose} title="Nouveau dossier partagé">97      {/* Name */}98      <div className="mb-4">99        <label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">100          Nom du dossier101        </label>102        <input103          type="text"104          value={name}105          onChange={(e) => setName(e.target.value)}106          onKeyDown={(e) => e.key === "Enter" && handleCreate()}107          placeholder="Ex: Projet Alpha, Famille..."108          className={inputClasses}109          autoFocus110        />111      </div>112113      {/* User picker */}114      <div className="mb-4">115        <label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5 flex items-center gap-1.5">116          <Users className="w-3.5 h-3.5" />117          Partager avec118          {selectedIds.length > 0 && (119            <span className="ml-auto text-blue-600 dark:text-blue-400 font-semibold">120              {selectedIds.length} sélectionné(s)121            </span>122          )}123        </label>124125        {loadingUsers ? (126          <div className="flex items-center justify-center py-6 text-sm text-gray-400">127            <div className="w-4 h-4 border-2 border-gray-300 dark:border-white/20 border-t-blue-700 rounded-full animate-spin mr-2" />128            Chargement des utilisateurs...129          </div>130        ) : users.length === 0 ? (131          <p className="text-xs text-gray-400 text-center py-4">132            Aucun autre utilisateur disponible.133          </p>134        ) : (135          <div className="border border-gray-200 dark:border-white/10 rounded-lg overflow-hidden">136            {/* Search within users */}137            {users.length > 4 && (138              <div className="p-2 border-b border-gray-100 dark:border-white/10 relative">139                <Search className="w-3.5 h-3.5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" />140                <input141                  type="text"142                  value={userSearch}143                  onChange={(e) => setUserSearch(e.target.value)}144                  placeholder="Filtrer les utilisateurs..."145                  className="w-full pl-7 pr-3 py-1.5 text-xs bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded focus:outline-none text-gray-900 dark:text-white"146                />147              </div>148            )}149150            <div className="max-h-44 overflow-auto">151              {filteredUsers.length === 0 && (152                <p className="text-xs text-gray-400 text-center py-3">Aucun résultat</p>153              )}154              {filteredUsers.map((u) => {155                const isSelected = selectedIds.includes(u.id);156                const displayName = u.name || u.username || u.id;157                const initial = displayName[0].toUpperCase();158                return (159                  <button160                    key={u.id}161                    onClick={() => toggleUser(u.id)}162                    className={`w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors border-b border-gray-50 dark:border-white/5 last:border-0 ${163                      isSelected164                        ? "bg-blue-50 dark:bg-blue-500/10"165                        : "hover:bg-gray-50 dark:hover:bg-white/5"166                    }`}167                  >168                    {/* Avatar */}169                    <div className={`w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0 text-xs font-semibold ${170                      isSelected171                        ? "bg-blue-700 text-white"172                        : "bg-gray-200 dark:bg-white/10 text-gray-600 dark:text-gray-300"173                    }`}>174                      {isSelected ? <Check className="w-3.5 h-3.5" /> : initial}175                    </div>176177                    <div className="flex-1 min-w-0">178                      {u.name && <p className="text-sm font-medium text-gray-900 dark:text-white truncate">{u.name}</p>}179                      <p className={`text-xs truncate ${u.name ? "text-gray-400" : "text-sm font-medium text-gray-900 dark:text-white"}`}>180                        @{u.username}181                      </p>182                    </div>183184                    {/* Checkbox indicator */}185                    <div className={`w-4 h-4 rounded border flex-shrink-0 flex items-center justify-center transition-colors ${186                      isSelected187                        ? "bg-blue-700 border-blue-700"188                        : "border-gray-300 dark:border-white/20"189                    }`}>190                      {isSelected && <Check className="w-3 h-3 text-white" />}191                    </div>192                  </button>193                );194              })}195            </div>196          </div>197        )}198      </div>199200      {/* Info note */}201      <p className="text-xs text-gray-400 dark:text-gray-500 mb-4 flex items-start gap-1.5">202        <span className="text-blue-400 mt-0.5">ℹ</span>203        Vous serez automatiquement membre. Le dossier apparaîtra dans la sidebar de chaque membre.204      </p>205206      <div className="flex gap-2 justify-end">207        <Button variant="ghost" onClick={handleClose}>208          Annuler209        </Button>210        <Button211          onClick={handleCreate}212          loading={loading}213          disabled={!name.trim()}214        >215          <Users className="w-4 h-4" />216          Créer le dossier partagé217        </Button>218      </div>219    </Modal>220  );221}222