// Send-gift flow — two-step: Select gifts → Recipient → Toast.
function SendSelectSheet({ initialIds, gifts, onCancel, onNext }) {
const all = gifts || []; // реальные held-подарки (проброс из app.jsx)
const [selected, setSelected] = React.useState(() => new Set(initialIds || []));
const [query, setQuery] = React.useState('');
const closeAnim = React.useContext(SheetCloseContext);
// Snapshot the initially-selected gifts so the ordering stays stable while
// the user toggles inside the picker — they sort to the top once, on open,
// not jumping around with every tap.
const initialOrder = React.useRef(new Set(initialIds || []));
const items = React.useMemo(() => {
const q = query.trim().toLowerCase();
const list = all.filter(g => !q || g.name.toLowerCase().includes(q));
return list.slice().sort((a, b) => {
const sa = initialOrder.current.has(a.id) ? 0 : 1;
const sb = initialOrder.current.has(b.id) ? 0 : 1;
return sa - sb;
});
}, [query, all]);
const allSelected = items.length > 0 && items.every(g => selected.has(g.id));
const count = selected.size;
const toggle = (id) => setSelected(prev => {
const s = new Set(prev);
s.has(id) ? s.delete(id) : s.add(id);
return s;
});
const toggleAll = () => setSelected(prev => {
if (allSelected) {
const s = new Set(prev);
items.forEach(g => s.delete(g.id));
return s;
}
const s = new Set(prev);
items.forEach(g => s.add(g.id));
return s;
});
return (
<>
setQuery(e.target.value)}
style={{ flex: 1, background: 'transparent', border: 0, outline: 0, color: 'var(--text)', fontSize: 14 }}/>
{items.map(g => {
const on = selected.has(g.id);
return (
toggle(g.id)} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: 10, borderRadius: 16,
background: 'var(--bg-input)',
border: on ? '0.5px solid var(--accent)' : '0.5px solid var(--border)',
boxShadow: on ? '0 0 0 1px var(--accent), 0 4px 14px var(--accent-glow)' : 'none',
transition: 'border-color 0.15s, box-shadow 0.15s',
cursor: 'pointer',
}}>
{fmtRub(g.rub)}
);
})}
>
);
}
function SendRecipientSheet({ gifts: initialGifts, onBack, onSend, onAddMore }) {
const [gifts, setGifts] = React.useState(initialGifts);
const [username, setUsername] = React.useState('');
const [anon, setAnon] = React.useState(false);
const [sending, setSending] = React.useState(false);
const [sendErr, setSendErr] = React.useState(null);
const closeAnim = React.useContext(SheetCloseContext);
const total = gifts.reduce((s, g) => s + g.rub, 0);
// РЕАЛЬНАЯ отправка: POST /me/gifts/{order_id}/send по одному на подарок.
const doSend = async () => {
const recip = username.trim();
if (!recip || gifts.length === 0 || sending) return;
setSending(true); setSendErr(null);
try {
const DS = window.DataSource;
if (DS && DS.sendGift) {
for (const g of gifts) {
await DS.sendGift(g.order_id || g.id, recip, anon); // anonymous реально уходит на бэк
}
} else if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) {
throw new Error('DataSource unavailable'); // прод в Telegram: НЕ притворяемся успехом
} else {
await new Promise(r => setTimeout(r, 600)); // дизайн-симулятор в браузере
}
onSend({ username: recip, anon, count: gifts.length });
} catch (e) {
setSendErr(window.friendlyError ? window.friendlyError(e, 'send') : 'Не удалось отправить — попробуй через минуту');
setSending(false);
}
};
const removeGift = (id) => setGifts(prev => prev.filter(g => g.id !== id));
// Hand the current id set up to the parent on navigation so the picker
// shows what's still in the recipient bundle (and removals stick).
const currentIds = gifts.map(g => g.id);
return (
<>
Получатель
Введи только логин Telegram
Выбраны подарки
{fmtRub(total)} · {gifts.length} шт.
{gifts.map(g => (
removeGift(g.id)} style={{
position: 'absolute', top: -4, right: -4,
width: 22, height: 22, borderRadius: '50%',
background: 'rgba(0,0,0,0.6)', color: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
border: '1.5px solid var(--bg-app)',
}}>
#{g.num}
))}
onAddMore(currentIds)} style={{
width: 72, height: 72, borderRadius: 16,
background: 'var(--accent-soft)', color: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', flexShrink: 0,
}}>
@
setUsername(e.target.value.replace(/^@/, ''))}
style={{ flex: 1, background: 'transparent', border: 0, outline: 0, color: 'var(--text)', fontSize: 16, fontWeight: 600, lineHeight: 1, padding: 0, marginLeft: -2 }}/>
setAnon(v => !v)}>
Отправить анонимно
{sendErr && (
{sendErr}
)}
Внимательно проверь имя получателя
>
);
}
// Success screen shown after the gift is sent — the gift "flies off" into
// Telegram with expanding rings, then a confetti + check celebration. The
// sheet auto-dismisses (slides down) once the celebration has played.
function SendSuccessSheet({ gifts, recipient }) {
const closeAnim = React.useContext(SheetCloseContext);
const [done, setDone] = React.useState(false);
React.useEffect(() => {
const t1 = setTimeout(() => setDone(true), 1000);
const t2 = setTimeout(() => closeAnim(), 3000);
return () => { clearTimeout(t1); clearTimeout(t2); };
}, []);
const g = gifts[0] || {};
const n = gifts.length;
const name = recipient?.anon ? 'анонимно' : ('@' + (recipient?.username || 'recipient'));
const giftWord = n === 1 ? 'подарок' : n < 5 ? 'подарка' : 'подарков';
return (
{done &&
}
{!done && <>
>}
{!done ? (
) : (
)}
{done ? 'Отправлено! 🎉' : 'Отправляем…'}
{done ? (
<>
{n > 1 ? `${n} ${giftWord}` : g.name} {n > 1 ? 'улетели' : 'улетел'} к{' '}
{name}
>
) : (
'Подарок летит в Telegram'
)}
);
}
window.SendSelectSheet = SendSelectSheet;
window.SendSuccessSheet = SendSuccessSheet;
window.SendRecipientSheet = SendRecipientSheet;