Skip to content

Commit 6491939

Browse files
authored
Merge pull request #13 from prgrms-aibe-devcourse/feat/11-dashboard-page-enhancement
[Feat] 대시보드 상위 통계 및 내 팀 기능 추가
2 parents c5b5e4d + 4d18a33 commit 6491939

2 files changed

Lines changed: 451 additions & 254 deletions

File tree

src/app/components/TeamInviteModal.tsx

Lines changed: 134 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import { useState } from "react";
1+
import { useState, useMemo } from "react";
22
import type { ReactNode } from "react";
3-
import { AlertCircle, ChevronRight, Copy, Folder, Globe2, Link2, Lock, Play, Trash2, X } from "lucide-react";
3+
import { AlertCircle, ChevronRight, Copy, Folder, Github, Globe2, Link2, Lock, Mail, Play, Trash2, X } from "lucide-react";
44
import { AnimatePresence, motion } from "motion/react";
55

66
interface TeamInviteModalProps {
77
isOpen: boolean;
88
onClose: () => void;
99
}
1010

11+
const CURRENT_USER_ID = "junwoo";
12+
1113
interface TeamMember {
1214
id: string;
1315
name: string;
1416
avatar: string;
1517
role: "owner" | "editor" | "viewer";
1618
warning?: boolean;
19+
isGithub?: boolean;
1720
}
1821

