BetnBanter
The social tracking and communication hub for tracking bets, competing with friends and settling up
Step 1 of 6
Create your account
or sign up with
Welcome back
Log in to your BetnBanter account
Step 2 of 6
Link your Venmo
Friends can tap your Venmo to settle up instantly after a bet.
@
💸
When you win a bet, your friends can tap your Venmo link directly from your profile to pay you instantly.
Step 3 of 6
What do you compete in?
Select all that apply — BetnBanter is built for these two.
🏈
Fantasy Football
Golf Matches
Step 4 of 6
Find your friends
Search by username or import contacts
People you may know
Step 5 of 6
Stay in the loop
Choose what you want to be notified about
🤝
Friend challenges
When someone sends you a bet
📰
Friend posts a bet
When someone in your circle posts
🔥
Reactions
When friends react to your slips
🏆
Weekly standings
Monday leaderboard recaps
💰
Balance reminders
Unsettled balance nudges
🎉 🏆 🎉
You're all set!
Your BetnBanter profile is ready.
Here's a summary of your setup.
Username
@alexjEdit
Competes in
Fantasy FootballEdit
Friends added
0Edit
Notifications
3 of 5 on

Post your first bet to get on the board

📋
Nothing here yet
Import your first bet or add friends to see their picks in your feed.
New bet
New matchup
Challenge a friend to a head-to-head bet.
🏈
Fantasy Football
Upload your weekly matchup screenshot — AI reads it and suggests fair odds.
Golf Match
Stroke play, match play, or scramble. 1v1, teams, or group — you set the format.
🤝
Custom Bet
Describe any bet in your own words — Nassau side bets, prop bets, bragging rights, anything.
Fantasy Football Bet
📷
Upload your matchup screenshot
AI will read your teams, scores & projections automatically
Your Team
Opponent's Team
Bet description
auto-built or edit freely
Golf Match Bet
Match description
Custom Bet
🤝
Custom Bet
Describe any bet — prop bets, bragging rights, anything.
Balances
No balances yet — create a head to head bet to get started.
No active bets yet.
Standings
Profile
@alexj
Member since 2026 · Fantasy Football & Golf
Stats
Record
0–0
Win Rate
Total Bets
0
Total Profit
$0
Hot Streak
Best Sport
Bet history
Notifications
🔔
No notifications yet
You'll see alerts here when friends post bets, challenge you, or react to your slips.
Groups
Your groups
Join via invite link
New group
Add friends first to invite them.
🏈
Sunday Sweats
5 members
📋
No bets yet
Group members' bets will appear here once they start posting.
Loading members...
New group bet
Each member puts in the same amount
Total pot (5 members)
$0.00
Friends
Requests
Your friends
No friends yet. Search by username or email above, or check the Suggested tab.
Friend
JR
Friend
@jakereynolds · Fantasy Football & Golf
Record
--
Win rate
--
Total Profit
--
ROI
--
Your record vs them
YOU
0
wins
VS
THEM
0
wins
No side bets yet
Recent bets
Loading...
Comments
Settings
Notifications
Push notifications
Bets, challenges, results
Challenge requests
Allow friends to challenge you
Auto-post bets to groups
Automatically share new bets in your groups
Privacy
Public profile
Anyone can see your bets
Show P&L publicly
Others can see your winnings
Account
Edit profile
Sports & format
Fantasy Football & Golf
Help & feedback
Legal notice
BetnBanter is a social tracking and communication app. It does not facilitate, process, hold, or transfer any wagers or funds. All bets are placed independently on third-party platforms. BetnBanter is intended for users 18 years of age or older.
Data
Reset stats
Never reset
Clears your record, win rate, P&L and bet history. This cannot be undone.
Bets vs Friend
YOU
0
THEM
0
NET
$0
No settled bets yet.
Group bet
Max $500 per person
Total pot
$0.00
Winner takes
$0.00
Post to my groups
Share this bet in all your groups' feeds
+ (b.stake||'0') + ''; if (done) { html += '
' + resEmoji + ' \u00b7 async function loadGroupMembers(groupId) { var container = document.getElementById('gscreen-members-list'); if (!container) return; if (!groupId || !window._sb) { container.innerHTML = '
No group selected.
'; return; } container.innerHTML = '
Loading members...
'; try { // Fetch all accepted members // Use view — avoids recursive RLS policy on group_members var mRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: groupId }); if (mRes.error) { console.error('loadGroupMembers RPC error:', mRes.error.message, mRes.error.code); container.innerHTML = '
Could not load members: ' + mRes.error.message + '
'; return; } if (!mRes.data || mRes.data.length === 0) { console.log('loadGroupMembers: no rows returned for group', groupId); container.innerHTML = '
No members yet.
'; return; } console.log('loadGroupMembers: got', mRes.data.length, 'rows:', mRes.data.map(function(r){return r.username||r.user_id;})); var profMap = {}; var pendingUids = []; mRes.data.forEach(function(r) { profMap[r.user_id] = { id: r.user_id, fname: r.fname, lname: r.lname, username: r.username }; if (r.status === 'pending') pendingUids.push(r.user_id); }); // accepted members only for the main count var acceptedData = mRes.data.filter(function(r){ return r.status === 'accepted'; }); container.innerHTML = ''; // Section: Members var membersLabel = document.createElement('div'); membersLabel.className = 'sec-lbl'; membersLabel.textContent = 'Members (' + acceptedData.length + ')'; // Also update the header member count var gdMembers = document.getElementById('gd-members'); if (gdMembers) gdMembers.textContent = acceptedData.length + ' member' + (acceptedData.length !== 1 ? 's' : ''); container.appendChild(membersLabel); acceptedData.forEach(function(m) { var p = profMap[m.user_id]; var name = p ? (p.fname + ' ' + (p.lname||'')).trim() : 'Unknown'; var handle = p && p.username ? '@' + p.username : ''; var initials = p ? ((p.fname||'?')[0] + ((p.lname||'?')[0]||'?')).toUpperCase() : '??'; var isYou = m.user_id === currentUser.id; var isCreator = m.user_id === window._currentGroupCreatedBy; var row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px 4px;border-bottom:0.5px solid var(--border);'; row.innerHTML = '
' + initials + '
' + '
' + '
' + name + (isYou ? ' (you)' : '') + '
' + '
' + handle + '
' + '
' + (isCreator ? 'Creator' : ''); container.appendChild(row); }); // Section: Pending invites if (pendingUids.length > 0) { var pendingLabel = document.createElement('div'); pendingLabel.className = 'sec-lbl'; pendingLabel.style.marginTop = '16px'; pendingLabel.textContent = 'Invited (' + pendingUids.length + ')'; container.appendChild(pendingLabel); pendingUids.forEach(function(uid) { var p = profMap[uid]; var name = p ? (p.fname + ' ' + (p.lname||'')).trim() : 'Unknown'; var handle = p && p.username ? '@' + p.username : ''; var initials = p ? ((p.fname||'?')[0] + ((p.lname||'?')[0]||'?')).toUpperCase() : '??'; var row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px 4px;border-bottom:0.5px solid var(--border);opacity:0.65;'; row.innerHTML = '
' + initials + '
' + '
' + '
' + name + '
' + '
' + handle + '
' + '
' + 'Invited'; container.appendChild(row); }); } } catch(e) { console.error('loadGroupMembers error:', e.message); container.innerHTML = '
Could not load members.
'; } } // ---- END GROUP MEMBERS TAB ---- async function renderGLB(period) { const el = document.getElementById('glb-list'); if (!el) return; var groupId = window._currentGroupId; if (!window._sb || !groupId) { el.innerHTML = '
No data yet.
'; return; } el.innerHTML = '
Loading...
'; var since = new Date(); if (period === 'week') since.setDate(since.getDate()-7); else if (period === 'month') since.setMonth(since.getMonth()-1); else since = new Date(0); var res = await window._sb.from('group_posts') .select('user_id, result, stake') .eq('group_id', groupId) .gte('created_at', since.toISOString()); if (res.error || !res.data || res.data.length === 0) { el.innerHTML = '
No bets posted yet.
'; return; } // Fetch profiles separately var glbUids = [...new Set(res.data.map(function(p) { return p.user_id; }))]; var { data: glbProfs } = await window._sb.from('profiles').select('id, fname, lname, username').in('id', glbUids); var glbProfMap = {}; (glbProfs || []).forEach(function(p) { glbProfMap[p.id] = p; }); var players = {}; res.data.forEach(function(post) { var uid = post.user_id; var p = glbProfMap[uid] || null; var name = p ? (p.username ? '@'+p.username : (p.fname+' '+(p.lname||'')).trim()) : 'User'; var stk = parseFloat((post.stake||'0').replace(/[^0-9.]/g,''))||0; if (!players[uid]) players[uid] = { name:name, won:0, lost:0, push:0, net:0 }; if (post.result==='won') { players[uid].won++; players[uid].net+=stk; } else if (post.result==='lost') { players[uid].lost++; players[uid].net-=stk; } else if (post.result==='push') players[uid].push++; }); var rows = Object.values(players).sort(function(a,b){return b.net-a.net;}); el.innerHTML = rows.map(function(r,i) { var pos = r.net >= 0; var plStr = (pos?'+':'-') + '$' + Math.abs(r.net).toFixed(2); var rec = r.won + '-' + r.lost + (r.push>0?'-'+r.push:''); return '
' + '
' + (i+1) + '
' + '
' + (r.name[0]||'?').toUpperCase() + '
' + '
' + r.name + '
' + rec + '
' + '
' + plStr + '
' + '
'; }).join(''); } function switchGLB(el, period) { el.closest('.seg').querySelectorAll('.seg-btn').forEach(b => b.classList.remove('on')); el.classList.add('on'); renderGLB(period); } function pickEmoji(btn) { document.querySelectorAll('.emoji-opt').forEach(b => b.classList.remove('on')); btn.classList.add('on'); currentGroupEmoji = btn.textContent.trim(); } function setAccess(type, btn) { groupAccess = type; document.getElementById('access-private').classList.toggle('on', type === 'private'); document.getElementById('access-link').classList.toggle('on', type === 'link'); } async function createGroup() { const name = document.getElementById('group-name-inp').value.trim(); if (!name) { toast('Enter a group name!'); return; } // Disable button immediately to prevent double-tap creating two groups var createBtn = document.querySelector('#screen-create-group .btn-primary'); if (createBtn) { createBtn.disabled = true; createBtn.textContent = 'Creating...'; } var groupId = 'g-' + Date.now(); var savedToDb = false; if (window._sb && currentUser.id) { var sbId = await saveGroupToSupabase(name, currentGroupEmoji, groupAccess); if (sbId) { groupId = sbId; savedToDb = true; } } if (savedToDb) toast('Group created!'); // If not savedToDb, saveGroupToSupabase already showed the error toast // Reload from DB — loadUserGroups clears groupsData and rebuilds from scratch, // so we must NOT push locally first (that would cause a duplicate). // loadUserGroups also calls renderGroups() internally when data is found. setTimeout(async function() { try { if (window._sb && currentUser.id) { await loadUserGroups(); } else { // Offline/demo fallback — add locally only when there's no DB groupsData.unshift({ id: groupId, emoji: currentGroupEmoji, name: name, members: 1, lastMsg: 'Group created', time: 'now', unread: 0, access: groupAccess, feedBets: [], createdBy: currentUser.id }); renderGroups(); } } catch(e) { console.error('createGroup reload error:', e.message); } finally { // Always re-enable button and navigate — even if reload fails if (createBtn) { createBtn.disabled = false; createBtn.textContent = 'Create group'; } // Clear the group name input for next time var nameInp = document.getElementById('group-name-inp'); if (nameInp) nameInp.value = ''; showScreen('groups'); } }, 300); } async function joinByLink() { const val = document.getElementById('invite-link-inp').value.trim(); if (!val) { toast('Paste an invite link first!'); return; } var groupId = val.split('/join/').pop().split('?')[0].trim(); if (!groupId || groupId === val) { toast('Invalid invite link.'); return; } if (!window._sb || !currentUser.id) { toast('Please log in first.'); return; } var gr = await window._sb.from('groups').select('id,name,emoji,created_by').eq('id', groupId).single(); if (gr.error || !gr.data) { toast('Group not found or link expired.'); return; } var ins = await window._sb.from('group_members').insert({ group_id: groupId, user_id: currentUser.id }); if (ins.error && ins.error.code !== '23505') { toast('Could not join group. Try again.'); return; } await loadUserGroups(); renderGroups(); toast('Joined ' + gr.data.name + '!'); document.getElementById('invite-link-inp').value = ''; } async function showGroupSettings() { var nameEl = document.getElementById('group-settings-name'); var titleEl = document.getElementById('group-settings-title'); if (nameEl) nameEl.textContent = document.getElementById('gd-name').textContent; if (titleEl) titleEl.textContent = 'Group Settings'; // If _currentGroupCreatedBy is missing, fetch it from DB var createdBy = window._currentGroupCreatedBy; if (!createdBy && window._currentGroupId && window._sb) { try { var gr = await window._sb.from('groups').select('created_by').eq('id', window._currentGroupId).single(); if (gr.data) { createdBy = gr.data.created_by; window._currentGroupCreatedBy = createdBy; } } catch(e) {} } var isCreator = createdBy && createdBy === currentUser.id; var creatorDiv = document.getElementById('group-settings-creator-actions'); var memberDiv = document.getElementById('group-settings-member-actions'); if (creatorDiv) creatorDiv.style.display = isCreator ? 'block' : 'none'; if (memberDiv) memberDiv.style.display = isCreator ? 'none' : 'block'; document.getElementById('group-settings-sheet').style.display = 'block'; } async function confirmLeaveGroup() { hideGroupSettings(); var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'this group'; if (!confirm('Leave ' + groupName + '? You can rejoin with an invite link.')) return; await leaveGroup(); } async function leaveGroup() { if (!window._sb || !currentUser.id || !window._currentGroupId) return; var r = await window._sb.from('group_members') .delete() .eq('group_id', window._currentGroupId) .eq('user_id', currentUser.id); if (r.error) { toast('Could not leave group: ' + r.error.message); return; } var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'group'; toast('You left ' + groupName); groupsData.splice(0, groupsData.length, ...groupsData.filter(function(g) { return g.id !== window._currentGroupId; })); window._currentGroupId = null; window._currentGroupCreatedBy = null; showScreen('groups'); renderGroups(); setTimeout(function() { loadUserGroups(); }, 400); } function hideGroupSettings() { document.getElementById('group-settings-sheet').style.display = 'none'; } function confirmDeleteGroup() { hideGroupSettings(); document.getElementById('group-delete-confirm').style.display = 'block'; } async function deleteGroup() { var groupId = window._currentGroupId; var btn = document.getElementById('delete-group-confirm-btn'); if (!groupId) { toast('No group selected.'); return; } if (btn) { btn.textContent = 'Deleting...'; btn.disabled = true; } if (window._sb && currentUser.id) { try { // Delete group members first (FK constraint), then the group await window._sb.from('group_members').delete().eq('group_id', groupId); } catch(e) {} // ignore if no members try { var res = await window._sb.from('groups').delete().eq('id', groupId).eq('created_by', currentUser.id); if (res.error) { toast('Could not delete group. Try again.'); if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } return; } } catch(e) { toast('Could not delete group. Try again.'); if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } return; } } // Remove from local state var idx = groupsData.findIndex(function(g) { return g.id === groupId; }); if (idx !== -1) groupsData.splice(idx, 1); window._currentGroupId = null; window._currentGroupCreatedBy = null; // Reset button for next delete if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } document.getElementById('group-delete-confirm').style.display = 'none'; renderGroups(); showScreen('groups'); toast('Group deleted.'); } async function showGroupInvite() { var groupId = window._currentGroupId; var link = 'betnbanter.com/join/' + (groupId || 'group'); var linkEl = document.getElementById('invite-link-text'); if (linkEl) linkEl.textContent = link; document.getElementById('group-invite-overlay').style.display = 'block'; // Load real friends into invite row var row = document.getElementById('invite-friends-row'); if (!row) return; row.innerHTML = '
Loading friends...
'; if (!window._sb || !currentUser.id) { row.innerHTML = '
No friends yet — add some first!
'; return; } var results = await Promise.all([ window._sb.from('friends').select('friend_id').eq('user_id', currentUser.id).eq('status', 'accepted'), window._sb.from('friends').select('user_id').eq('friend_id', currentUser.id).eq('status', 'accepted') ]); var seen = new Set(); var friendIds = []; (results[0].data || []).forEach(function(f) { if (!seen.has(f.friend_id)) { seen.add(f.friend_id); friendIds.push(f.friend_id); } }); (results[1].data || []).forEach(function(f) { if (!seen.has(f.user_id)) { seen.add(f.user_id); friendIds.push(f.user_id); } }); var friends = []; if (friendIds.length > 0) { var { data: profData } = await window._sb.from('profiles').select('id, fname, lname').in('id', friendIds); friends = profData || []; } if (friends.length === 0) { row.innerHTML = '
No friends yet — add some first!
'; return; } // Also fetch who is already in the group so we can show them as "Added" var acceptedMembers = new Set(); var pendingMembers = new Set(); if (window._currentGroupId) { try { var emRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: window._currentGroupId }); (emRes.data || []).forEach(function(r) { if (r.status === 'accepted') acceptedMembers.add(r.user_id); else pendingMembers.add(r.user_id); }); } catch(e) {} } row.innerHTML = ''; friends.forEach(function(p) { var name = p.fname + ' ' + (p.lname ? p.lname[0] + '.' : ''); var btn = document.createElement('button'); var isAccepted = acceptedMembers.has(p.id); var isPending = pendingMembers.has(p.id); if (isAccepted) { btn.className = 'inv-chip on'; btn.textContent = name + ' ✓'; btn.disabled = true; btn.style.opacity = '0.5'; btn.style.cursor = 'default'; } else if (isPending) { btn.className = 'inv-chip'; btn.textContent = name + ' (invited)'; btn.disabled = true; btn.style.opacity = '0.6'; btn.style.cursor = 'default'; } else { btn.className = 'inv-chip'; btn.textContent = name; btn.onclick = function() { toggleInv(btn); }; } btn.dataset.friendId = p.id; row.appendChild(btn); }); } async function sendGroupInvites() { var groupId = window._currentGroupId; if (!groupId || !window._sb || !currentUser.id) { closeInviteOverlay(); return; } var selected = document.querySelectorAll('#invite-friends-row .inv-chip.on'); if (selected.length === 0) { toast('Select at least one friend to invite.'); return; } var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'a group'; var inviterName = (currentUser.fname + ' ' + (currentUser.lname||'')).trim() || 'Someone'; // First check who is already a member so we skip them // Use view to avoid recursive RLS var existingRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: groupId }); var alreadyIn = new Set((existingRes.data || []).map(function(r){ return r.user_id; })); var count = 0; var errors = []; for (var i = 0; i < selected.length; i++) { var fid = selected[i].dataset.friendId; if (!fid) continue; if (alreadyIn.has(fid)) continue; // already a member — skip silently // Insert the group_members row // RLS policy needed: auth.uid() = user_id OR group creator/member can insert for others var r = await window._sb.from('group_members').insert({ group_id: groupId, user_id: fid, status: 'pending' }); if (r.error) { console.warn('group_members invite error:', r.error.message, r.error.code); if (r.error.code === '42501') { errors.push('Permission denied — check group_members INSERT policy in Supabase.'); } else if (r.error.code === '23505') { // Already a member (unique constraint) — not an error count++; } else { errors.push(r.error.message); } continue; } count++; } if (errors.length > 0) { toast(errors[0]); } else if (count > 0) { toast(count + ' friend' + (count !== 1 ? 's' : '') + ' invited to ' + groupName + '!'); } else { toast('No new members to invite.'); } closeInviteOverlay(); await loadUserGroups(); renderGroups(); } // ---- GROUP INVITE ACCEPT / DECLINE ---- function showGroupInvitePrompt(groupId, groupName, inviterName) { var existing = document.getElementById('group-invite-prompt'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'group-invite-prompt'; overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:400;display:flex;align-items:flex-end;justify-content:center;'; var sheet = document.createElement('div'); sheet.onclick = function(e) { e.stopPropagation(); }; sheet.style.cssText = 'width:100%;max-width:480px;background:var(--surface);border-radius:20px 20px 0 0;padding:28px 20px calc(env(safe-area-inset-bottom,0px) + 90px);box-sizing:border-box;'; var handle = document.createElement('div'); handle.style.cssText = 'width:36px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 20px;'; var icon = document.createElement('div'); icon.style.cssText = 'font-size:22px;text-align:center;margin-bottom:8px;'; icon.textContent = '\uD83D\uDC65'; var title = document.createElement('div'); title.style.cssText = 'font-size:18px;font-weight:700;text-align:center;margin-bottom:6px;'; title.textContent = 'Group Invite'; var sub = document.createElement('div'); sub.style.cssText = 'font-size:14px;color:var(--text-2);text-align:center;margin-bottom:24px;'; sub.innerHTML = '' + inviterName + ' invited you to join ' + groupName + ''; var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:10px;'; var decBtn = document.createElement('button'); decBtn.style.cssText = 'flex:1;padding:13px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text-1);font-family:var(--font);font-size:15px;font-weight:600;cursor:pointer;'; decBtn.textContent = 'Decline'; decBtn.addEventListener('click', function() { declineGroupInvite(groupId); }); var accBtn = document.createElement('button'); accBtn.id = 'gip-accept-btn'; accBtn.style.cssText = 'flex:2;padding:13px;border:none;border-radius:var(--radius);background:var(--green);color:#fff;font-family:var(--font);font-size:15px;font-weight:600;cursor:pointer;'; accBtn.textContent = 'Accept'; accBtn.addEventListener('click', function() { acceptGroupInvite(groupId, groupName); }); btnRow.appendChild(decBtn); btnRow.appendChild(accBtn); sheet.appendChild(handle); sheet.appendChild(icon); sheet.appendChild(title); sheet.appendChild(sub); sheet.appendChild(btnRow); overlay.appendChild(sheet); overlay.addEventListener('click', function() { overlay.remove(); }); document.body.appendChild(overlay); } async function acceptGroupInvite(groupId, groupName) { var prompt = document.getElementById('group-invite-prompt'); var accBtn = document.getElementById('gip-accept-btn'); if (accBtn) { accBtn.textContent = 'Joining...'; accBtn.disabled = true; } if (!window._sb || !currentUser.id) { toast('Not logged in.'); return; } // Update status to accepted var r = await window._sb.from('group_members') .update({ status: 'accepted' }) .eq('group_id', groupId) .eq('user_id', currentUser.id); if (r.error) { console.error('acceptGroupInvite error:', r.error.message, r.error.code); if (r.error.code === '42501') { toast('Permission denied — check group_members UPDATE policy.'); } else { toast('Could not join: ' + r.error.message); } if (accBtn) { accBtn.textContent = 'Accept'; accBtn.disabled = false; } return; } if (prompt) prompt.remove(); toast('You joined ' + groupName + '! 🎉'); // Mark the invite notification as read var nid = 'grp_inv_' + groupId; notifStore.forEach(function(n) { if (n.id === nid) n.read = true; }); saveNotifStore(); saveReadNotifs(); updateNotifBadge(); // Reload groups and navigate await loadUserGroups(); renderGroups(); showScreen('groups'); } async function declineGroupInvite(groupId) { var prompt = document.getElementById('group-invite-prompt'); if (prompt) prompt.remove(); if (!window._sb || !currentUser.id) return; await window._sb.from('group_members').delete().eq('group_id', groupId).eq('user_id', currentUser.id); var nid = 'grp_inv_' + groupId; seenNotifIds.delete(nid); notifStore = notifStore.filter(function(n) { return n.id !== nid; }); saveNotifStore(); saveSeenNotifs(); updateNotifBadge(); renderNotifications(); toast('Invite declined.'); } // ---- END GROUP INVITE ACCEPT / DECLINE ---- function closeInviteOverlay() { document.getElementById('group-invite-overlay').style.display = 'none'; } function copyInviteLink() { var linkEl = document.getElementById('invite-link-text'); var link = linkEl ? linkEl.textContent : ('betnbanter.com/join/' + (window._currentGroupId || 'group')); if (navigator.clipboard) { navigator.clipboard.writeText(link).then(() => toast('Link copied!')).catch(() => toast('Link: ' + link)); } else { toast('Link: ' + link); } } var unreadChatCount = 0; var _chatGroupId = null; // which group's chat is currently open async function loadGroupChat(groupId) { if (!window._sb || !groupId) return; _chatGroupId = groupId; var msgs = document.getElementById('chat-messages'); if (!msgs) return; msgs.innerHTML = ''; var res = await window._sb.from('group_chat') .select('*') .eq('group_id', groupId) .order('created_at', { ascending: true }) .limit(100); if (res.error || !res.data) return; // Fetch profiles separately var chatUids = [...new Set(res.data.map(function(m) { return m.user_id; }))]; var { data: chatProfs } = await window._sb.from('profiles').select('id, fname, lname, username').in('id', chatUids); var chatProfMap = {}; (chatProfs || []).forEach(function(p) { chatProfMap[p.id] = p; }); res.data.forEach(function(m) { var p = chatProfMap[m.user_id] || null; var isMe = m.user_id === currentUser.id; var div = document.createElement('div'); div.className = 'chat-msg ' + (isMe ? 'mine' : 'other'); if (isMe) { div.innerHTML = '
' + (m.message||'') + '
'; } else { var initials = p ? ((p.fname||'U')[0]+(p.lname||'')[0]).toUpperCase() : 'U?'; div.innerHTML = '
' + initials + '
' + (m.message||'') + '
'; } msgs.appendChild(div); }); scrollChat(); } async function sendChat() { const inp = document.getElementById('chat-inp'); const msg = inp.value.trim(); if (!msg) return; inp.value = ''; var groupId = window._currentGroupId; const msgs = document.getElementById('chat-messages'); const div = document.createElement('div'); div.className = 'chat-msg mine'; div.innerHTML = '
' + msg + '
'; msgs.appendChild(div); scrollChat(); if (window._sb && currentUser.id && groupId) { await window._sb.from('group_chat').insert({ group_id: groupId, user_id: currentUser.id, message: msg }); } } function receiveChatMessage(text, avClass, initials) { const msgs = document.getElementById('chat-messages'); if (!msgs) return; const div = document.createElement('div'); div.className = 'chat-msg other'; div.innerHTML = '
' + initials + '
' + text + '
'; msgs.appendChild(div); scrollChat(); // Show badge if chat tab is not currently active const chatTab = document.getElementById('gtab-chat'); if (chatTab && !chatTab.classList.contains('active')) { unreadChatCount++; const badge = document.getElementById('chat-badge'); if (badge) badge.style.display = 'inline-block'; } } function scrollChat() { const msgs = document.getElementById('chat-messages'); if (msgs) setTimeout(() => msgs.scrollTop = msgs.scrollHeight, 50); } function calcGroupPot() { const s = parseFloat(document.getElementById('gbet-stake').value) || 0; const el = document.getElementById('group-pot'); if (el) el.textContent = '$' + (s * 5).toFixed(2); } async function postGroupBet() { const desc = (document.getElementById('gbet-desc') || {}).value || ''; const stake = (document.getElementById('gbet-stake') || {}).value || ''; const trimmedDesc = desc.trim(); if (!trimmedDesc) { toast('Describe the bet first!'); return; } if (!stake || parseFloat(stake) <= 0) { toast('Set a stake amount!'); return; } if (!window._sb || !currentUser.id) { toast('Not logged in.'); return; } if (!window._currentGroupId) { toast('No group selected.'); return; } const btn = document.querySelector('#screen-create-group-bet .btn-primary'); if (btn) { btn.textContent = 'Posting...'; btn.disabled = true; } try { var ins = await window._sb.from('group_posts').insert({ group_id: window._currentGroupId, user_id: currentUser.id, bet_desc: trimmedDesc, platform: 'Group Bet', stake: stake, sport: null, result: 'pending' }); if (ins.error) { console.error('postGroupBet error:', ins.error.message, ins.error.code); if (ins.error.code === '42501') { toast('Permission denied — check group_posts RLS INSERT policy.'); } else { toast('Could not post bet: ' + ins.error.message); } return; } // Clear the form var descEl = document.getElementById('gbet-desc'); var stakeEl = document.getElementById('gbet-stake'); if (descEl) descEl.value = ''; if (stakeEl) stakeEl.value = ''; toast('Group bet posted!'); showScreen('group-detail'); switchGroupTab('bets'); loadGroupBets(window._currentGroupId); loadGroupFeed(window._currentGroupId); } catch(e) { console.error('postGroupBet exception:', e.message); toast('Error: ' + e.message); } finally { if (btn) { btn.textContent = 'Post group bet'; btn.disabled = false; } } } // ── DEMO DATA ── // Feed cards for demo friends function renderDemoFeed() { // No demo data — start fresh for beta const feedEmpty = document.getElementById('feed-empty'); if (feedEmpty) feedEmpty.style.display = 'flex'; } // Balances for demo friends function renderDemoBalances() { // No demo balances — start fresh } // Notifications for demo activity function renderDemoNotifs() { // No demo notifications — start fresh } // Friends list for demo friends function renderDemoFriends() { // No demo friends — start fresh document.getElementById('friends-list-lbl').textContent = 'Your friends'; } // Standings leaderboard with demo data function buildDemoLB() { const you = { av:'av-g', i: currentUser.initials || '?', n:'You', r:'0–0', pl:'$0', pos:true }; return { today:[{...you}], week:[{...you}], month:[{...you}], year:[{...you}], all:[{...you}] }; } // Groups demo data function buildDemoGroups() { return []; } // Group leaderboard demo data function buildDemoGLB() { return { week:[], month:[], all:[] }; } // Bet history for "You" — 1 pending from demo side bet const demoHistData = []; // Suggested friends for the Friends tab const demoSuggested = []; function populateInviteChips() { ['challenge-chips','group-invite-chips'].forEach(id => { const el = document.getElementById(id); if (!el) return; el.innerHTML = demoFriends.map(f => `` ).join(''); }); } function renderDemoSuggested() { const list = document.getElementById('suggested-list'); if (!list) return; list.innerHTML = ''; demoSuggested.forEach(s => { const card = document.createElement('div'); card.className = 'card'; card.style.padding = '12px 14px'; const mutualHtml = s.mutuals.map(m => `${m}`).join(''); card.innerHTML = `
${s.init}
${s.name}
${s.handle} · ${s.mutuals.length} mutual friend${s.mutuals.length>1?'s':''}
${mutualHtml}
`; list.appendChild(card); }); } // init renderObFriends(''); toggleSplashBtns(); initDevBypass(); // ---- BET CREATION SCREENS ---- var _betSelectedFriend = { ff: null, golf: null, custom: null }; function populateBetFriendPicker(prefix) { _betSelectedFriend[prefix] = null; var selEl = document.getElementById(prefix + '-selected-friend'); if (selEl) selEl.style.display = 'none'; var listEl = document.getElementById(prefix + '-friend-list'); var emptyEl = document.getElementById(prefix + '-friend-empty'); var errEl = document.getElementById(prefix + '-error'); if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; } if (!listEl) return; var friends = (typeof friendsData !== 'undefined' ? friendsData : []).filter(function(f){ return f && f.id; }); if (friends.length === 0) { listEl.innerHTML = ''; if (emptyEl) emptyEl.style.display = 'block'; return; } if (emptyEl) emptyEl.style.display = 'none'; listEl.innerHTML = ''; friends.forEach(function(f) { var name = f.name || ((f.fname||'') + ' ' + (f.lname||'')).trim() || 'Friend'; var initials = (name.split(' ').map(function(w){ return w[0]||''; }).join('').toUpperCase()||'F').substring(0,2); var row = document.createElement('div'); row.setAttribute('data-fid', f.id); row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;'; row.innerHTML = '
' + initials + '
' + '
' + name + '
' + '
'; row.addEventListener('click', (function(fObj, fName) { return function() { listEl.querySelectorAll('[data-fid]').forEach(function(r) { r.style.borderColor = 'var(--border)'; r.style.background = ''; var c = r.querySelector('.friend-pick-check'); if (c) { c.style.background = ''; c.style.borderColor = 'var(--border)'; } }); row.style.borderColor = 'var(--green)'; row.style.background = '#f0faf0'; var chk = row.querySelector('.friend-pick-check'); if (chk) { chk.style.background = 'var(--green)'; chk.style.borderColor = 'var(--green)'; } _betSelectedFriend[prefix] = { id: fObj.id, name: fName }; var s = document.getElementById(prefix + '-selected-friend'); if (s) { s.textContent = '✓ Challenging ' + fName; s.style.display = 'block'; } }; })(f, name)); listEl.appendChild(row); }); } async function submitH2HChallenge(prefix) { var sportMap = { ff: 'Fantasy Football', golf: 'Golf', custom: 'Other' }; var descEl = document.getElementById(prefix + '-desc'); var stakeEl = document.getElementById(prefix + '-stake'); var errEl = document.getElementById(prefix + '-error'); var desc = descEl ? descEl.value.trim() : ''; // For Golf/Nassau: sum the Nassau stakes if no single stake field var stake = 0; if (prefix === 'golf' && _golfFormat === 'Nassau') { var f = parseFloat((document.getElementById('nassau-front') || {}).value) || 0; var b = parseFloat((document.getElementById('nassau-back') || {}).value) || 0; var o = parseFloat((document.getElementById('nassau-overall') || {}).value) || 0; stake = f + b + o; } else { stake = stakeEl ? (parseFloat(stakeEl.value) || 0) : 0; } var friend = _betSelectedFriend[prefix]; function showErr(msg) { if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; } else toast(msg); } if (errEl) errEl.style.display = 'none'; if (!desc) { showErr('Please describe the bet.'); return; } if (prefix !== 'custom' && stake <= 0) { showErr('Please enter a stake amount.'); return; } if (!friend) { showErr('Please select a friend to challenge.'); return; } if (!window._sb || !currentUser.id) { showErr('You must be logged in.'); return; } var btn = document.querySelector('#screen-create-' + prefix + '-bet .btn-primary'); if (btn) { btn.textContent = 'Sending...'; btn.disabled = true; } try { var ins = await window._sb.from('h2h_challenges').insert({ initiator_id: currentUser.id, recipient_id: friend.id, description: desc, stake: stake || 0, sport: sportMap[prefix] || 'Other', status: 'pending' }); if (ins.error) throw new Error(ins.error.code === '42501' ? 'Permission denied — check RLS INSERT policy on h2h_challenges.' : ins.error.message); if (descEl) descEl.value = ''; if (stakeEl) stakeEl.value = ''; _betSelectedFriend[prefix] = null; var selEl = document.getElementById(prefix + '-selected-friend'); if (selEl) selEl.style.display = 'none'; toast('Challenge sent to ' + friend.name + '!'); showScreen('bets'); if (typeof loadSentChallenges === 'function') loadSentChallenges(); } catch(e) { console.error('submitH2HChallenge:', e.message); showErr('Error: ' + e.message); if (btn) { btn.textContent = 'Send Challenge'; btn.disabled = false; } } } // ---- END BET CREATION SCREENS ---- // ---- FANTASY FOOTBALL AI SCREENSHOT ANALYSIS ---- var _ffSuggestedStake = null; // stored by AI for Apply button async function handleFFScreenshot(input) { if (!input.files || !input.files[0]) return; var file = input.files[0]; // Show preview image var preview = document.getElementById('ff-preview-img'); var previewWrap = document.getElementById('ff-upload-preview'); var prompt = document.getElementById('ff-upload-prompt'); var loading = document.getElementById('ff-upload-loading'); var zone = document.getElementById('ff-upload-zone'); var reader = new FileReader(); reader.onload = function(e) { if (preview) { preview.src = e.target.result; } if (previewWrap) previewWrap.style.display = 'block'; if (prompt) prompt.style.display = 'none'; }; reader.readAsDataURL(file); // Show loading spinner if (loading) loading.style.display = 'block'; if (zone) zone.style.pointerEvents = 'none'; try { // Convert to base64 for Anthropic API var base64 = await fileToBase64(file); var mediaType = file.type || 'image/jpeg'; // Step 1: Extract matchup data from screenshot var extractRes = await callAnthropicVision(base64, mediaType, 'You are reading a fantasy football matchup screenshot. Extract ONLY valid JSON (no markdown): ' + '{"myTeam":"team name","myProj":142.4,"oppTeam":"opponent name","oppProj":138.1,"week":"Week 8","platform":"ESPN"}. ' + 'Replace values with what you see. Use null for any field not visible. Return only the JSON object, nothing else.' ); var matchupData = null; try { var cleaned = extractRes.replace(/```json|```/g, '').trim(); matchupData = JSON.parse(cleaned); } catch(e) { console.warn('Parse error:', e.message, extractRes); } // Fill form fields if we got data if (matchupData) { ffDescEdited = false; // allow auto-rebuild if (matchupData.myTeam) { var el = document.getElementById('ff-my-team'); if(el) el.value = matchupData.myTeam; } if (matchupData.myProj) { var el = document.getElementById('ff-my-proj'); if(el) el.value = matchupData.myProj; } if (matchupData.oppTeam) { var el = document.getElementById('ff-opp-team'); if(el) el.value = matchupData.oppTeam; } if (matchupData.oppProj) { var el = document.getElementById('ff-opp-proj'); if(el) el.value = matchupData.oppProj; } if (matchupData.week) { var wk = document.getElementById('ff-week'); if (wk) { for (var i = 0; i < wk.options.length; i++) { if (wk.options[i].text === matchupData.week) { wk.selectedIndex = i; break; } } } } if (matchupData.platform) { var pl = document.getElementById('ff-platform'); if (pl) { for (var i = 0; i < pl.options.length; i++) { if (pl.options[i].text.toLowerCase() === (matchupData.platform||'').toLowerCase()) { pl.selectedIndex = i; break; } } } } ffBuildDesc(); } // Step 2: Generate fair odds from the projections var myProj = matchupData && matchupData.myProj ? parseFloat(matchupData.myProj) : null; var oppProj = matchupData && matchupData.oppProj ? parseFloat(matchupData.oppProj) : null; var myTeam = matchupData && matchupData.myTeam ? matchupData.myTeam : 'Your team'; var oppTeam = matchupData && matchupData.oppTeam ? matchupData.oppTeam : 'Their team'; var oddsPrompt = 'You are a fantasy football betting analyst. Suggest fair betting odds and a recommended stake for this matchup. ' + 'Matchup: ' + myTeam + ' (proj. ' + (myProj||'unknown') + ' pts) vs ' + oppTeam + ' (proj. ' + (oppProj||'unknown') + ' pts). ' + 'Provide 5 short lines: 1) Who is favoured and by how much. 2) Fair money line odds (e.g. -130 / +110). ' + '3) Point spread (e.g. -6.5). 4) Recommended flat stake (e.g. $20 straight up, or handicap if lopsided). ' + '5) One sentence: competitive or lopsided? Be concise. No markdown headers.'; var oddsText = await callAnthropicText(oddsPrompt); // Show odds box var oddsBox = document.getElementById('ff-odds-box'); var oddsContent = document.getElementById('ff-odds-content'); if (oddsBox) oddsBox.style.display = 'block'; if (oddsContent) oddsContent.textContent = oddsText; // Try to extract a suggested stake number for the Apply button var stakeMatch = oddsText.match(/\$(\d+)/); _ffSuggestedStake = stakeMatch ? stakeMatch[1] : null; toast('Matchup analysed! Check the AI odds below.'); } catch(e) { console.error('FF screenshot error:', e.message); toast('Could not read screenshot: ' + e.message); // Reset upload zone if (prompt) prompt.style.display = 'block'; if (previewWrap) previewWrap.style.display = 'none'; } finally { if (loading) loading.style.display = 'none'; if (zone) zone.style.pointerEvents = 'auto'; // Reset input so same file can be re-selected input.value = ''; } } function ffApplyOdds() { if (_ffSuggestedStake) { var stakeEl = document.getElementById('ff-stake'); if (stakeEl) { stakeEl.value = _ffSuggestedStake; toast('Stake set to $' + _ffSuggestedStake); } } } function fileToBase64(file) { return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function(e) { // Strip the data:image/xxx;base64, prefix var result = e.target.result; var base64 = result.split(',')[1]; resolve(base64); }; reader.onerror = reject; reader.readAsDataURL(file); }); } async function callAnthropicVision(base64, mediaType, prompt) { var response = await fetch(CLAUDE_PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY }, body: JSON.stringify({ model: 'claude-opus-4-5', max_tokens: 512, messages: [{ role: 'user', content: [ { type: 'image', source: { type: 'base64', media_type: mediaType, data: base64 } }, { type: 'text', text: prompt } ] }] }) }); if (!response.ok) { var err = await response.json().catch(function(){ return {}; }); throw new Error(err.error && err.error.message ? err.error.message : 'Proxy error ' + response.status); } var data = await response.json(); return data.content && data.content[0] ? data.content[0].text : ''; } async function callAnthropicText(prompt) { var response = await fetch(CLAUDE_PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 400, messages: [{ role: 'user', content: prompt }] }) }); if (!response.ok) { var err = await response.json().catch(function(){ return {}; }); throw new Error(err.error && err.error.message ? err.error.message : 'Proxy error ' + response.status); } var data = await response.json(); return data.content && data.content[0] ? data.content[0].text : ''; } // ---- END FANTASY FOOTBALL AI ---- // ---- FF + GOLF AUTO-DESC BUILDERS ---- var ffDescEdited = false; var golfDescEdited = false; var _golfFormat = 'Stroke Play'; function ffBuildDesc() { if (ffDescEdited) return; // user manually edited — don't overwrite var platform = (document.getElementById('ff-platform') || {}).value || 'Fantasy'; var week = (document.getElementById('ff-week') || {}).value || ''; var myTeam = ((document.getElementById('ff-my-team') || {}).value || '').trim(); var myProj = ((document.getElementById('ff-my-proj') || {}).value || '').trim(); var oppTeam = ((document.getElementById('ff-opp-team') || {}).value || '').trim(); var oppProj = ((document.getElementById('ff-opp-proj') || {}).value || '').trim(); var parts = []; if (platform && week) parts.push(platform + ' ' + week + ' matchup'); else if (week) parts.push(week + ' matchup'); else if (platform) parts.push(platform + ' matchup'); else parts.push('Fantasy Football matchup'); if (myTeam && oppTeam) { var scoreStr = (myProj && oppProj) ? ' (proj. ' + myProj + '–' + oppProj + ')' : ''; parts.push(myTeam + ' vs ' + oppTeam + scoreStr); } else if (myTeam) { parts.push('My team: ' + myTeam + (myProj ? ' (proj. ' + myProj + ')' : '')); } parts.push('Loser pays up.'); var el = document.getElementById('ff-desc'); if (el) el.value = parts.join('. '); } function selectGolfFormat(btn, fmt) { _golfFormat = fmt; document.querySelectorAll('#golf-format-chips .inv-chip').forEach(function(b) { b.classList.remove('on'); }); btn.classList.add('on'); // Show/hide Nassau section var nassau = document.getElementById('nassau-section'); var stakeSection = document.getElementById('golf-stake-section'); if (nassau) nassau.style.display = (fmt === 'Nassau') ? 'block' : 'none'; if (stakeSection) stakeSection.style.display = (fmt === 'Nassau') ? 'none' : 'block'; golfBuildDesc(); } function golfBuildDesc() { if (golfDescEdited) return; var course = ((document.getElementById('golf-course') || {}).value || '').trim(); var dateVal = ((document.getElementById('golf-date') || {}).value || '').trim(); var holes = ((document.getElementById('golf-holes') || {}).value || '18'); var handicap = ((document.getElementById('golf-handicap') || {}).value || 'gross'); var stake = ((document.getElementById('golf-stake') || {}).value || '').trim(); var dateStr = ''; if (dateVal) { try { dateStr = new Date(dateVal + 'T12:00:00').toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric' }); } catch(e) { dateStr = dateVal; } } var parts = []; var formatLabel = _golfFormat || 'Stroke Play'; var holesLabel = holes === '18' ? '18 holes' : holes + ' holes'; var capLabel = handicap === 'net' ? 'net (with handicaps)' : 'gross (no handicaps)'; if (course) parts.push(holesLabel + ' ' + formatLabel + ' at ' + course); else parts.push(holesLabel + ' ' + formatLabel); if (dateStr) parts.push(dateStr); parts.push(capLabel.charAt(0).toUpperCase() + capLabel.slice(1)); if (formatLabel === 'Nassau') { var front = ((document.getElementById('nassau-front') || {}).value || '').trim(); var back = ((document.getElementById('nassau-back') || {}).value || '').trim(); var overall = ((document.getElementById('nassau-overall') || {}).value || '').trim(); if (front || back || overall) { parts.push('Nassau: $' + (front||'?') + ' front / $' + (back||'?') + ' back / $' + (overall||'?') + ' overall'); } } else if (stake) { parts.push('$' + stake + ' match'); } var el = document.getElementById('golf-desc'); if (el) el.value = parts.join('. ') + '.'; } // Reset edited flags and re-init golf format when screens open var _origShowScreen = showScreen; // Patch showScreen to reset form state when navigating to bet screens (function() { var _orig = showScreen; showScreen = function(id) { if (id === 'create-ff-bet') { ffDescEdited = false; } if (id === 'create-golf-bet') { golfDescEdited = false; _golfFormat = 'Stroke Play'; // Reset format chips setTimeout(function() { document.querySelectorAll('#golf-format-chips .inv-chip').forEach(function(b) { b.classList.toggle('on', b.getAttribute('data-fmt') === 'Stroke Play'); }); var nassau = document.getElementById('nassau-section'); var stakeSection = document.getElementById('golf-stake-section'); if (nassau) nassau.style.display = 'none'; if (stakeSection) stakeSection.style.display = 'block'; }, 0); } _orig(id); }; })(); // ---- END FF + GOLF AUTO-DESC BUILDERS ---- + (b.stake||'0') + '
'; } if (isMe && !done) { html += '
'; html += ''; html += ''; html += ''; html += '
'; } div.innerHTML = html; if (isMe && !done) { div.querySelectorAll('.settle-btn').forEach(function(btn) { btn.addEventListener('click', function() { settleGroupBet(b.id, btn.dataset.r); }); }); } return div; } if (active.length > 0) { var al = document.createElement('div'); al.className = 'sec-lbl'; al.textContent = 'Active (' + active.length + ')'; container.appendChild(al); active.forEach(function(b){ container.appendChild(mkCard(b)); }); } if (settled.length > 0) { var sl = document.createElement('div'); sl.className = 'sec-lbl'; sl.style.marginTop = '16px'; sl.textContent = 'Settled (' + settled.length + ')'; container.appendChild(sl); settled.forEach(function(b){ container.appendChild(mkCard(b)); }); } if (active.length === 0 && settled.length === 0) { container.innerHTML = '
No group bets yet.
'; } } catch(e) { console.error('loadGroupBets:', e.message); container.innerHTML = '
Could not load bets.
'; } } async function settleGroupBet(postId, result) { if (!window._sb || !currentUser.id) return; var r = await window._sb.from('group_posts') .update({ result: result }) .eq('id', postId) .eq('user_id', currentUser.id); if (r.error) { toast('Could not settle: ' + r.error.message); return; } var e = result === 'won' ? '\uD83C\uDFC6' : result === 'lost' ? '\uD83D\uDCB8' : '\uD83E\uDD1D'; toast(e + ' Bet marked as ' + result + '!'); loadGroupBets(window._currentGroupId); loadGroupFeed(window._currentGroupId); } // ---- END GROUP BETS TAB ---- // ---- GROUP MEMBERS TAB ---- async function loadGroupMembers(groupId) { var container = document.getElementById('gscreen-members-list'); if (!container) return; if (!groupId || !window._sb) { container.innerHTML = '
No group selected.
'; return; } container.innerHTML = '
Loading members...
'; try { // Fetch all accepted members // Use view — avoids recursive RLS policy on group_members var mRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: groupId }); if (mRes.error) { console.error('loadGroupMembers RPC error:', mRes.error.message, mRes.error.code); container.innerHTML = '
Could not load members: ' + mRes.error.message + '
'; return; } if (!mRes.data || mRes.data.length === 0) { console.log('loadGroupMembers: no rows returned for group', groupId); container.innerHTML = '
No members yet.
'; return; } console.log('loadGroupMembers: got', mRes.data.length, 'rows:', mRes.data.map(function(r){return r.username||r.user_id;})); var profMap = {}; var pendingUids = []; mRes.data.forEach(function(r) { profMap[r.user_id] = { id: r.user_id, fname: r.fname, lname: r.lname, username: r.username }; if (r.status === 'pending') pendingUids.push(r.user_id); }); // accepted members only for the main count var acceptedData = mRes.data.filter(function(r){ return r.status === 'accepted'; }); container.innerHTML = ''; // Section: Members var membersLabel = document.createElement('div'); membersLabel.className = 'sec-lbl'; membersLabel.textContent = 'Members (' + acceptedData.length + ')'; // Also update the header member count var gdMembers = document.getElementById('gd-members'); if (gdMembers) gdMembers.textContent = acceptedData.length + ' member' + (acceptedData.length !== 1 ? 's' : ''); container.appendChild(membersLabel); acceptedData.forEach(function(m) { var p = profMap[m.user_id]; var name = p ? (p.fname + ' ' + (p.lname||'')).trim() : 'Unknown'; var handle = p && p.username ? '@' + p.username : ''; var initials = p ? ((p.fname||'?')[0] + ((p.lname||'?')[0]||'?')).toUpperCase() : '??'; var isYou = m.user_id === currentUser.id; var isCreator = m.user_id === window._currentGroupCreatedBy; var row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px 4px;border-bottom:0.5px solid var(--border);'; row.innerHTML = '
' + initials + '
' + '
' + '
' + name + (isYou ? ' (you)' : '') + '
' + '
' + handle + '
' + '
' + (isCreator ? 'Creator' : ''); container.appendChild(row); }); // Section: Pending invites if (pendingUids.length > 0) { var pendingLabel = document.createElement('div'); pendingLabel.className = 'sec-lbl'; pendingLabel.style.marginTop = '16px'; pendingLabel.textContent = 'Invited (' + pendingUids.length + ')'; container.appendChild(pendingLabel); pendingUids.forEach(function(uid) { var p = profMap[uid]; var name = p ? (p.fname + ' ' + (p.lname||'')).trim() : 'Unknown'; var handle = p && p.username ? '@' + p.username : ''; var initials = p ? ((p.fname||'?')[0] + ((p.lname||'?')[0]||'?')).toUpperCase() : '??'; var row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px 4px;border-bottom:0.5px solid var(--border);opacity:0.65;'; row.innerHTML = '
' + initials + '
' + '
' + '
' + name + '
' + '
' + handle + '
' + '
' + 'Invited'; container.appendChild(row); }); } } catch(e) { console.error('loadGroupMembers error:', e.message); container.innerHTML = '
Could not load members.
'; } } // ---- END GROUP MEMBERS TAB ---- async function renderGLB(period) { const el = document.getElementById('glb-list'); if (!el) return; var groupId = window._currentGroupId; if (!window._sb || !groupId) { el.innerHTML = '
No data yet.
'; return; } el.innerHTML = '
Loading...
'; var since = new Date(); if (period === 'week') since.setDate(since.getDate()-7); else if (period === 'month') since.setMonth(since.getMonth()-1); else since = new Date(0); var res = await window._sb.from('group_posts') .select('user_id, result, stake') .eq('group_id', groupId) .gte('created_at', since.toISOString()); if (res.error || !res.data || res.data.length === 0) { el.innerHTML = '
No bets posted yet.
'; return; } // Fetch profiles separately var glbUids = [...new Set(res.data.map(function(p) { return p.user_id; }))]; var { data: glbProfs } = await window._sb.from('profiles').select('id, fname, lname, username').in('id', glbUids); var glbProfMap = {}; (glbProfs || []).forEach(function(p) { glbProfMap[p.id] = p; }); var players = {}; res.data.forEach(function(post) { var uid = post.user_id; var p = glbProfMap[uid] || null; var name = p ? (p.username ? '@'+p.username : (p.fname+' '+(p.lname||'')).trim()) : 'User'; var stk = parseFloat((post.stake||'0').replace(/[^0-9.]/g,''))||0; if (!players[uid]) players[uid] = { name:name, won:0, lost:0, push:0, net:0 }; if (post.result==='won') { players[uid].won++; players[uid].net+=stk; } else if (post.result==='lost') { players[uid].lost++; players[uid].net-=stk; } else if (post.result==='push') players[uid].push++; }); var rows = Object.values(players).sort(function(a,b){return b.net-a.net;}); el.innerHTML = rows.map(function(r,i) { var pos = r.net >= 0; var plStr = (pos?'+':'-') + '$' + Math.abs(r.net).toFixed(2); var rec = r.won + '-' + r.lost + (r.push>0?'-'+r.push:''); return '
' + '
' + (i+1) + '
' + '
' + (r.name[0]||'?').toUpperCase() + '
' + '
' + r.name + '
' + rec + '
' + '
' + plStr + '
' + '
'; }).join(''); } function switchGLB(el, period) { el.closest('.seg').querySelectorAll('.seg-btn').forEach(b => b.classList.remove('on')); el.classList.add('on'); renderGLB(period); } function pickEmoji(btn) { document.querySelectorAll('.emoji-opt').forEach(b => b.classList.remove('on')); btn.classList.add('on'); currentGroupEmoji = btn.textContent.trim(); } function setAccess(type, btn) { groupAccess = type; document.getElementById('access-private').classList.toggle('on', type === 'private'); document.getElementById('access-link').classList.toggle('on', type === 'link'); } async function createGroup() { const name = document.getElementById('group-name-inp').value.trim(); if (!name) { toast('Enter a group name!'); return; } // Disable button immediately to prevent double-tap creating two groups var createBtn = document.querySelector('#screen-create-group .btn-primary'); if (createBtn) { createBtn.disabled = true; createBtn.textContent = 'Creating...'; } var groupId = 'g-' + Date.now(); var savedToDb = false; if (window._sb && currentUser.id) { var sbId = await saveGroupToSupabase(name, currentGroupEmoji, groupAccess); if (sbId) { groupId = sbId; savedToDb = true; } } if (savedToDb) toast('Group created!'); // If not savedToDb, saveGroupToSupabase already showed the error toast // Reload from DB — loadUserGroups clears groupsData and rebuilds from scratch, // so we must NOT push locally first (that would cause a duplicate). // loadUserGroups also calls renderGroups() internally when data is found. setTimeout(async function() { try { if (window._sb && currentUser.id) { await loadUserGroups(); } else { // Offline/demo fallback — add locally only when there's no DB groupsData.unshift({ id: groupId, emoji: currentGroupEmoji, name: name, members: 1, lastMsg: 'Group created', time: 'now', unread: 0, access: groupAccess, feedBets: [], createdBy: currentUser.id }); renderGroups(); } } catch(e) { console.error('createGroup reload error:', e.message); } finally { // Always re-enable button and navigate — even if reload fails if (createBtn) { createBtn.disabled = false; createBtn.textContent = 'Create group'; } // Clear the group name input for next time var nameInp = document.getElementById('group-name-inp'); if (nameInp) nameInp.value = ''; showScreen('groups'); } }, 300); } async function joinByLink() { const val = document.getElementById('invite-link-inp').value.trim(); if (!val) { toast('Paste an invite link first!'); return; } var groupId = val.split('/join/').pop().split('?')[0].trim(); if (!groupId || groupId === val) { toast('Invalid invite link.'); return; } if (!window._sb || !currentUser.id) { toast('Please log in first.'); return; } var gr = await window._sb.from('groups').select('id,name,emoji,created_by').eq('id', groupId).single(); if (gr.error || !gr.data) { toast('Group not found or link expired.'); return; } var ins = await window._sb.from('group_members').insert({ group_id: groupId, user_id: currentUser.id }); if (ins.error && ins.error.code !== '23505') { toast('Could not join group. Try again.'); return; } await loadUserGroups(); renderGroups(); toast('Joined ' + gr.data.name + '!'); document.getElementById('invite-link-inp').value = ''; } async function showGroupSettings() { var nameEl = document.getElementById('group-settings-name'); var titleEl = document.getElementById('group-settings-title'); if (nameEl) nameEl.textContent = document.getElementById('gd-name').textContent; if (titleEl) titleEl.textContent = 'Group Settings'; // If _currentGroupCreatedBy is missing, fetch it from DB var createdBy = window._currentGroupCreatedBy; if (!createdBy && window._currentGroupId && window._sb) { try { var gr = await window._sb.from('groups').select('created_by').eq('id', window._currentGroupId).single(); if (gr.data) { createdBy = gr.data.created_by; window._currentGroupCreatedBy = createdBy; } } catch(e) {} } var isCreator = createdBy && createdBy === currentUser.id; var creatorDiv = document.getElementById('group-settings-creator-actions'); var memberDiv = document.getElementById('group-settings-member-actions'); if (creatorDiv) creatorDiv.style.display = isCreator ? 'block' : 'none'; if (memberDiv) memberDiv.style.display = isCreator ? 'none' : 'block'; document.getElementById('group-settings-sheet').style.display = 'block'; } async function confirmLeaveGroup() { hideGroupSettings(); var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'this group'; if (!confirm('Leave ' + groupName + '? You can rejoin with an invite link.')) return; await leaveGroup(); } async function leaveGroup() { if (!window._sb || !currentUser.id || !window._currentGroupId) return; var r = await window._sb.from('group_members') .delete() .eq('group_id', window._currentGroupId) .eq('user_id', currentUser.id); if (r.error) { toast('Could not leave group: ' + r.error.message); return; } var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'group'; toast('You left ' + groupName); groupsData.splice(0, groupsData.length, ...groupsData.filter(function(g) { return g.id !== window._currentGroupId; })); window._currentGroupId = null; window._currentGroupCreatedBy = null; showScreen('groups'); renderGroups(); setTimeout(function() { loadUserGroups(); }, 400); } function hideGroupSettings() { document.getElementById('group-settings-sheet').style.display = 'none'; } function confirmDeleteGroup() { hideGroupSettings(); document.getElementById('group-delete-confirm').style.display = 'block'; } async function deleteGroup() { var groupId = window._currentGroupId; var btn = document.getElementById('delete-group-confirm-btn'); if (!groupId) { toast('No group selected.'); return; } if (btn) { btn.textContent = 'Deleting...'; btn.disabled = true; } if (window._sb && currentUser.id) { try { // Delete group members first (FK constraint), then the group await window._sb.from('group_members').delete().eq('group_id', groupId); } catch(e) {} // ignore if no members try { var res = await window._sb.from('groups').delete().eq('id', groupId).eq('created_by', currentUser.id); if (res.error) { toast('Could not delete group. Try again.'); if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } return; } } catch(e) { toast('Could not delete group. Try again.'); if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } return; } } // Remove from local state var idx = groupsData.findIndex(function(g) { return g.id === groupId; }); if (idx !== -1) groupsData.splice(idx, 1); window._currentGroupId = null; window._currentGroupCreatedBy = null; // Reset button for next delete if (btn) { btn.textContent = 'Yes, Delete Group'; btn.disabled = false; } document.getElementById('group-delete-confirm').style.display = 'none'; renderGroups(); showScreen('groups'); toast('Group deleted.'); } async function showGroupInvite() { var groupId = window._currentGroupId; var link = 'betnbanter.com/join/' + (groupId || 'group'); var linkEl = document.getElementById('invite-link-text'); if (linkEl) linkEl.textContent = link; document.getElementById('group-invite-overlay').style.display = 'block'; // Load real friends into invite row var row = document.getElementById('invite-friends-row'); if (!row) return; row.innerHTML = '
Loading friends...
'; if (!window._sb || !currentUser.id) { row.innerHTML = '
No friends yet — add some first!
'; return; } var results = await Promise.all([ window._sb.from('friends').select('friend_id').eq('user_id', currentUser.id).eq('status', 'accepted'), window._sb.from('friends').select('user_id').eq('friend_id', currentUser.id).eq('status', 'accepted') ]); var seen = new Set(); var friendIds = []; (results[0].data || []).forEach(function(f) { if (!seen.has(f.friend_id)) { seen.add(f.friend_id); friendIds.push(f.friend_id); } }); (results[1].data || []).forEach(function(f) { if (!seen.has(f.user_id)) { seen.add(f.user_id); friendIds.push(f.user_id); } }); var friends = []; if (friendIds.length > 0) { var { data: profData } = await window._sb.from('profiles').select('id, fname, lname').in('id', friendIds); friends = profData || []; } if (friends.length === 0) { row.innerHTML = '
No friends yet — add some first!
'; return; } // Also fetch who is already in the group so we can show them as "Added" var acceptedMembers = new Set(); var pendingMembers = new Set(); if (window._currentGroupId) { try { var emRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: window._currentGroupId }); (emRes.data || []).forEach(function(r) { if (r.status === 'accepted') acceptedMembers.add(r.user_id); else pendingMembers.add(r.user_id); }); } catch(e) {} } row.innerHTML = ''; friends.forEach(function(p) { var name = p.fname + ' ' + (p.lname ? p.lname[0] + '.' : ''); var btn = document.createElement('button'); var isAccepted = acceptedMembers.has(p.id); var isPending = pendingMembers.has(p.id); if (isAccepted) { btn.className = 'inv-chip on'; btn.textContent = name + ' ✓'; btn.disabled = true; btn.style.opacity = '0.5'; btn.style.cursor = 'default'; } else if (isPending) { btn.className = 'inv-chip'; btn.textContent = name + ' (invited)'; btn.disabled = true; btn.style.opacity = '0.6'; btn.style.cursor = 'default'; } else { btn.className = 'inv-chip'; btn.textContent = name; btn.onclick = function() { toggleInv(btn); }; } btn.dataset.friendId = p.id; row.appendChild(btn); }); } async function sendGroupInvites() { var groupId = window._currentGroupId; if (!groupId || !window._sb || !currentUser.id) { closeInviteOverlay(); return; } var selected = document.querySelectorAll('#invite-friends-row .inv-chip.on'); if (selected.length === 0) { toast('Select at least one friend to invite.'); return; } var groupName = document.getElementById('gd-name') ? document.getElementById('gd-name').textContent : 'a group'; var inviterName = (currentUser.fname + ' ' + (currentUser.lname||'')).trim() || 'Someone'; // First check who is already a member so we skip them // Use view to avoid recursive RLS var existingRes = await window._sb.rpc('get_group_member_profiles', { p_group_id: groupId }); var alreadyIn = new Set((existingRes.data || []).map(function(r){ return r.user_id; })); var count = 0; var errors = []; for (var i = 0; i < selected.length; i++) { var fid = selected[i].dataset.friendId; if (!fid) continue; if (alreadyIn.has(fid)) continue; // already a member — skip silently // Insert the group_members row // RLS policy needed: auth.uid() = user_id OR group creator/member can insert for others var r = await window._sb.from('group_members').insert({ group_id: groupId, user_id: fid, status: 'pending' }); if (r.error) { console.warn('group_members invite error:', r.error.message, r.error.code); if (r.error.code === '42501') { errors.push('Permission denied — check group_members INSERT policy in Supabase.'); } else if (r.error.code === '23505') { // Already a member (unique constraint) — not an error count++; } else { errors.push(r.error.message); } continue; } count++; } if (errors.length > 0) { toast(errors[0]); } else if (count > 0) { toast(count + ' friend' + (count !== 1 ? 's' : '') + ' invited to ' + groupName + '!'); } else { toast('No new members to invite.'); } closeInviteOverlay(); await loadUserGroups(); renderGroups(); } // ---- GROUP INVITE ACCEPT / DECLINE ---- function showGroupInvitePrompt(groupId, groupName, inviterName) { var existing = document.getElementById('group-invite-prompt'); if (existing) existing.remove(); var overlay = document.createElement('div'); overlay.id = 'group-invite-prompt'; overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:400;display:flex;align-items:flex-end;justify-content:center;'; var sheet = document.createElement('div'); sheet.onclick = function(e) { e.stopPropagation(); }; sheet.style.cssText = 'width:100%;max-width:480px;background:var(--surface);border-radius:20px 20px 0 0;padding:28px 20px calc(env(safe-area-inset-bottom,0px) + 90px);box-sizing:border-box;'; var handle = document.createElement('div'); handle.style.cssText = 'width:36px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 20px;'; var icon = document.createElement('div'); icon.style.cssText = 'font-size:22px;text-align:center;margin-bottom:8px;'; icon.textContent = '\uD83D\uDC65'; var title = document.createElement('div'); title.style.cssText = 'font-size:18px;font-weight:700;text-align:center;margin-bottom:6px;'; title.textContent = 'Group Invite'; var sub = document.createElement('div'); sub.style.cssText = 'font-size:14px;color:var(--text-2);text-align:center;margin-bottom:24px;'; sub.innerHTML = '' + inviterName + ' invited you to join ' + groupName + ''; var btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;gap:10px;'; var decBtn = document.createElement('button'); decBtn.style.cssText = 'flex:1;padding:13px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text-1);font-family:var(--font);font-size:15px;font-weight:600;cursor:pointer;'; decBtn.textContent = 'Decline'; decBtn.addEventListener('click', function() { declineGroupInvite(groupId); }); var accBtn = document.createElement('button'); accBtn.id = 'gip-accept-btn'; accBtn.style.cssText = 'flex:2;padding:13px;border:none;border-radius:var(--radius);background:var(--green);color:#fff;font-family:var(--font);font-size:15px;font-weight:600;cursor:pointer;'; accBtn.textContent = 'Accept'; accBtn.addEventListener('click', function() { acceptGroupInvite(groupId, groupName); }); btnRow.appendChild(decBtn); btnRow.appendChild(accBtn); sheet.appendChild(handle); sheet.appendChild(icon); sheet.appendChild(title); sheet.appendChild(sub); sheet.appendChild(btnRow); overlay.appendChild(sheet); overlay.addEventListener('click', function() { overlay.remove(); }); document.body.appendChild(overlay); } async function acceptGroupInvite(groupId, groupName) { var prompt = document.getElementById('group-invite-prompt'); var accBtn = document.getElementById('gip-accept-btn'); if (accBtn) { accBtn.textContent = 'Joining...'; accBtn.disabled = true; } if (!window._sb || !currentUser.id) { toast('Not logged in.'); return; } // Update status to accepted var r = await window._sb.from('group_members') .update({ status: 'accepted' }) .eq('group_id', groupId) .eq('user_id', currentUser.id); if (r.error) { console.error('acceptGroupInvite error:', r.error.message, r.error.code); if (r.error.code === '42501') { toast('Permission denied — check group_members UPDATE policy.'); } else { toast('Could not join: ' + r.error.message); } if (accBtn) { accBtn.textContent = 'Accept'; accBtn.disabled = false; } return; } if (prompt) prompt.remove(); toast('You joined ' + groupName + '! 🎉'); // Mark the invite notification as read var nid = 'grp_inv_' + groupId; notifStore.forEach(function(n) { if (n.id === nid) n.read = true; }); saveNotifStore(); saveReadNotifs(); updateNotifBadge(); // Reload groups and navigate await loadUserGroups(); renderGroups(); showScreen('groups'); } async function declineGroupInvite(groupId) { var prompt = document.getElementById('group-invite-prompt'); if (prompt) prompt.remove(); if (!window._sb || !currentUser.id) return; await window._sb.from('group_members').delete().eq('group_id', groupId).eq('user_id', currentUser.id); var nid = 'grp_inv_' + groupId; seenNotifIds.delete(nid); notifStore = notifStore.filter(function(n) { return n.id !== nid; }); saveNotifStore(); saveSeenNotifs(); updateNotifBadge(); renderNotifications(); toast('Invite declined.'); } // ---- END GROUP INVITE ACCEPT / DECLINE ---- function closeInviteOverlay() { document.getElementById('group-invite-overlay').style.display = 'none'; } function copyInviteLink() { var linkEl = document.getElementById('invite-link-text'); var link = linkEl ? linkEl.textContent : ('betnbanter.com/join/' + (window._currentGroupId || 'group')); if (navigator.clipboard) { navigator.clipboard.writeText(link).then(() => toast('Link copied!')).catch(() => toast('Link: ' + link)); } else { toast('Link: ' + link); } } var unreadChatCount = 0; var _chatGroupId = null; // which group's chat is currently open async function loadGroupChat(groupId) { if (!window._sb || !groupId) return; _chatGroupId = groupId; var msgs = document.getElementById('chat-messages'); if (!msgs) return; msgs.innerHTML = ''; var res = await window._sb.from('group_chat') .select('*') .eq('group_id', groupId) .order('created_at', { ascending: true }) .limit(100); if (res.error || !res.data) return; // Fetch profiles separately var chatUids = [...new Set(res.data.map(function(m) { return m.user_id; }))]; var { data: chatProfs } = await window._sb.from('profiles').select('id, fname, lname, username').in('id', chatUids); var chatProfMap = {}; (chatProfs || []).forEach(function(p) { chatProfMap[p.id] = p; }); res.data.forEach(function(m) { var p = chatProfMap[m.user_id] || null; var isMe = m.user_id === currentUser.id; var div = document.createElement('div'); div.className = 'chat-msg ' + (isMe ? 'mine' : 'other'); if (isMe) { div.innerHTML = '
' + (m.message||'') + '
'; } else { var initials = p ? ((p.fname||'U')[0]+(p.lname||'')[0]).toUpperCase() : 'U?'; div.innerHTML = '
' + initials + '
' + (m.message||'') + '
'; } msgs.appendChild(div); }); scrollChat(); } async function sendChat() { const inp = document.getElementById('chat-inp'); const msg = inp.value.trim(); if (!msg) return; inp.value = ''; var groupId = window._currentGroupId; const msgs = document.getElementById('chat-messages'); const div = document.createElement('div'); div.className = 'chat-msg mine'; div.innerHTML = '
' + msg + '
'; msgs.appendChild(div); scrollChat(); if (window._sb && currentUser.id && groupId) { await window._sb.from('group_chat').insert({ group_id: groupId, user_id: currentUser.id, message: msg }); } } function receiveChatMessage(text, avClass, initials) { const msgs = document.getElementById('chat-messages'); if (!msgs) return; const div = document.createElement('div'); div.className = 'chat-msg other'; div.innerHTML = '
' + initials + '
' + text + '
'; msgs.appendChild(div); scrollChat(); // Show badge if chat tab is not currently active const chatTab = document.getElementById('gtab-chat'); if (chatTab && !chatTab.classList.contains('active')) { unreadChatCount++; const badge = document.getElementById('chat-badge'); if (badge) badge.style.display = 'inline-block'; } } function scrollChat() { const msgs = document.getElementById('chat-messages'); if (msgs) setTimeout(() => msgs.scrollTop = msgs.scrollHeight, 50); } function calcGroupPot() { const s = parseFloat(document.getElementById('gbet-stake').value) || 0; const el = document.getElementById('group-pot'); if (el) el.textContent = '$' + (s * 5).toFixed(2); } async function postGroupBet() { const desc = (document.getElementById('gbet-desc') || {}).value || ''; const stake = (document.getElementById('gbet-stake') || {}).value || ''; const trimmedDesc = desc.trim(); if (!trimmedDesc) { toast('Describe the bet first!'); return; } if (!stake || parseFloat(stake) <= 0) { toast('Set a stake amount!'); return; } if (!window._sb || !currentUser.id) { toast('Not logged in.'); return; } if (!window._currentGroupId) { toast('No group selected.'); return; } const btn = document.querySelector('#screen-create-group-bet .btn-primary'); if (btn) { btn.textContent = 'Posting...'; btn.disabled = true; } try { var ins = await window._sb.from('group_posts').insert({ group_id: window._currentGroupId, user_id: currentUser.id, bet_desc: trimmedDesc, platform: 'Group Bet', stake: stake, sport: null, result: 'pending' }); if (ins.error) { console.error('postGroupBet error:', ins.error.message, ins.error.code); if (ins.error.code === '42501') { toast('Permission denied — check group_posts RLS INSERT policy.'); } else { toast('Could not post bet: ' + ins.error.message); } return; } // Clear the form var descEl = document.getElementById('gbet-desc'); var stakeEl = document.getElementById('gbet-stake'); if (descEl) descEl.value = ''; if (stakeEl) stakeEl.value = ''; toast('Group bet posted!'); showScreen('group-detail'); switchGroupTab('bets'); loadGroupBets(window._currentGroupId); loadGroupFeed(window._currentGroupId); } catch(e) { console.error('postGroupBet exception:', e.message); toast('Error: ' + e.message); } finally { if (btn) { btn.textContent = 'Post group bet'; btn.disabled = false; } } } // ── DEMO DATA ── // Feed cards for demo friends function renderDemoFeed() { // No demo data — start fresh for beta const feedEmpty = document.getElementById('feed-empty'); if (feedEmpty) feedEmpty.style.display = 'flex'; } // Balances for demo friends function renderDemoBalances() { // No demo balances — start fresh } // Notifications for demo activity function renderDemoNotifs() { // No demo notifications — start fresh } // Friends list for demo friends function renderDemoFriends() { // No demo friends — start fresh document.getElementById('friends-list-lbl').textContent = 'Your friends'; } // Standings leaderboard with demo data function buildDemoLB() { const you = { av:'av-g', i: currentUser.initials || '?', n:'You', r:'0–0', pl:'$0', pos:true }; return { today:[{...you}], week:[{...you}], month:[{...you}], year:[{...you}], all:[{...you}] }; } // Groups demo data function buildDemoGroups() { return []; } // Group leaderboard demo data function buildDemoGLB() { return { week:[], month:[], all:[] }; } // Bet history for "You" — 1 pending from demo side bet const demoHistData = []; // Suggested friends for the Friends tab const demoSuggested = []; function populateInviteChips() { ['challenge-chips','group-invite-chips'].forEach(id => { const el = document.getElementById(id); if (!el) return; el.innerHTML = demoFriends.map(f => `` ).join(''); }); } function renderDemoSuggested() { const list = document.getElementById('suggested-list'); if (!list) return; list.innerHTML = ''; demoSuggested.forEach(s => { const card = document.createElement('div'); card.className = 'card'; card.style.padding = '12px 14px'; const mutualHtml = s.mutuals.map(m => `${m}`).join(''); card.innerHTML = `
${s.init}
${s.name}
${s.handle} · ${s.mutuals.length} mutual friend${s.mutuals.length>1?'s':''}
${mutualHtml}
`; list.appendChild(card); }); } // init renderObFriends(''); toggleSplashBtns(); initDevBypass(); // ---- BET CREATION SCREENS ---- var _betSelectedFriend = { ff: null, golf: null, custom: null }; function populateBetFriendPicker(prefix) { _betSelectedFriend[prefix] = null; var selEl = document.getElementById(prefix + '-selected-friend'); if (selEl) selEl.style.display = 'none'; var listEl = document.getElementById(prefix + '-friend-list'); var emptyEl = document.getElementById(prefix + '-friend-empty'); var errEl = document.getElementById(prefix + '-error'); if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; } if (!listEl) return; var friends = (typeof friendsData !== 'undefined' ? friendsData : []).filter(function(f){ return f && f.id; }); if (friends.length === 0) { listEl.innerHTML = ''; if (emptyEl) emptyEl.style.display = 'block'; return; } if (emptyEl) emptyEl.style.display = 'none'; listEl.innerHTML = ''; friends.forEach(function(f) { var name = f.name || ((f.fname||'') + ' ' + (f.lname||'')).trim() || 'Friend'; var initials = (name.split(' ').map(function(w){ return w[0]||''; }).join('').toUpperCase()||'F').substring(0,2); var row = document.createElement('div'); row.setAttribute('data-fid', f.id); row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;'; row.innerHTML = '
' + initials + '
' + '
' + name + '
' + '
'; row.addEventListener('click', (function(fObj, fName) { return function() { listEl.querySelectorAll('[data-fid]').forEach(function(r) { r.style.borderColor = 'var(--border)'; r.style.background = ''; var c = r.querySelector('.friend-pick-check'); if (c) { c.style.background = ''; c.style.borderColor = 'var(--border)'; } }); row.style.borderColor = 'var(--green)'; row.style.background = '#f0faf0'; var chk = row.querySelector('.friend-pick-check'); if (chk) { chk.style.background = 'var(--green)'; chk.style.borderColor = 'var(--green)'; } _betSelectedFriend[prefix] = { id: fObj.id, name: fName }; var s = document.getElementById(prefix + '-selected-friend'); if (s) { s.textContent = '✓ Challenging ' + fName; s.style.display = 'block'; } }; })(f, name)); listEl.appendChild(row); }); } async function submitH2HChallenge(prefix) { var sportMap = { ff: 'Fantasy Football', golf: 'Golf', custom: 'Other' }; var descEl = document.getElementById(prefix + '-desc'); var stakeEl = document.getElementById(prefix + '-stake'); var errEl = document.getElementById(prefix + '-error'); var desc = descEl ? descEl.value.trim() : ''; // For Golf/Nassau: sum the Nassau stakes if no single stake field var stake = 0; if (prefix === 'golf' && _golfFormat === 'Nassau') { var f = parseFloat((document.getElementById('nassau-front') || {}).value) || 0; var b = parseFloat((document.getElementById('nassau-back') || {}).value) || 0; var o = parseFloat((document.getElementById('nassau-overall') || {}).value) || 0; stake = f + b + o; } else { stake = stakeEl ? (parseFloat(stakeEl.value) || 0) : 0; } var friend = _betSelectedFriend[prefix]; function showErr(msg) { if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; } else toast(msg); } if (errEl) errEl.style.display = 'none'; if (!desc) { showErr('Please describe the bet.'); return; } if (prefix !== 'custom' && stake <= 0) { showErr('Please enter a stake amount.'); return; } if (!friend) { showErr('Please select a friend to challenge.'); return; } if (!window._sb || !currentUser.id) { showErr('You must be logged in.'); return; } var btn = document.querySelector('#screen-create-' + prefix + '-bet .btn-primary'); if (btn) { btn.textContent = 'Sending...'; btn.disabled = true; } try { var ins = await window._sb.from('h2h_challenges').insert({ initiator_id: currentUser.id, recipient_id: friend.id, description: desc, stake: stake || 0, sport: sportMap[prefix] || 'Other', status: 'pending' }); if (ins.error) throw new Error(ins.error.code === '42501' ? 'Permission denied — check RLS INSERT policy on h2h_challenges.' : ins.error.message); if (descEl) descEl.value = ''; if (stakeEl) stakeEl.value = ''; _betSelectedFriend[prefix] = null; var selEl = document.getElementById(prefix + '-selected-friend'); if (selEl) selEl.style.display = 'none'; toast('Challenge sent to ' + friend.name + '!'); showScreen('bets'); if (typeof loadSentChallenges === 'function') loadSentChallenges(); } catch(e) { console.error('submitH2HChallenge:', e.message); showErr('Error: ' + e.message); if (btn) { btn.textContent = 'Send Challenge'; btn.disabled = false; } } } // ---- END BET CREATION SCREENS ---- // ---- FANTASY FOOTBALL AI SCREENSHOT ANALYSIS ---- var _ffSuggestedStake = null; // stored by AI for Apply button async function handleFFScreenshot(input) { if (!input.files || !input.files[0]) return; var file = input.files[0]; // Show preview image var preview = document.getElementById('ff-preview-img'); var previewWrap = document.getElementById('ff-upload-preview'); var prompt = document.getElementById('ff-upload-prompt'); var loading = document.getElementById('ff-upload-loading'); var zone = document.getElementById('ff-upload-zone'); var reader = new FileReader(); reader.onload = function(e) { if (preview) { preview.src = e.target.result; } if (previewWrap) previewWrap.style.display = 'block'; if (prompt) prompt.style.display = 'none'; }; reader.readAsDataURL(file); // Show loading spinner if (loading) loading.style.display = 'block'; if (zone) zone.style.pointerEvents = 'none'; try { // Convert to base64 for Anthropic API var base64 = await fileToBase64(file); var mediaType = file.type || 'image/jpeg'; // Step 1: Extract matchup data from screenshot var extractRes = await callAnthropicVision(base64, mediaType, 'You are reading a fantasy football matchup screenshot. Extract ONLY valid JSON (no markdown): ' + '{"myTeam":"team name","myProj":142.4,"oppTeam":"opponent name","oppProj":138.1,"week":"Week 8","platform":"ESPN"}. ' + 'Replace values with what you see. Use null for any field not visible. Return only the JSON object, nothing else.' ); var matchupData = null; try { var cleaned = extractRes.replace(/```json|```/g, '').trim(); matchupData = JSON.parse(cleaned); } catch(e) { console.warn('Parse error:', e.message, extractRes); } // Fill form fields if we got data if (matchupData) { ffDescEdited = false; // allow auto-rebuild if (matchupData.myTeam) { var el = document.getElementById('ff-my-team'); if(el) el.value = matchupData.myTeam; } if (matchupData.myProj) { var el = document.getElementById('ff-my-proj'); if(el) el.value = matchupData.myProj; } if (matchupData.oppTeam) { var el = document.getElementById('ff-opp-team'); if(el) el.value = matchupData.oppTeam; } if (matchupData.oppProj) { var el = document.getElementById('ff-opp-proj'); if(el) el.value = matchupData.oppProj; } if (matchupData.week) { var wk = document.getElementById('ff-week'); if (wk) { for (var i = 0; i < wk.options.length; i++) { if (wk.options[i].text === matchupData.week) { wk.selectedIndex = i; break; } } } } if (matchupData.platform) { var pl = document.getElementById('ff-platform'); if (pl) { for (var i = 0; i < pl.options.length; i++) { if (pl.options[i].text.toLowerCase() === (matchupData.platform||'').toLowerCase()) { pl.selectedIndex = i; break; } } } } ffBuildDesc(); } // Step 2: Generate fair odds from the projections var myProj = matchupData && matchupData.myProj ? parseFloat(matchupData.myProj) : null; var oppProj = matchupData && matchupData.oppProj ? parseFloat(matchupData.oppProj) : null; var myTeam = matchupData && matchupData.myTeam ? matchupData.myTeam : 'Your team'; var oppTeam = matchupData && matchupData.oppTeam ? matchupData.oppTeam : 'Their team'; var oddsPrompt = 'You are a fantasy football betting analyst. Suggest fair betting odds and a recommended stake for this matchup. ' + 'Matchup: ' + myTeam + ' (proj. ' + (myProj||'unknown') + ' pts) vs ' + oppTeam + ' (proj. ' + (oppProj||'unknown') + ' pts). ' + 'Provide 5 short lines: 1) Who is favoured and by how much. 2) Fair money line odds (e.g. -130 / +110). ' + '3) Point spread (e.g. -6.5). 4) Recommended flat stake (e.g. $20 straight up, or handicap if lopsided). ' + '5) One sentence: competitive or lopsided? Be concise. No markdown headers.'; var oddsText = await callAnthropicText(oddsPrompt); // Show odds box var oddsBox = document.getElementById('ff-odds-box'); var oddsContent = document.getElementById('ff-odds-content'); if (oddsBox) oddsBox.style.display = 'block'; if (oddsContent) oddsContent.textContent = oddsText; // Try to extract a suggested stake number for the Apply button var stakeMatch = oddsText.match(/\$(\d+)/); _ffSuggestedStake = stakeMatch ? stakeMatch[1] : null; toast('Matchup analysed! Check the AI odds below.'); } catch(e) { console.error('FF screenshot error:', e.message); toast('Could not read screenshot: ' + e.message); // Reset upload zone if (prompt) prompt.style.display = 'block'; if (previewWrap) previewWrap.style.display = 'none'; } finally { if (loading) loading.style.display = 'none'; if (zone) zone.style.pointerEvents = 'auto'; // Reset input so same file can be re-selected input.value = ''; } } function ffApplyOdds() { if (_ffSuggestedStake) { var stakeEl = document.getElementById('ff-stake'); if (stakeEl) { stakeEl.value = _ffSuggestedStake; toast('Stake set to $' + _ffSuggestedStake); } } } function fileToBase64(file) { return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function(e) { // Strip the data:image/xxx;base64, prefix var result = e.target.result; var base64 = result.split(',')[1]; resolve(base64); }; reader.onerror = reject; reader.readAsDataURL(file); }); } async function callAnthropicVision(base64, mediaType, prompt) { var response = await fetch(CLAUDE_PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY }, body: JSON.stringify({ model: 'claude-opus-4-5', max_tokens: 512, messages: [{ role: 'user', content: [ { type: 'image', source: { type: 'base64', media_type: mediaType, data: base64 } }, { type: 'text', text: prompt } ] }] }) }); if (!response.ok) { var err = await response.json().catch(function(){ return {}; }); throw new Error(err.error && err.error.message ? err.error.message : 'Proxy error ' + response.status); } var data = await response.json(); return data.content && data.content[0] ? data.content[0].text : ''; } async function callAnthropicText(prompt) { var response = await fetch(CLAUDE_PROXY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 400, messages: [{ role: 'user', content: prompt }] }) }); if (!response.ok) { var err = await response.json().catch(function(){ return {}; }); throw new Error(err.error && err.error.message ? err.error.message : 'Proxy error ' + response.status); } var data = await response.json(); return data.content && data.content[0] ? data.content[0].text : ''; } // ---- END FANTASY FOOTBALL AI ---- // ---- FF + GOLF AUTO-DESC BUILDERS ---- var ffDescEdited = false; var golfDescEdited = false; var _golfFormat = 'Stroke Play'; function ffBuildDesc() { if (ffDescEdited) return; // user manually edited — don't overwrite var platform = (document.getElementById('ff-platform') || {}).value || 'Fantasy'; var week = (document.getElementById('ff-week') || {}).value || ''; var myTeam = ((document.getElementById('ff-my-team') || {}).value || '').trim(); var myProj = ((document.getElementById('ff-my-proj') || {}).value || '').trim(); var oppTeam = ((document.getElementById('ff-opp-team') || {}).value || '').trim(); var oppProj = ((document.getElementById('ff-opp-proj') || {}).value || '').trim(); var parts = []; if (platform && week) parts.push(platform + ' ' + week + ' matchup'); else if (week) parts.push(week + ' matchup'); else if (platform) parts.push(platform + ' matchup'); else parts.push('Fantasy Football matchup'); if (myTeam && oppTeam) { var scoreStr = (myProj && oppProj) ? ' (proj. ' + myProj + '–' + oppProj + ')' : ''; parts.push(myTeam + ' vs ' + oppTeam + scoreStr); } else if (myTeam) { parts.push('My team: ' + myTeam + (myProj ? ' (proj. ' + myProj + ')' : '')); } parts.push('Loser pays up.'); var el = document.getElementById('ff-desc'); if (el) el.value = parts.join('. '); } function selectGolfFormat(btn, fmt) { _golfFormat = fmt; document.querySelectorAll('#golf-format-chips .inv-chip').forEach(function(b) { b.classList.remove('on'); }); btn.classList.add('on'); // Show/hide Nassau section var nassau = document.getElementById('nassau-section'); var stakeSection = document.getElementById('golf-stake-section'); if (nassau) nassau.style.display = (fmt === 'Nassau') ? 'block' : 'none'; if (stakeSection) stakeSection.style.display = (fmt === 'Nassau') ? 'none' : 'block'; golfBuildDesc(); } function golfBuildDesc() { if (golfDescEdited) return; var course = ((document.getElementById('golf-course') || {}).value || '').trim(); var dateVal = ((document.getElementById('golf-date') || {}).value || '').trim(); var holes = ((document.getElementById('golf-holes') || {}).value || '18'); var handicap = ((document.getElementById('golf-handicap') || {}).value || 'gross'); var stake = ((document.getElementById('golf-stake') || {}).value || '').trim(); var dateStr = ''; if (dateVal) { try { dateStr = new Date(dateVal + 'T12:00:00').toLocaleDateString('en-US', { weekday:'short', month:'short', day:'numeric' }); } catch(e) { dateStr = dateVal; } } var parts = []; var formatLabel = _golfFormat || 'Stroke Play'; var holesLabel = holes === '18' ? '18 holes' : holes + ' holes'; var capLabel = handicap === 'net' ? 'net (with handicaps)' : 'gross (no handicaps)'; if (course) parts.push(holesLabel + ' ' + formatLabel + ' at ' + course); else parts.push(holesLabel + ' ' + formatLabel); if (dateStr) parts.push(dateStr); parts.push(capLabel.charAt(0).toUpperCase() + capLabel.slice(1)); if (formatLabel === 'Nassau') { var front = ((document.getElementById('nassau-front') || {}).value || '').trim(); var back = ((document.getElementById('nassau-back') || {}).value || '').trim(); var overall = ((document.getElementById('nassau-overall') || {}).value || '').trim(); if (front || back || overall) { parts.push('Nassau: $' + (front||'?') + ' front / $' + (back||'?') + ' back / $' + (overall||'?') + ' overall'); } } else if (stake) { parts.push('$' + stake + ' match'); } var el = document.getElementById('golf-desc'); if (el) el.value = parts.join('. ') + '.'; } // Reset edited flags and re-init golf format when screens open var _origShowScreen = showScreen; // Patch showScreen to reset form state when navigating to bet screens (function() { var _orig = showScreen; showScreen = function(id) { if (id === 'create-ff-bet') { ffDescEdited = false; } if (id === 'create-golf-bet') { golfDescEdited = false; _golfFormat = 'Stroke Play'; // Reset format chips setTimeout(function() { document.querySelectorAll('#golf-format-chips .inv-chip').forEach(function(b) { b.classList.toggle('on', b.getAttribute('data-fmt') === 'Stroke Play'); }); var nassau = document.getElementById('nassau-section'); var stakeSection = document.getElementById('golf-stake-section'); if (nassau) nassau.style.display = 'none'; if (stakeSection) stakeSection.style.display = 'block'; }, 0); } _orig(id); }; })(); // ---- END FF + GOLF AUTO-DESC BUILDERS ----