// My Gifts — collection of owned NFT gifts + action sheet (Send / Withdraw / Sell). function MyGiftActions({ gift, onClose, onWithdraw, onView, onSend }) { if (!gift) return null; const target = typeof document !== 'undefined' && document.querySelector('.app-view') || typeof document !== 'undefined' && document.body; if (!target) return null; return ReactDOM.createPortal(
#{gift.num}
{gift.name}
Куплен за
{(() => { // Реальная дата покупки из заказа; фоллбэк — сегодня. const d = gift.created_at ? new Date(gift.created_at) : new Date(); return isNaN(d) ? '—' : d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }); })()}
{fmtRub(gift.rub)}
Забрать в Telegram → подарок появится на твоём профиле, его смогут увидеть друзья
, target ); } function WithdrawConfirm({ gift, onClose, onDone }) { const [stage, setStage] = React.useState('confirm'); const [err, setErr] = React.useState(null); const [username, setUsername] = React.useState(''); const uname = username.trim().replace(/^@/, ''); const unameOk = /^[A-Za-z0-9_]{5,32}$/.test(uname); React.useEffect(() => { if (stage !== 'progress') return; let cancel = false; (async () => { try { // РЕАЛЬНЫЙ вывод: POST /me/gifts/{order_id}/withdraw {recipient} — // маркет выводит NFT на основной аккаунт, оттуда за ⭐ отправляет @username. const DS = window.DataSource; if (DS && DS.withdrawGift) { await DS.withdrawGift(gift.order_id || gift.id, uname); } else if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) { throw new Error('DataSource unavailable'); // прод в Telegram: НЕ притворяемся успехом } else { await new Promise(r => setTimeout(r, 1500)); // дизайн-симулятор в браузере } if (!cancel) setStage('done'); } catch (e) { if (!cancel) { setErr(window.friendlyError ? window.friendlyError(e, 'withdraw') : 'Не удалось запустить вывод — попробуй через минуту'); setStage('confirm'); } } })(); return () => { cancel = true; }; }, [stage]); if (!gift) return null; const target = typeof document !== 'undefined' && document.querySelector('.app-view') || typeof document !== 'undefined' && document.body; if (!target) return null; return ReactDOM.createPortal( {stage === 'confirm' &&
Забрать {gift.name}?
Укажи Telegram-получателя — подарок появится на его профиле в течение пары минут. Можно указать свой @username.
@ 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 }}/>
{err && (
{err}
)} {close => ( )}
} {stage === 'progress' &&
Передаём в Telegram…
Подключаемся к API Portals
} {stage === 'done' &&
Готово! 🎉
{gift.name}{gift.num != null ? ` #${gift.num}` : ''} уже летит к{' '} @{uname} — придёт в течение пары минут
{close => ( )}
} , target ); } function MyGiftsScreen({ onStartSend, onGiftClick, gifts, onChanged }) { const held = gifts || []; // реальные held-подарки из /me/gifts (проброс из app.jsx) const [selected, setSelected] = React.useState(null); const [withdrawing, setWithdrawing] = React.useState(null); const [myFilter, setMyFilter] = React.useState('vault'); const [page, setPage] = React.useState(1); // «Выведенные» — реальный /me/gifts?released=1; рефетч, когда меняется held // (после вывода/отправки подарок переезжает из хранилища в выведенные). const _rel = (window.DataSource && window.DataSource.useReleasedGifts) ? window.DataSource.useReleasedGifts('rel-' + held.length) : { data: null }; const releasedItems = (_rel.data && _rel.data.items) || []; const items = myFilter === 'withdrawn' ? releasedItems : held; React.useEffect(() => { setPage(1); }, [myFilter]); const PAGE_SIZE = 6; const totalPages = Math.max(1, Math.ceil(items.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const pageItems = items.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); const totalRub = items.reduce((s, i) => s + i.rub, 0); const totalTon = items.reduce((s, i) => s + i.ton, 0); return ( <>
Коллекция
{items.length}
подарков
Стоимость
{fmtRub(totalRub)}
{[ ['vault', 'В хранилище'], ['withdrawn', 'Выведенные']]. map(([k, l]) => { const on = (myFilter || 'vault') === k; return ( ); })}
{pageItems.map((g) => { return (
{ if (myFilter === 'vault') setSelected(g); }} style={{ display: 'flex', flexDirection: 'column', gap: 8, cursor: myFilter === 'vault' ? 'pointer' : 'default', opacity: myFilter === 'withdrawn' ? 0.75 : 1 }}>
#{g.num}
{g.name}
{fmtRub(g.rub)}
); })}
{totalPages > 1 &&
{safePage} / {totalPages}
}
Всего {items.length} {items.length === 1 ? 'подарок' : items.length < 5 ? 'подарка' : 'подарков'}
{selected && !withdrawing && setSelected(null)} onWithdraw={(g) => {setWithdrawing(g);}} onSell={() => setSelected(null)} onView={(g) => { setSelected(null); onGiftClick && onGiftClick(g); }} onSend={(g) => { setSelected(null); onStartSend && onStartSend(g.id); }} /> } {withdrawing && {setWithdrawing(null);setSelected(null);}} onDone={() => {setWithdrawing(null);setSelected(null); onChanged && onChanged();}} /> } ); } window.MyGiftsScreen = MyGiftsScreen;