Skip to content

Commit 714ddf9

Browse files
Zeff01claude
andcommitted
feat: add modal-based member selection for project team configuration
Replaces dropdown-based team leader and sub lead selection with an improved modal UX similar to AddMembersModal. **New Component:** - SelectMemberModal: Reusable modal for selecting team members - Search functionality with live filtering - Filter tabs (Smart Filter, Mentors, Graduated, Admins, All) - Single selection mode and optional-single mode (with clear) - Exclusion list to prevent duplicate selections - Clean, modern UI with better visibility **Changes to ProjectEditModal:** - Replace CustomSelect for Team Leader with button + SelectMemberModal - Replace CustomSelect for Sub Lead with button + SelectMemberModal - Team Leader: Single selection, required - Sub Lead: Optional selection, can be cleared - Both exclude selected team members from options **Benefits:** - Same great UX as AddMembersModal (search, filters, recently added) - Newly added users (like Ron Nucum) appear immediately - Better visibility with larger modal and grid view - Easier to find and select team members - Consistent UX across all member selection interfaces Fixes the issue where users couldn't easily find newly added mentors in the team leader dropdown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 10d2462 commit 714ddf9

2 files changed

Lines changed: 338 additions & 74 deletions

File tree

‎apps/codebility/app/home/projects/_components/ProjectEditModal.tsx‎

