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&display=swap";
const INITIAL = {
name: "Jordan Rivera",
email: "jordan.rivera@example.com",
role: "Member",
department: "Product Design",
position: "Senior Designer",
};
const fieldClass =
"h-11 w-full rounded-lg border border-[#e2e8f0] bg-white px-3 text-[14px] leading-normal text-[#0f172a] outline-none transition-colors placeholder:text-[#94a3b8] focus:border-[#4f46e5] focus:ring-2 focus:ring-[#4f46e5]/15";
export default function ProfileSettingsForm() {
const [form, setForm] = useState(INITIAL);
const [avatar, setAvatar] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState("");
const [saved, setSaved] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
useEffect(() => {
return () => {
if (avatar) URL.revokeObjectURL(avatar);
};
}, [avatar]);
const update = (key: keyof typeof INITIAL) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setSaved(false);
setForm({ ...form, [key]: e.target.value });
};
const onFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!["image/jpeg", "image/png"].includes(file.type)) {
setAvatarError("JPG 또는 PNG 파일만 올릴 수 있습니다.");
return;
}
if (file.size > 2 * 1024 * 1024) {
setAvatarError("2MB 이하 파일만 올릴 수 있습니다.");
return;
}
setAvatarError("");
setSaved(false);
setAvatar(URL.createObjectURL(file));
};
const initials = form.name
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0]?.toUpperCase())
.join("");
const dirty = JSON.stringify(form) !== JSON.stringify(INITIAL) || avatar !== null;
return (
<section className="w-full bg-[#f5f5f5] px-4 py-10 font-['Inter',sans-serif] sm:px-6 sm:py-16">
<link rel="stylesheet" href={FONT_URL} precedence="default" />
<form
onSubmit={(e) => {
e.preventDefault();
if (!form.name.trim()) return;
setSaved(true);
}}
className="mx-auto flex w-full max-w-[640px] flex-col gap-8 rounded-2xl bg-white p-6 drop-shadow-[0px_4px_10px_rgba(0,0,0,0.03)] sm:p-10"
>
<div className="flex flex-wrap items-center gap-6">
<div className="flex size-20 shrink-0 items-center justify-center overflow-hidden rounded-[40px] bg-gradient-to-br from-[#c7d2fe] to-[#4f46e5] text-[24px] font-semibold text-white">
{avatar ? <img src={avatar} alt="프로필 사진 미리보기" className="size-full object-cover" /> : <span aria-hidden="true">{initials}</span>}
</div>
<div className="flex flex-col gap-1">
<button
type="button"
onClick={() => fileRef.current?.click()}
className="flex items-center justify-center gap-2 self-start rounded-lg border border-[#e2e8f0] bg-white px-4 py-2.5 text-[14px] font-semibold leading-normal text-[#0f172a] transition-colors hover:bg-[#f8fafc] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
Change Avatar
</button>
<input ref={fileRef} type="file" accept="image/jpeg,image/png" onChange={onFile} className="sr-only" aria-label="프로필 사진 선택" tabIndex={-1} />
<p className={`text-[12px] leading-normal ${avatarError ? "text-[#dc2626]" : "text-[#94a3b8]"}`} role={avatarError ? "alert" : undefined}>
{avatarError || "JPG or PNG. Max size 2MB."}
</p>
</div>
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<label htmlFor="pf-name" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Name
</label>
<input id="pf-name" required value={form.name} onChange={update("name")} className={fieldClass} />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="pf-email" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Email
</label>
<input
id="pf-email"
value={form.email}
readOnly
aria-describedby="pf-email-hint"
className="h-11 w-full cursor-not-allowed rounded-lg border border-[#e2e8f0] bg-[#f1f5f9] px-3 text-[14px] leading-normal text-[#94a3b8] outline-none"
/>
<p id="pf-email-hint" className="text-[12px] leading-normal text-[#94a3b8]">
Email address cannot be changed for security reasons.
</p>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="pf-role" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Role
</label>
<div className="relative">
<select id="pf-role" value={form.role} onChange={update("role")} className={`${fieldClass} cursor-pointer appearance-none pr-9`}>
<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 className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<label htmlFor="pf-dept" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Department
</label>
<input id="pf-dept" value={form.department} onChange={update("department")} className={fieldClass} />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="pf-pos" className="text-[14px] font-semibold leading-normal text-[#0f172a]">
Position
</label>
<input id="pf-pos" value={form.position} onChange={update("position")} className={fieldClass} />
</div>
</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
<p className="mr-auto text-[13px] text-[#16a34a]" aria-live="polite">
{saved ? "변경 사항을 저장했습니다." : ""}
</p>
<button
type="button"
onClick={() => {
setForm(INITIAL);
setAvatar(null);
setAvatarError("");
setSaved(false);
}}
disabled={!dirty}
className="rounded-lg border border-[#e2e8f0] bg-white px-4 py-2.5 text-[14px] font-semibold leading-normal text-[#0f172a] transition-colors hover:bg-[#f8fafc] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5] disabled:cursor-not-allowed disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
className="rounded-lg bg-[#4f46e5] px-4 py-2.5 text-[14px] font-semibold leading-normal text-white transition-colors hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]"
>
Save Changes
</button>
</div>
</form>
</section>
);
}
적용 사이트 0
이 블록이 실제로 쓰인 사이트입니다.
아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.
재현 후기
v1 · 0개아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.