Claude Code · 모름 · React (TSX)
결과 코드펼치기
import { useEffect, useRef, useState } from "react";
const FONT_URL =
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Instrument+Sans:wght@600;700&display=swap";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function InviteMemberModal() {
const [open, setOpen] = useState(true);
const [emails, setEmails] = useState(["mina@example.com", "joon@example.com", "hana@example.com"]);
const [draft, setDraft] = useState("");
const [error, setError] = useState("");
const [role, setRole] = useState("Member");
const [sent, setSent] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
inputRef.current?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
const close = () => {
setOpen(false);
requestAnimationFrame(() => triggerRef.current?.focus());
};
const add = () => {
const value = draft.trim().toLowerCase();
if (!value) return;
if (!EMAIL_RE.test(value)) {
setError("올바른 이메일 주소를 입력해 주세요.");
return;
}
if (emails.includes(value)) {
setError("이미 추가한 이메일입니다.");
return;
}
setEmails([...emails, value]);
setDraft("");
setError("");
setSent(false);
};
return (
<section className="relative flex min-h-[640px] w-full items-center justify-center overflow-hidden bg-[#f8fafc] px-4 py-16 font-['Inter',sans-serif]">
<link rel="stylesheet" href={FONT_URL} precedence="default" />
<div className="flex flex-col items-center gap-3 text-center">
<p className="text-[14px] text-[#475569]">{sent ? `${emails.length}명에게 초대장을 보냈습니다.` : "팀원을 워크스페이스에 초대하세요."}</p>
<button
ref={triggerRef}
type="button"
onClick={() => setOpen(true)}
className="rounded-lg bg-[#4f46e5] px-4 py-2.5 text-[14px] font-semibold text-white transition-colors hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
Invite Member
</button>
</div>
{open && (
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40" onClick={close} aria-hidden="true" />
<div
role="dialog"
aria-modal="true"
aria-labelledby="invite-title"
aria-describedby="invite-desc"
className="relative flex w-full max-w-[520px] flex-col gap-6 rounded-[20px] bg-white p-6 drop-shadow-[0px_20px_20px_rgba(0,0,0,0.2)] sm:p-8"
>
<div className="flex items-center justify-between">
<h2 id="invite-title" className="font-['Instrument_Sans',sans-serif] text-[22px] font-bold leading-normal text-[#0f172a]">
Invite Member
</h2>
<button
type="button"
onClick={close}
aria-label="닫기"
className="flex size-8 items-center justify-center rounded-2xl bg-[#f1f5f9] text-[#475569] transition-colors hover:bg-[#e2e8f0] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6M9 9l6 6" />
</svg>
</button>
</div>
<div className="flex flex-col gap-5">
<p id="invite-desc" className="text-[14px] leading-[1.5] text-[#475569]">
Invite team members to your workspace by entering their email addresses. You can invite multiple people at once.
</p>
<div className="flex flex-col gap-3">
<div className="flex items-end gap-2">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<label htmlFor="invite-email" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Email Address
</label>
<input
ref={inputRef}
id="invite-email"
type="email"
value={draft}
onChange={(e) => {
setDraft(e.target.value);
setError("");
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
e.preventDefault();
add();
}
}}
placeholder="Enter email address"
aria-invalid={error ? true : undefined}
aria-describedby={error ? "invite-error" : undefined}
className={`h-11 w-full rounded-lg border bg-white px-3 text-[14px] text-[#0f172a] outline-none transition-colors placeholder:text-[#94a3b8] focus:ring-2 ${
error ? "border-[#ef4444] focus:ring-[#ef4444]/15" : "border-[#e2e8f0] focus:border-[#4f46e5] focus:ring-[#4f46e5]/15"
}`}
/>
</div>
<button
type="button"
onClick={add}
className="h-11 shrink-0 rounded-lg border border-[#e2e8f0] bg-white px-4 text-[14px] font-semibold text-[#0f172a] transition-colors hover:bg-[#f8fafc] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
Add
</button>
</div>
{error && (
<p id="invite-error" role="alert" className="text-[12px] text-[#ef4444]">
{error}
</p>
)}
{emails.length > 0 && (
<ul className="flex flex-wrap gap-2" aria-label="초대할 이메일">
{emails.map((email) => (
<li key={email} className="flex items-center gap-1.5 rounded-full border border-[#c7d2fe] bg-[#eef2ff] py-1.5 pl-2.5 pr-1.5">
<span className="text-[13px] font-medium leading-normal text-[#4f46e5]">{email}</span>
<button
type="button"
onClick={() => setEmails(emails.filter((x) => x !== email))}
aria-label={`${email} 삭제`}
className="flex size-4 items-center justify-center rounded-lg text-[#4f46e5] hover:opacity-70 focus-visible:outline-2 focus-visible:outline-[#4f46e5]"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6M9 9l6 6" />
</svg>
</button>
</li>
))}
</ul>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="invite-role" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Assigned Role
</label>
<div className="relative">
<select
id="invite-role"
value={role}
onChange={(e) => setRole(e.target.value)}
className="h-11 w-full cursor-pointer appearance-none rounded-lg border border-[#e2e8f0] bg-white px-3 pr-9 text-[14px] text-[#0f172a] outline-none focus:border-[#4f46e5] focus:ring-2 focus:ring-[#4f46e5]/15"
>
<option>Member</option>
<option>Editor</option>
<option>Admin</option>
</select>
<svg className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[#0f172a]" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6" />
</svg>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={close}
className="rounded-lg border border-[#e2e8f0] bg-white px-4 py-2.5 text-[14px] font-semibold text-[#0f172a] transition-colors hover:bg-[#f8fafc] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
Cancel
</button>
<button
type="button"
disabled={emails.length === 0}
onClick={() => {
setSent(true);
close();
}}
className="rounded-lg bg-[#4f46e5] px-4 py-2.5 text-[14px] font-semibold text-white transition-colors hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] disabled:cursor-not-allowed disabled:opacity-50"
>
Send Invitations
</button>
</div>
</div>
</div>
)}
</section>
);
}
적용 사이트 0
이 블록이 실제로 쓰인 사이트입니다.
아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.
재현 후기
v1 · 0개아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.