Lines changed: 83 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
DialogTitle,
2121
} from "@/components/ui/dialog";
2222
import { MemberSelection } from "@/components/ui/MemberSelection";
23+
import { SelectMemberModal } from "@/components/ui/SelectMemberModal";
2324
import { Skeleton } from "@/components/ui/skeleton/skeleton";
2425
import { useModal as useGlobalModal } from "@/hooks/use-modal";
2526
import { useModal } from "@/hooks/use-modal-projects";
@@ -80,6 +81,10 @@ const ProjectEditModal = () => {
8081
// ─────────────────────────────────────────────────────────────────────────
8182
const [selectedMembers, setSelectedMembers] = useState<Codev[]>([]);
8283

84+
// Modal states for member selection
85+
const [teamLeaderModalOpen, setTeamLeaderModalOpen] = useState(false);
86+
const [subLeadModalOpen, setSubLeadModalOpen] = useState(false);
87+
8388
// Image states
8489
const [projectImage, setProjectImage] = useState<string | null>(null);
8590
const [openImageCropper, setOpenImageCropper] = useState(false);
@@ -669,91 +674,71 @@ const ProjectEditModal = () => {
669674
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
670675
Team Leader *
671676
</label>
672-
<CustomSelect
673-
options={enhancedUserOptions.filter(
674-
(user) => !selectedMembers.find((member) => member.id === user.value),
677+
<button
678+
type="button"
679+
onClick={() => setTeamLeaderModalOpen(true)}
680+
className="w-full h-11 px-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-left flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
681+
>
682+
{currentTeamLeader ? (
683+
<>
684+
<img
685+
src={currentTeamLeader.image_url || "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg"}
686+
alt={`${currentTeamLeader.first_name} ${currentTeamLeader.last_name}`}
687+
className="w-8 h-8 rounded-full object-cover"
688+
/>
689+
<div className="flex-1 min-w-0">
690+
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
691+
{currentTeamLeader.first_name} {currentTeamLeader.last_name}
692+
</div>
693+
{currentTeamLeader.display_position && (
694+
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
695+
{currentTeamLeader.display_position}
696+
</div>
697+
)}
698+
</div>
699+
</>
700+
) : (
701+
<span className="text-gray-400">Select Team Leader</span>
675702
)}
676-
value={currentTeamLeader?.id || ""}
677-
onChange={(value) => {
678-
const leader = users.find((u) => u.id === value);
679-
if (leader) setCurrentTeamLeader(leader);
680-
}}
681-
placeholder="Select Team Leader"
682-
disabled={isUsersLoading}
683-
searchable
684-
/>
703+
</button>
685704
</div>
686705

687-
{/* ── CBP-116: Sublead selector ─────────────────────────────────────
688-
Optional — no asterisk, no toast validation required.
689-
Excluded from members list and excluded from team leader list.
690-
Clearing the select (empty string value) sets sublead to null.
691-
──────────────────────────────────────────────────────────────────── */}
706+
{/* Sub Lead */}
692707
<div className="space-y-1">
693708
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
694709
Sub Lead{" "}
695710
<span className="font-normal text-gray-500 dark:text-gray-400">
696711
(Optional — acts as team lead when lead is unavailable)
697712
</span>
698713
</label>
699-
<CustomSelect
700-
options={[
701-
// "None" option — value must not be empty string (Radix UI Select.Item constraint)
702-
{ id: "none", value: "none", label: "No sublead assigned", subLabel: "" },
703-
// Only show members of this project, excluding the team leader.
704-
// Source: fullProjectData.project_members (bypasses RLS filtering
705-
// that drops some users from getProjectCodevs results).
706-
...(fullProjectData?.project_members ?? [])
707-
.filter(
708-
(pm: any) =>
709-
pm.codev &&
710-
pm.codev_id !== currentTeamLeader?.id &&
711-
pm.role !== "team_leader",
712-
)
713-
.map((pm: any) => ({
714-
id: pm.codev_id,
715-
value: pm.codev_id,
716-
label: `${pm.codev.first_name} ${pm.codev.last_name}`,
717-
subLabel: pm.codev.display_position || "",
718-
imageUrl: pm.codev.image_url || null,
719-
})),
720-
]}
721-
value={currentSubLead?.id || "none"}
722-
onChange={(value) => {
723-
if (!value || value === "none") {
724-
// User selected "None" — clear the sublead
725-
setCurrentSubLead(null);
726-
return;
727-
}
728-
// Find in users first, fall back to project_members embedded data
729-
const subLeadFromUsers = users.find((u) => u.id === value);
730-
if (subLeadFromUsers) {
731-
setCurrentSubLead(subLeadFromUsers);
732-
return;
733-
}
734-
// Fallback: construct from fullProjectData (handles RLS-filtered users)
735-
const subLeadMember = fullProjectData?.project_members?.find(
736-
(pm: any) => pm.codev_id === value,
737-
);
738-
if (subLeadMember?.codev) {
739-
setCurrentSubLead({
740-
id: subLeadMember.codev_id,
741-
first_name: subLeadMember.codev.first_name,
742-
last_name: subLeadMember.codev.last_name,
743-
image_url: subLeadMember.codev.image_url || null,
744-
display_position: subLeadMember.codev.display_position || null,
745-
email_address: subLeadMember.codev.email_address || "",
746-
positions: [],
747-
tech_stacks: [],
748-
} as unknown as Codev);
749-
}
750-
}}
751-
placeholder="Select Sub Lead"
752-
disabled={isUsersLoading}
753-
searchable
754-
/>
714+
<button
715+
type="button"
716+
onClick={() => setSubLeadModalOpen(true)}
717+
className="w-full h-11 px-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-left flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
718+
>
719+
{currentSubLead ? (
720+
<>
721+
<img
722+
src={currentSubLead.image_url || "https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg"}
723+
alt={`${currentSubLead.first_name} ${currentSubLead.last_name}`}
724+
className="w-8 h-8 rounded-full object-cover"
725+
/>
726+
<div className="flex-1 min-w-0">
727+
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
728+
{currentSubLead.first_name} {currentSubLead.last_name}
729+
</div>
730+
{currentSubLead.display_position && (
731+
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
732+
{currentSubLead.display_position}
733+
</div>
734+
)}
735+
</div>
736+
</>
737+
) : (
738+
<span className="text-gray-400">No sublead assigned</span>
739+
)}
740+
</button>
755741
</div>
756-
{/* ─────────────────────────────────────────────────────────────── */}
757742

758743
{/* Team Members */}
759744
<div className="space-y-1">
@@ -1005,6 +990,30 @@ const ProjectEditModal = () => {
1005990
</div>
1006991
</DialogFooter>
1007992
</DialogContent>
993+
994+
{/* Team Leader Selection Modal */}
995+
<SelectMemberModal
996+
isOpen={teamLeaderModalOpen}
997+
onClose={() => setTeamLeaderModalOpen(false)}
998+
onSelect={(member) => setCurrentTeamLeader(member)}
999+
users={users}
1000+
selectedMember={currentTeamLeader}
1001+
title="Select Team Leader"
1002+
mode="single"
1003+
excludeUserIds={selectedMembers.map(m => m.id)}
1004+
/>
1005+
1006+
{/* Sub Lead Selection Modal */}
1007+
<SelectMemberModal
1008+
isOpen={subLeadModalOpen}
1009+
onClose={() => setSubLeadModalOpen(false)}
1010+
onSelect={(member) => setCurrentSubLead(member)}
1011+
users={users}
1012+
selectedMember={currentSubLead}
1013+
title="Select Sub Lead"
1014+
mode="optional-single"
1015+
excludeUserIds={[currentTeamLeader?.id || '', ...selectedMembers.map(m => m.id)].filter(Boolean)}
1016+
/>
10081017
</Dialog>
10091018
);
10101019
};

0 commit comments

Comments
 (0)