/*__IIFE_WRAPPED__*/ ;(function(){ // screens-auth.jsx — Splash / Auth(メール認証コード) / Onboarding(名前) / Pairing const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React; const { useApp, Icon, Btn, Charm, Field, Amulet } = window; const ME_FALLBACK = 'あなた'; /* ambient soft blobs (mood ③) */ function Blobs({ opacity = 1 }) { return (
); } function Wordmark({ size = 56 }) { return (
amulet
アミュレット
); } function Eyebrowy({ children }) { return
{children}
; } /* ════════ SPLASH ════════ */ const SPLASH_LINES = ['ようこそ!', 'はじめよう🐾', '会えてうれしい', '本音、聞かせてね']; function SplashScreen() { const { go, live } = useApp(); const [lineIdx, setLineIdx] = useStateA(0); useEffectA(() => { const id = setInterval(() => setLineIdx(i => (i + 1) % SPLASH_LINES.length), 2600); return () => clearInterval(id); }, []); return (

言えない本音を、
AIが最高の思い出に変える

{/* 犬(プラン生成ローディングと同じLottieBox)が吹き出しでお出迎え。 犬は右端に固定し、メッセージ長が変わっても位置が動かないよう吹き出しは左へ伸ばす */}
{/* 吹き出し: 右下の犬を指す。right固定でテキストは左へ伸びる */}
{SPLASH_LINES[lineIdx]} {/* 犬(右)を指す吹き出しのしっぽ */}
{/* 犬: 右端固定 */}
{ if (live.user) { go(live.userDoc?.coupleId ? 'home' : 'pairing', {}, { replace: true }); } else go('auth', {}, { replace: true }); }}>はじめる

メールアドレスだけで始められます(パスワード不要)

); } /* ════════ AUTH — メール + 6桁認証コード ════════ */ /* AuthScreen の外で定義(内側で定義すると再レンダーごとに input がリマウントされる) */ function AuthShell({ children, title, sub, overlay }) { return (

{title}

{sub}

{children}
{overlay}
); } /* 審査員用テストログインの案内ダイアログ(ボトムシート) */ function JudgeLoginDialog({ open, busy, onClose, onConfirm }) { if (!open) return null; return (
e.stopPropagation()} style={{ width: '100%', background: 'var(--paper)', borderRadius: '26px 26px 0 0', padding: '14px 22px 30px', boxShadow: '0 -10px 40px rgba(0,0,0,.18)', animation: 'floatup .3s ease', maxHeight: '82%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
テストユーザーで体験する
審査員のみなさま向けの入り口です

このボタンを押すと、共有のテストユーザー(test@example.com)としてログインした状態になります。メール認証は不要で、すぐに機能をお試しいただけます。

⚠️ このテストユーザーは他の審査員の方と共有されており、誰でも内容を自由に編集できます。表示が乱れていた場合は申し訳ございません。

まっさらな状態でお試しになりたい場合は、この画面を閉じてご自身のメールアドレスでログインいただくと、完全に新規のユーザーとして開始できます。そちらもぜひお試しください。

{busy ? 'ログイン中…' : 'テストユーザーでログイン'} 閉じる
); } function AuthScreen() { const { go, toast } = useApp(); // stage: 'email' メアド入力 / 'code' 6桁コード入力 const [stage, setStage] = useStateA('email'); const [email, setEmail] = useStateA(''); const [code, setCode] = useStateA(''); const [busy, setBusy] = useStateA(false); const [aboutOpen, setAboutOpen] = useStateA(false); // 「このアプリについて」ダイアログ const [judgeOpen, setJudgeOpen] = useStateA(false); // 審査員用テストログインの案内ダイアログ // 審査員用: test@example.com(サーバー側でコード000000固定・メール送信なし)で即ログイン const testLogin = async () => { setBusy(true); try { await Amulet.requestEmailCode('test@example.com'); // メールは飛ばない await Amulet.verifyEmailCode('test@example.com', '000000'); go('home', {}, { replace: true }); } catch (e) { console.error(e); toast(Amulet.friendlyError(e)); } finally { setBusy(false); setJudgeOpen(false); } }; const send = async () => { if (!/^\S+@\S+\.\S+$/.test(email)) { toast('メールアドレスの形式を確認してください'); return; } setBusy(true); try { await Amulet.requestEmailCode(email.trim().toLowerCase()); setCode(''); setStage('code'); } catch (e) { console.error(e); toast(Amulet.friendlyError(e)); } finally { setBusy(false); } }; const verify = async () => { const c = code.replace(/\D/g, ''); if (c.length !== 6) { toast('6桁のコードを入力してください'); return; } setBusy(true); try { await Amulet.verifyEmailCode(email.trim().toLowerCase(), c); // サインイン成功 → app.jsx の自動ルーティングに任せず明示遷移。 // サインイン画面には戻せないので履歴を置き換える go('onboarding', {}, { replace: true }); } catch (e) { console.error(e); toast(Amulet.friendlyError(e)); } finally { setBusy(false); } }; if (stage === 'code') { return (
setCode(v.replace(/\D/g, '').slice(0, 6))} />
{busy ? '確認中…' : 'サインイン'}
📩 迷惑メール(スパム)フォルダに届くことがよくあります。 受信トレイに見当たらないときは、必ず迷惑メールフォルダもご確認ください。コードの有効期限は10分です。
); } return ( setAboutOpen(false)} /> setJudgeOpen(false)} onConfirm={testLogin} /> }>
{busy ? '送信中…' : 'コードを送る'}
初めての方も、2回目以降の方も同じ手順です。アカウントは自動でつくられ、パスワードを保存することはありません。

