feat(workspace): remove CreateWorkspaceModal and change the logic to redirect to WorkspaceCreate page
This commit is contained in:
@@ -1,252 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Search, Trash2, UserPlus, Loader2, AlertCircle } from "lucide-react";
|
|
||||||
import { searchUserByExactMobile, type SearchedUser } from "../api/users";
|
|
||||||
import { useTranslation } from "../hooks/useTranslation";
|
|
||||||
import { Modal } from "./Modal";
|
|
||||||
|
|
||||||
export interface WorkspaceMemberInput extends SearchedUser {
|
|
||||||
role: "admin" | "member";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateWorkspaceModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: { name: string; description: string; members: { user_id: string | number; role: string }[] }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreateWorkspaceModal: React.FC<CreateWorkspaceModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const { t, lang } = useTranslation();
|
|
||||||
const isFa = lang === "fa";
|
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [description, setDescription] = useState("");
|
|
||||||
const [members, setMembers] = useState<WorkspaceMemberInput[]>([]);
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [searchResult, setSearchResult] = useState<SearchedUser | null>(null);
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
const [searchError, setSearchError] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchQuery.trim().length < 11) {
|
|
||||||
setSearchResult(null);
|
|
||||||
setSearchError(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delayDebounceFn = setTimeout(async () => {
|
|
||||||
setIsSearching(true);
|
|
||||||
setSearchError(false);
|
|
||||||
|
|
||||||
const user = await searchUserByExactMobile(searchQuery.trim());
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
setSearchResult(user);
|
|
||||||
} else {
|
|
||||||
setSearchResult(null);
|
|
||||||
setSearchError(true);
|
|
||||||
}
|
|
||||||
setIsSearching(false);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(delayDebounceFn);
|
|
||||||
}, [searchQuery]);
|
|
||||||
|
|
||||||
const handleAddMember = () => {
|
|
||||||
if (!searchResult || members.some((m) => m.id === searchResult.id)) return;
|
|
||||||
|
|
||||||
setMembers([...members, { ...searchResult, role: "member" }]);
|
|
||||||
setSearchQuery("");
|
|
||||||
setSearchResult(null);
|
|
||||||
setSearchError(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveMember = (id: string | number) => {
|
|
||||||
setMembers(members.filter((m) => m.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRoleChange = (id: string | number, role: "admin" | "member") => {
|
|
||||||
setMembers(members.map((m) => (m.id === id ? { ...m, role } : m)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
members: members.map((m) => ({ user_id: m.id, role: m.role })),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
title={t.workspace?.createNew || "Create New Workspace"}
|
|
||||||
isFa={isFa}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{t.workspace?.cancel || "Cancel"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isLoading || !name.trim()}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
||||||
{isLoading ? (t.workspace?.creating || "Creating...") : (t.workspace?.submit || "Create")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-5 space-y-5">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
||||||
{t.workspace?.nameLabel || "Workspace Name"} <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
placeholder={t.workspace?.namePlaceholder || "e.g. Design Team"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
||||||
{t.workspace?.descriptionLabel || "Description"}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
rows={2}
|
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-transparent text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
||||||
placeholder={t.workspace?.descriptionPlaceholder || "What is this workspace for?"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 pt-4 border-t border-slate-200 dark:border-slate-800">
|
|
||||||
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
||||||
{t.workspace?.inviteMembers || "Invite Members (Exact Mobile Number)"}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div className={`absolute inset-y-0 ${isFa ? "right-0 pr-3" : "left-0 pl-3"} flex items-center pointer-events-none`}>
|
|
||||||
{isSearching ? (
|
|
||||||
<Loader2 className="w-4 h-4 text-blue-500 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Search className="w-4 h-4 text-slate-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder={t.workspace?.searchMemberPlaceholder || "e.g. 09123456789 or +989123456789"}
|
|
||||||
className={`w-full ${isFa ? "pr-9 pl-3" : "pl-9 pr-3"} py-2 border border-slate-300 dark:border-slate-700 rounded-lg bg-slate-50 dark:bg-slate-800/50 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error State */}
|
|
||||||
{searchError && (
|
|
||||||
<div className="flex items-center gap-2 p-3 text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-500/10 rounded-lg border border-red-100 dark:border-red-500/20">
|
|
||||||
<AlertCircle className="w-4 h-4" />
|
|
||||||
{t.workspace?.userNotFound || "No user found with this exact number."}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success State - Single Result */}
|
|
||||||
{searchResult && (
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/30 rounded-lg">
|
|
||||||
{searchResult.profile_picture ? (
|
|
||||||
<img src={searchResult.profile_picture} alt={searchResult.first_name} className="w-10 h-10 rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="w-10 h-10 rounded-full bg-blue-200 dark:bg-blue-900/50 flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-sm">
|
|
||||||
{searchResult.first_name?.[0] || "U"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">
|
|
||||||
{searchResult.first_name} {searchResult.last_name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 truncate" dir="ltr">
|
|
||||||
{searchResult.mobile}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleAddMember}
|
|
||||||
disabled={members.some(m => m.id === searchResult.id)}
|
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-400 text-white text-sm font-medium rounded-md transition-colors flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<UserPlus className="w-4 h-4" />
|
|
||||||
{members.some(m => m.id === searchResult.id)
|
|
||||||
? (t.workspace?.userAlreadyAdded || "Added")
|
|
||||||
: (t.workspace?.addMember || "Add")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Added Members List */}
|
|
||||||
{members.length > 0 && (
|
|
||||||
<div className="space-y-2 mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
|
||||||
<h4 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">
|
|
||||||
{t.workspace?.selectedMembers || "Selected Members"}
|
|
||||||
</h4>
|
|
||||||
{members.map((member) => (
|
|
||||||
<div key={member.id} className="flex items-center gap-3 p-2 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50">
|
|
||||||
{member.profile_picture ? (
|
|
||||||
<img src={member.profile_picture} alt={member.first_name} className="w-8 h-8 rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 font-medium text-sm">
|
|
||||||
{member.first_name?.[0] || "U"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">
|
|
||||||
{member.first_name} {member.last_name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={member.role}
|
|
||||||
onChange={(e) => handleRoleChange(member.id, e.target.value as "admin" | "member")}
|
|
||||||
className="px-2 py-1.5 border border-slate-300 dark:border-slate-700 rounded-md bg-white dark:bg-slate-900 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="member">{t.workspace?.roleMember || "Member"}</option>
|
|
||||||
<option value="admin">{t.workspace?.roleAdmin || "Admin"}</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveMember(member.id)}
|
|
||||||
className="p-1.5 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,13 @@ import React, { useState, useRef, useEffect } from "react";
|
|||||||
import { useWorkspace } from "../context/WorkspaceContext";
|
import { useWorkspace } from "../context/WorkspaceContext";
|
||||||
import { useTranslation } from "../hooks/useTranslation";
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
import { Check, ChevronDown, Plus, Briefcase, Settings } from "lucide-react";
|
import { Check, ChevronDown, Plus, Briefcase, Settings } from "lucide-react";
|
||||||
import { CreateWorkspaceModal } from "./CreateWorkspaceModal"; // Adjust path if needed
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const WorkspaceSelector: React.FC = () => {
|
export const WorkspaceSelector: React.FC = () => {
|
||||||
|
const { workspaces, activeWorkspace, setActiveWorkspace } = useWorkspace();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { workspaces, activeWorkspace, setActiveWorkspace, addWorkspace } = useWorkspace();
|
|
||||||
const { t, lang } = useTranslation();
|
const { t, lang } = useTranslation();
|
||||||
const isFa = lang === "fa";
|
const isFa = lang === "fa";
|
||||||
|
|
||||||
@@ -25,11 +22,6 @@ export const WorkspaceSelector: React.FC = () => {
|
|||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateWorkspace = async (data: { name: string; description: string; members: any[] }) => {
|
|
||||||
await addWorkspace(data);
|
|
||||||
setIsCreateModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
{/* Selector Button */}
|
{/* Selector Button */}
|
||||||
@@ -88,13 +80,14 @@ export const WorkspaceSelector: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setIsCreateModalOpen(true);
|
navigate("/workspaces/create");
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors"
|
className="w-full flex items-center gap-2 px-2 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-md transition-colors font-medium"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
{t.workspace?.createNew || "Create New Workspace"}
|
{t.workspace?.createNew || "Create New Workspace"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
@@ -108,12 +101,6 @@ export const WorkspaceSelector: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Advanced Create Workspace Modal */}
|
|
||||||
<CreateWorkspaceModal
|
|
||||||
isOpen={isCreateModalOpen}
|
|
||||||
onClose={() => setIsCreateModalOpen(false)}
|
|
||||||
onSubmit={handleCreateWorkspace}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user