1922
const initialMembers: TeamMember[] = [
23+
{ id: "junwoo", name: "김준우", avatar: "준", role: "owner" },
2024
{ id: "jaejun", name: "김재준", avatar: "재", role: "editor" },
2125
{ id: "jinpil", name: "김진필", avatar: "필", role: "editor" },
22-
{ id: "junwoo", name: "김준우", avatar: "준", role: "owner" },
2326
{ id: "jinhyun", name: "김진현", avatar: "현", role: "editor", warning: true },
2427
{ id: "hyun", name: "안현", avatar: "안", role: "editor", warning: true }
2528
];
@@ -32,27 +35,36 @@ const permissionLabels = {
3235

3336
export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
3437
const [inviteValue, setInviteValue] = useState("");
38+
const [inviteMode, setInviteMode] = useState<"email" | "github">("email");
3539
const [members, setMembers] = useState(initialMembers);
3640
const [copyState, setCopyState] = useState<"idle" | "copied">("idle");
3741
const [confirmTarget, setConfirmTarget] = useState<TeamMember | null>(null);
3842

43+
// Current user always first, rest follow in original order
44+
const sortedMembers = useMemo(() => {
45+
const me = members.find((m) => m.id === CURRENT_USER_ID);
46+
const others = members.filter((m) => m.id !== CURRENT_USER_ID);
47+
return me ? [me, ...others] : others;
48+
}, [members]);
49+
3950
if (!isOpen) return null;
4051

4152
const canInvite = inviteValue.trim().length > 0;
4253

4354
const handleInvite = () => {
44-
const emails = inviteValue
45-
.split(";")
46-
.map((email) => email.trim())
55+
const values = inviteValue
56+
.split(/[,;]/)
57+
.map((v) => v.trim())
4758
.filter(Boolean);
4859

49-
if (emails.length === 0) return;
60+
if (values.length === 0) return;
5061

51-
const invitedMembers = emails.map((email, index) => ({
62+
const invitedMembers = values.map((val, index) => ({
5263
id: `${Date.now()}-${index}`,
53-
name: email,
54-
avatar: email.charAt(0).toUpperCase(),
55-
role: "editor" as const
64+
name: val,
65+
avatar: val.charAt(0).toUpperCase(),
66+
role: "editor" as const,
67+
isGithub: inviteMode === "github",
5668
}));
5769

5870
setMembers((prev) => [...invitedMembers, ...prev]);
@@ -143,6 +155,30 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
143155
</div>
144156

145157
<div className="px-5 py-5" style={{ maxHeight: "58vh", overflowY: "auto" }}>
158+
{/* Mode toggle */}
159+
<div className="flex gap-1 mb-3 p-1 rounded-xl" style={{ background: "#f3f4f6" }}>
160+
{(["email", "github"] as const).map((mode) => (
161+
<button
162+
key={mode}
163+
type="button"
164+
onClick={() => { setInviteMode(mode); setInviteValue(""); }}
165+
className="flex flex-1 items-center justify-center gap-1.5 rounded-lg py-2 tracking-tight transition-all"
166+
style={{
167+
background: inviteMode === mode ? "#ffffff" : "transparent",
168+
color: inviteMode === mode ? "#111827" : "#6b7280",
169+
fontSize: "13px",
170+
fontWeight: 900,
171+
border: "none",
172+
cursor: "pointer",
173+
boxShadow: inviteMode === mode ? "0 1px 4px rgba(0,0,0,0.10)" : "none",
174+
}}
175+
>
176+
{mode === "email" ? <Mail size={14} /> : <Github size={14} />}
177+
{mode === "email" ? "이메일로 초대" : "GitHub ID로 초대"}
178+
</button>
179+
))}
180+
</div>
181+
146182
<div className="flex gap-2">
147183
<input
148184
value={inviteValue}
@@ -153,7 +189,11 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
153189
handleInvite();
154190
}
155191
}}
156-
placeholder="쉼표 또는 세미콜론으로 구분된 이메일을 추가하여 초대하세요"
192+
placeholder={
193+
inviteMode === "email"
194+
? "쉼표 또는 세미콜론으로 구분된 이메일을 추가하여 초대하세요"
195+
: "쉼표 또는 세미콜론으로 구분된 GitHub ID를 추가하여 초대하세요"
196+
}
157197
className="min-w-0 flex-1 rounded-lg px-4 py-3 outline-none"
158198
style={{
159199
border: "2px solid #6d5dfc",
@@ -170,7 +210,7 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
170210
className="rounded-lg border-0 px-5 py-3 tracking-tight"
171211
style={{
172212
background: canInvite ? "#6d5dfc" : "#d1d5db",
173-
color: canInvite ? "#ffffff" : "#ffffff",
213+
color: "#ffffff",
174214
cursor: canInvite ? "pointer" : "not-allowed",
175215
fontSize: "14px",
176216
fontWeight: 900
@@ -197,66 +237,88 @@ export function TeamInviteModal({ isOpen, onClose }: TeamInviteModalProps) {
197237
action={<ChevronRight size={16} />}
198238
/>
199239

200-
{members.map((member) => (
201-
<div key={member.id} className="flex items-center gap-3 rounded-xl px-1 py-2">
202-
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full" style={{
203-
background: member.id === "junwoo" ? "#ff5b8a" : "linear-gradient(135deg, #20e3ff, #7c3aed)",
204-
color: "#ffffff",
205-
fontSize: "12px",
206-
fontWeight: 950
207-
}}>
208-
{member.avatar}
209-
</div>
210-
<span className="min-w-0 flex-1 truncate tracking-tight" style={{
211-
color: "#374151",
212-
fontSize: "14px",
213-
fontWeight: 900
214-
}}>
215-
{member.name}
216-
</span>
217-
{member.warning && (
218-
<AlertCircle size={18} style={{ color: "#f59e0b" }} />
219-
)}
220-
{member.role === "owner" ? (
221-
<span className="tracking-tight" style={{ color: "#374151", fontSize: "13px", fontWeight: 900 }}>
222-
{permissionLabels.owner}
223-
</span>
224-
) : (
225-
<div className="flex items-center gap-1.5">
226-
<select
227-
value={member.role}
228-
onChange={(event) => handleRoleChange(member.id, event.target.value as TeamMember["role"])}
229-
className="rounded-lg border-0 px-2 py-1 outline-none"
230-
style={{
231-
background: "#ffffff",
232-
color: "#374151",
233-
fontSize: "13px",
234-
fontWeight: 900,
235-
cursor: "pointer"
236-
}}
237-
aria-label={`${member.name} 권한`}
238-
>
239-
<option value="editor">{permissionLabels.editor}</option>
240-
<option value="viewer">{permissionLabels.viewer}</option>
241-
</select>
242-
<button
243-
type="button"
244-
onClick={() => handleRemoveClick(member)}
245-
className="flex h-8 w-8 items-center justify-center rounded-lg border-0"
246-
style={{
247-
background: "rgba(239, 68, 68, 0.08)",
248-
color: "#ef4444",
249-
cursor: "pointer"
250-
}}
251-
aria-label={`${member.name} 삭제`}
252-
title={`${member.name} 삭제`}
253-
>
254-
<Trash2 size={15} />
255-
</button>
240+
{sortedMembers.map((member) => {
241+
const isMe = member.id === CURRENT_USER_ID;
242+
return (
243+
<div
244+
key={member.id}
245+
className="flex items-center gap-3 rounded-xl px-1 py-2"
246+
style={{}}
247+
>
248+
{/* Avatar */}
249+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full" style={{
250+
background: isMe ? "#ff5b8a" : member.isGithub ? "#24292e" : "linear-gradient(135deg, #20e3ff, #7c3aed)",
251+
color: "#ffffff",
252+
fontSize: "12px",
253+
fontWeight: 950
254+
}}>
255+
{member.isGithub ? <Github size={14} /> : member.avatar}
256256
</div>
257-
)}
258-
</div>
259-
))}
257+
258+
{/* Name */}
259+
<span className="min-w-0 flex-1 truncate tracking-tight" style={{
260+
color: "#374151",
261+
fontSize: "14px",
262+
fontWeight: 900
263+
}}>
264+
{member.name}
265+
{isMe && (
266+
<span style={{ color: "#374151", fontWeight: 700, fontSize: "13px", marginLeft: "6px" }}>
267+
(나)
268+
</span>
269+
)}
270+
</span>
271+
272+
{member.warning && !isMe && (
273+
<AlertCircle size={18} style={{ color: "#f59e0b" }} />
274+
)}
275+
276+
{/* Role / controls */}
277+
{isMe ? (
278+
<span className="tracking-tight" style={{ color: "#374151", fontSize: "13px", fontWeight: 900 }}>
279+
{permissionLabels[member.role]}
280+
</span>
281+
) : member.role === "owner" ? (
282+
<span className="tracking-tight" style={{ color: "#374151", fontSize: "13px", fontWeight: 900 }}>
283+
{permissionLabels.owner}
284+
</span>
285+
) : (
286+
<div className="flex items-center gap-1.5">
287+
<select
288+
value={member.role}
289+
onChange={(event) => handleRoleChange(member.id, event.target.value as TeamMember["role"])}
290+
className="rounded-lg border-0 px-2 py-1 outline-none"
291+
style={{
292+
background: "#ffffff",
293+
color: "#374151",
294+
fontSize: "13px",
295+
fontWeight: 900,
296+
cursor: "pointer"
297+
}}
298+
aria-label={`${member.name} 권한`}
299+
>
300+
<option value="editor">{permissionLabels.editor}</option>
301+
<option value="viewer">{permissionLabels.viewer}</option>
302+
</select>
303+
<button
304+
type="button"
305+
onClick={() => handleRemoveClick(member)}
306+
className="flex h-8 w-8 items-center justify-center rounded-lg border-0"
307+
style={{
308+
background: "rgba(239, 68, 68, 0.08)",
309+
color: "#ef4444",
310+
cursor: "pointer"
311+
}}
312+
aria-label={`${member.name} 삭제`}
313+
title={`${member.name} 삭제`}
314+
>
315+
<Trash2 size={15} />
316+
</button>
317+
</div>
318+
)}
319+
</div>
320+
);
321+
})}
260322
</div>
261323
</div>
262324
</div>

0 commit comments

Comments
 (0)