Amulet はハッカソンのプロトタイプです。続けることで に同意したものとみなされます(タップで内容を表示)

{/* ── 審査員用入り口 ── */}
FOR JUDGES ・ 審査員のみなさまへ

ログインの手間なくすぐお試しいただけます。
下のボタンで共有のテストユーザーとして体験できます。

setJudgeOpen(true)}> テストユーザーでログイン
); } /* ════════ ONBOARDING — 名前の登録 ════════ */ function OnboardingScreen() { const { go, back, live, toast } = useApp(); const [name, setName] = useStateA(live.userDoc?.name || ''); const [busy, setBusy] = useStateA(false); const save = async () => { const v = name.trim(); if (!v) { toast('名前を入力してください'); return; } setBusy(true); try { await Amulet.saveName(v.slice(0, 20)); // ペア成立済み=設定からの名前編集。呼び出し元(設定)へ戻す。 // 初回登録なら次はペアリングで、この画面には戻せない。 if (live.userDoc?.coupleId) back(); else go('pairing', {}, { replace: true }); } catch (e) { console.error(e); toast(Amulet.friendlyError(e)); } finally { setBusy(false); } }; return (
STEP — PROFILE

なんて呼べばいい?

この名前はパートナーにも表示されます。
ニックネームでも大丈夫。

{busy ? '保存中…' : 'これでOK'}
); } /* ════════ PAIRING ════════ */ function PairingScreen() { const { go, back, live, toast } = useApp(); const [linked, setLinked] = useStateA(false); const [partnerName, setPartnerName] = useStateA(''); const [copied, setCopied] = useStateA(false); const [code, setCode] = useStateA(''); const [myCode, setMyCode] = useStateA(null); const [busy, setBusy] = useStateA(false); const wasUnpaired = useRefA(!live.userDoc?.coupleId); // 自分の招待コードを発行 useEffectA(() => { if (!live.user) return; let dead = false; Amulet.createInvite() .then(r => { if (!dead) setMyCode(r.code); }) .catch(e => { // すでにペア済みなどは静かにスキップ console.warn('createInvite:', e.message); }); return () => { dead = true; }; }, [live.user]); // パートナーが自分のコードを使った → userDoc.coupleId が立つ(双方向反映) useEffectA(() => { if (wasUnpaired.current && live.userDoc?.coupleId && !linked) { Amulet.fetchPartner(live.userDoc.coupleId) .then(p => setPartnerName(p?.name || '')) .finally(() => setLinked(true)); } }, [live.userDoc?.coupleId]); useEffectA(() => { if (!linked) return; // ペア成立後にペアリング画面へ戻れても意味がないので履歴を置き換える const t = setTimeout(() => go('home', {}, { replace: true }), 2300); return () => clearTimeout(t); }, [linked]); const redeem = async () => { const v = code.trim().toUpperCase(); if (v.length < 4) { toast('招待コードを入力してください'); return; } setBusy(true); try { const r = await Amulet.redeemInvite(v); setPartnerName(r.partnerName || ''); setLinked(true); } catch (e) { console.error(e); toast(Amulet.friendlyError(e)); } finally { setBusy(false); } }; const copy = () => { if (!myCode) return; navigator.clipboard?.writeText(myCode).catch(() => {}); setCopied(true); setTimeout(() => setCopied(false), 1500); }; const share = () => { const text = `Amulet で一緒にデートを計画しよう。招待コード: ${myCode}`; if (navigator.share) navigator.share({ text }).catch(() => {}); else { navigator.clipboard?.writeText(text).catch(() => {}); toast('招待文をコピーしました'); } }; if (linked) { const myName = live.userDoc?.name || ME_FALLBACK; return (
ふたりが、つながりました
{myName} & {partnerName || 'パートナー'}
); } const codeChars = (myCode || '······').split(''); return (
STEP — PAIRING

パートナーと、お守りを結ぶ

どちらかの招待コードを相手が入力すると、ふたりのAmuletがひとつになります。

{/* my code */}
あなたの招待コード{!myCode && '(発行中…)'}
{codeChars.map((c, i) => (
{c}
))}
{copied ? 'コピーしました' : 'コピー'} シェア
相手がコードを入力すると、この画面が自動で切り替わります
— または、相手のコードを入力 —
{busy ? '確認中…' : 'ペアリングする'}
); } Object.assign(window, { SplashScreen, AuthScreen, OnboardingScreen, PairingScreen, Blobs, Wordmark }); })();