Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions components/headshots/DisplaySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from "react";
import { Member, TEAMS } from "../../constants/headshot-constants";

interface DisplaySectionProps {
headshot: Member;
localHeadshot: Member;
showTeamsDropdown: boolean;
onFieldChange: (field: keyof Member, value: string | string[]) => void;
onTeamToggle: (teamId: string) => void;
onToggleDropdown: () => void;
getTeamNamesForDisplay: (teamIds: string[]) => string;
}

const DisplaySection: React.FC<DisplaySectionProps> = ({
headshot,
localHeadshot,
showTeamsDropdown,
onFieldChange,
onTeamToggle,
onToggleDropdown,
getTeamNamesForDisplay,
}) => {
return (
<section className="flex items-start gap-2 w-2/3">
<div className="grid grid-rows-3 gap-2 w-full">
<div className="flex flex-col items-start gap-2 w-full">
<label className="text-xs font-medium">
name{" "}
{headshot.isDuplicate && (
<span className="text-error font-bold">
FOUND EXISTING NAME1!!!!
</span>
)}
</label>
<input
type="text"
value={localHeadshot.name}
onChange={(e) => onFieldChange("name", e.target.value)}
placeholder="Enter name"
className="w-full border rounded px-2 py-1"
/>
</div>

<div className="flex flex-col items-start gap-2 w-full">
<label className="text-xs font-medium">role</label>
<input
type="text"
value={localHeadshot.role}
onChange={(e) => onFieldChange("role", e.target.value)}
placeholder="Enter role"
className="w-full border rounded px-2 py-1"
/>
</div>

<div className="flex flex-col items-start gap-2 w-full relative">
<label className="text-xs font-medium">teams</label>
<div className="relative w-full">
<button
type="button"
onClick={onToggleDropdown}
className="w-full border rounded px-2 py-1 flex items-center justify-between"
>
<span className="text-sm">
{localHeadshot.teams.length > 0
? getTeamNamesForDisplay(localHeadshot.teams)
: "Select teams..."}
</span>
<svg
className={`w-4 h-4 ml-2 transition-transform ${
showTeamsDropdown ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>

{showTeamsDropdown && (
<div className="absolute z-20 w-full mt-1 bg-white border rounded max-h-40 overflow-y-scroll">
{TEAMS.map((team) => (
<label
key={team.id}
className="flex items-center px-3 py-2 cursor-pointer"
>
<input
type="checkbox"
checked={localHeadshot.teams.includes(team.id)}
onChange={() => onTeamToggle(team.id)}
className="mr-2"
/>
<span className="text-sm">{team.name}</span>
</label>
))}
</div>
)}
</div>
</div>
</div>
{localHeadshot.img && (
<div className="w-40 h-40 relative flex-shrink-0 ml-4">
<img
src={localHeadshot.img}
alt={localHeadshot.name || "default member"}
className="w-full h-full object-cover rounded-full"
/>
</div>
)}
</section>
);
};

export default DisplaySection;
120 changes: 120 additions & 0 deletions components/headshots/HeadshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from "react";
import {
DEFAULT_PHOTO,
TEAMS,
Member,
} from "../../constants/headshot-constants";
import InputSection from "./InputSection";
import DisplaySection from "./DisplaySection";

interface HeadshotCardProps {
headshot: Member;
index: number;
onDelete: (index: number) => void;
onUpdate: (index: number, updates: Partial<Member>) => void;
onUrlChange: (index: number, url: string) => void;
onAddEmpty: (index: number) => void;
onCheckExisting?: (index: number, name: string) => boolean;
getExistingMember?: (name: string) => Member | null;
}

const HeadshotCard: React.FC<HeadshotCardProps> = ({
headshot,
index,
onDelete,
onUpdate,
onUrlChange,
onAddEmpty,
onCheckExisting,
getExistingMember,
}) => {
const [localHeadshot, setLocalHeadshot] = useState(headshot);
const [showTeamsDropdown, setShowTeamsDropdown] = useState(false);

useEffect(() => {
setLocalHeadshot(headshot);
}, [headshot]);

const existingMember =
getExistingMember && localHeadshot.name.trim()
? getExistingMember(localHeadshot.name)
: null;

const isEmptyCard =
!localHeadshot.img &&
!localHeadshot.name &&
!localHeadshot.role &&
localHeadshot.teams.length === 0;

const handleFieldChange = (field: keyof Member, value: string | string[]) => {
const newHeadshot = { ...localHeadshot, [field]: value };
setLocalHeadshot(newHeadshot);
onUpdate(index, newHeadshot);
};

const handleUrlChange = (url: string) => {
setLocalHeadshot((prev) => ({ ...prev, img: url }));
onUrlChange(index, url);
};

const handleAddEmpty = () => {
handleUrlChange(DEFAULT_PHOTO);
onAddEmpty(index);
};

const handleReplaceWithExisting = () => {
if (!onCheckExisting || !localHeadshot.name.trim()) return;
onCheckExisting(index, localHeadshot.name);
};

const handleTeamToggle = (teamId: string) => {
const newTeams = localHeadshot.teams.includes(teamId)
? localHeadshot.teams.filter((t) => t !== teamId)
: [...localHeadshot.teams, teamId];
handleFieldChange("teams", newTeams);
};

const getTeamNamesForDisplay = (teamIds: string[]) => {
return teamIds
.map((id) => TEAMS.find((team) => team.id === id)?.name || id)
.join(", ");
};

return (
<section
className={`flex w-full p-4 shadow-md space-x-4 relative ${
headshot.isDuplicate ? "border-2 border-yellow-200" : ""
}`}
>
<button
onClick={() => onDelete(index)}
className="absolute top-0 right-0 text-black w-6 h-6 flex items-center justify-center text-s"
aria-label="Delete headshot"
>
×
</button>

<InputSection
imageUrl={localHeadshot.img}
onUrlChange={handleUrlChange}
isEmptyCard={isEmptyCard}
isDuplicate={localHeadshot.isDuplicate}
onAddEmpty={handleAddEmpty}
onReplaceWithExisting={handleReplaceWithExisting}
existingMember={existingMember}
getTeamNamesForDisplay={getTeamNamesForDisplay}
/>
<DisplaySection
headshot={headshot}
localHeadshot={localHeadshot}
showTeamsDropdown={showTeamsDropdown}
onFieldChange={handleFieldChange}
onTeamToggle={handleTeamToggle}
onToggleDropdown={() => setShowTeamsDropdown(!showTeamsDropdown)}
getTeamNamesForDisplay={getTeamNamesForDisplay}
/>
</section>
);
};

export default HeadshotCard;
84 changes: 84 additions & 0 deletions components/headshots/InputSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from "react";
import { Member } from "../../constants/headshot-constants";

interface InputSectionProps {
imageUrl: string;
onUrlChange: (url: string) => void;
isEmptyCard: boolean;
isDuplicate?: boolean;
onAddEmpty: () => void;
onReplaceWithExisting: () => void;
existingMember: Member | null;
getTeamNamesForDisplay: (teamIds: string[]) => string;
}

const InputSection: React.FC<InputSectionProps> = ({
imageUrl,
onUrlChange,
isEmptyCard,
isDuplicate,
onAddEmpty,
onReplaceWithExisting,
existingMember,
getTeamNamesForDisplay,
}) => {
return (
<section className="flex flex-col items-start gap-2 w-1/3">
<label className="text-xs font-medium">firebase URL</label>
<input
type="text"
value={imageUrl}
onChange={(e) => onUrlChange(e.target.value)}
className="w-full border rounded"
placeholder="Enter image URL"
/>

{isEmptyCard && (
<button
onClick={onAddEmpty}
className="px-3 py-1 bg-sky-500 text-white text-sm font-semibold hover:bg-blue-300 rounded"
>
add empty member
</button>
)}

{isDuplicate && (
<button
onClick={onReplaceWithExisting}
className="w-full px-3 py-1 bg-pink-300 text-white text-sm font-semibold hover:bg-pink-500 rounded"
>
replace w existing member
</button>
)}

{existingMember && (
<div className="w-full border rounded p-2">
<div className="text-xs font-semibold mb-2">
previous memberdetails:
</div>
<div className="flex flex-col items-center">
{existingMember.img && (
<img
src={existingMember.img}
alt={existingMember.name}
className="w-16 h-16 object-cover rounded-full mb-2"
/>
)}
<div className="text-xs text-center">
<div className="font-medium">{existingMember.name}</div>
<div className="text-xs">{existingMember.role}</div>
{existingMember.teams.length > 0 && (
<div className="text-xs">
{getTeamNamesForDisplay(existingMember.teams)}
</div>
)}
<div className="text-xs">{existingMember.term}</div>
</div>
</div>
</div>
)}
</section>
);
};

export default InputSection;
2 changes: 2 additions & 0 deletions components/members/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export function roleType(role: string): number {
// EXEC TEAM ROLES
["president", 0],
["co-president", 0],
["director lead", 0],
["internal director", 1],
["external director", 1],
["vp engineering", 2],
Expand All @@ -15,6 +16,7 @@ export function roleType(role: string): number {
["vp finance", 9],
["vp talent", 10],
// CLUB SUPPORT ROLES
["finance coordinator", 10],
["graphic designer", 11],
["content strategist", 12],
["user researcher", 13],
Expand Down
Loading
Loading