// PhotoUpload — 共通の写真アップロードコンポーネント // 使い方: // ...} // title="顔がはっきり写った写真を1枚" // hint="笑顔の写真がおすすめ。サングラスは不可です。" // accept="image/*" /> // // File オブジェクトを保持し、変更時に親に通知。 // 送信時は palpetEncodeFile(file) で Base64 にして webhook へ POST。 const { useRef, useState, useEffect } = React; function PhotoUpload({ kind='card', value, onChange, title, hint, accept='image/*', sampleTone='warm' }) { const inputRef = useRef(null); const [previewURL, setPreviewURL] = useState(null); // value が変わったら preview URL を更新(メモリリーク対策つき) useEffect(() => { if (value instanceof File) { const url = URL.createObjectURL(value); setPreviewURL(url); return () => URL.revokeObjectURL(url); } else if (typeof value === 'string' && value) { // すでに data URL or http URL setPreviewURL(value); } else { setPreviewURL(null); } }, [value]); const pick = () => inputRef.current?.click(); const onFile = (e) => { const f = e.target.files?.[0]; if (f) onChange(f); // 同じファイルを連続で選べるようにリセット e.target.value = ''; }; const remove = () => onChange(null); const photoTones = { warm: ['#F4DBA8','#E8B97A'], peach: ['#F8CFB8','#EBA689'], sage: ['#CBE0CC','#9CC2A4'], cream: ['#F5E7CC','#E0CFA8'], }; const [a,b] = photoTones[sampleTone] || photoTones.warm; const placeholderBG = `repeating-linear-gradient(135deg, ${a} 0 12px, ${b} 12px 24px)`; const PreviewBox = ({ size, round }) => (
{previewURL ? ( preview ) : ( 📷 photo )}
); // 円形(プロフィール写真) if (kind === 'circle') { return (
{previewURL && value instanceof File ? ( <>
写真を選びました
{value.name} ・ {fmtSize(value.size)}
) : ( <>
{title}
{hint}
)}
{previewURL && ( )}
); } // 正方形(ペット写真などのカード) if (kind === 'square') { return (
{previewURL && value instanceof File ? ( <>
写真を選びました
{value.name} ・ {fmtSize(value.size)}
) : ( <>
{title}
{hint}
)}
); } // カードタイプ(身分証など)— アイコン+説明+「始める」/プレビュー切替 const cardActive = !!previewURL; return (
{cardActive ? ( ) : (
📷
)}
{title}
{cardActive && value instanceof File ? (
✓ {value.name} ・ {fmtSize(value.size)}
) : (
{hint}
)}
{cardActive && ( )}
); } const btnPrimary = { background:'var(--c-primary-soft)',color:'var(--c-primary-deep)',border:'none', padding:'9px 14px',borderRadius:10,fontWeight:700,cursor:'pointer', fontFamily:'inherit',fontSize:13,whiteSpace:'nowrap', }; const btnGhost = { background:'transparent',color:'#9c8a72',border:'1px solid rgba(0,0,0,.08)', padding:'7px 12px',borderRadius:10,fontWeight:600,cursor:'pointer', fontFamily:'inherit',fontSize:12,whiteSpace:'nowrap', }; function fmtSize(bytes) { if (!bytes && bytes !== 0) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB'; return (bytes/(1024*1024)).toFixed(1) + ' MB'; } // File を base64 にして webhook へ POST するためのヘルパ // 使い方: const payload = await palpetEncodeFile(file) // 返り値: { name, type, size, base64 }(PNG/JPEG向け、巨大ファイルでも問題なく扱えるが2MB超は webhook 側で拒否することを想定) window.palpetEncodeFile = function (file) { if (!(file instanceof File)) return Promise.resolve(null); return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => { const result = String(r.result || ''); // data:image/jpeg;base64,XXXX... の base64 部分だけ取り出す const base64 = result.split(',')[1] || ''; resolve({ name: file.name, type: file.type, size: file.size, base64 }); }; r.onerror = reject; r.readAsDataURL(file); }); }; window.PhotoUpload = PhotoUpload;