diff --git a/api/channels.mjs b/api/channels.mjs new file mode 100644 index 0000000..328eddb --- /dev/null +++ b/api/channels.mjs @@ -0,0 +1,57 @@ +import { createClient } from '@supabase/supabase-js'; + +export default async function handler(req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') return res.status(200).end(); + if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' }); + + const url = process.env.QWICKY_SUPABASE_URL; + const key = process.env.QWICKY_SUPABASE_SERVICE_KEY; + + if (!url || !key) { + return res.status(500).json({ error: 'Missing QWICKY_SUPABASE_URL or QWICKY_SUPABASE_SERVICE_KEY env vars' }); + } + + try { + const supabase = createClient(url, key); + + // Fetch all registered channels + const { data: channels, error: chErr } = await supabase + .from('tournament_channels') + .select('*') + .order('tournament_id'); + + if (chErr) throw chErr; + + // Fetch latest submission per tournament_id (ordered desc so first match = latest) + const { data: submissions, error: subErr } = await supabase + .from('match_submissions') + .select('tournament_id, created_at, game_data') + .order('created_at', { ascending: false }); + + if (subErr) throw subErr; + + // Build map: tournament_id -> latest submission + const latestByTournament = {}; + for (const sub of submissions) { + if (!latestByTournament[sub.tournament_id]) { + latestByTournament[sub.tournament_id] = sub; + } + } + + // Enrich channels with latest submission info + const enriched = channels.map(ch => ({ + ...ch, + latest_submission_at: latestByTournament[ch.tournament_id]?.created_at || null, + latest_game_date: latestByTournament[ch.tournament_id]?.game_data?.date || null, + })); + + return res.json({ channels: enriched }); + } catch (err) { + console.error('Error fetching channels:', err); + return res.status(500).json({ error: 'Failed to fetch channels', details: err.message }); + } +} diff --git a/src/components/TournamentInfo.jsx b/src/components/TournamentInfo.jsx index 53fa9d3..13cb2e3 100644 --- a/src/components/TournamentInfo.jsx +++ b/src/components/TournamentInfo.jsx @@ -1,5 +1,5 @@ // src/components/TournamentInfo.jsx -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { calculateStandings } from './division/DivisionStandings'; const countryToFlag = (code) => { @@ -131,10 +131,32 @@ function DivisionStandingsCard({ division, onNavigate }) { export default function TournamentInfo({ tournament, updateTournament, onNavigateToDivision }) { const [copiedField, setCopiedField] = useState(null); const [showSettings, setShowSettings] = useState(false); + const [channels, setChannels] = useState(null); + const [channelsLoading, setChannelsLoading] = useState(false); + const [channelsError, setChannelsError] = useState(null); const tournamentSlug = (tournament.name || 'my-tournament') .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + const loadChannels = useCallback(async () => { + setChannelsLoading(true); + setChannelsError(null); + try { + const res = await fetch('/api/channels'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + setChannels(data.channels || []); + } catch (err) { + setChannelsError(err.message); + } finally { + setChannelsLoading(false); + } + }, []); + + useEffect(() => { + loadChannels(); + }, [loadChannels]); + const botInviteUrl = 'https://discord.com/oauth2/authorize?client_id=1469479991929733140&permissions=83968&integration_type=0&scope=bot+applications.commands'; const copyToClipboard = (text, field) => { @@ -291,6 +313,93 @@ export default function TournamentInfo({ tournament, updateTournament, onNavigat
  • Review submissions in the Results tab of each division
  • + + {/* Registered Channels status */} +
    +
    + + +
    + + {channelsError && ( +

    Error: {channelsError}

    + )} + + {!channelsError && channels !== null && channels.length === 0 && ( +

    No channels registered yet.

    + )} + + {!channelsError && channels !== null && channels.length > 0 && ( +
    + + + + + + + + + + + + {channels.map(ch => { + const isCurrent = ch.tournament_id === tournamentSlug; + const lastDate = ch.latest_submission_at; + const gameDate = ch.latest_game_date + ? ch.latest_game_date.split(' ')[0] + : null; + const daysAgo = lastDate + ? Math.floor((Date.now() - new Date(lastDate).getTime()) / 86400000) + : null; + const activityColor = + daysAgo === null ? 'text-qw-muted' : + daysAgo > 30 ? 'text-qw-loss' : + daysAgo > 14 ? 'text-yellow-400' : + 'text-qw-win'; + const activityLabel = + daysAgo === null ? 'never' : + daysAgo === 0 ? 'today' : + `${daysAgo}d ago`; + + return ( + + + + + + + + ); + })} + +
    TournamentDivisionChannelLast gameActivity
    + {isCurrent && } + {ch.tournament_id} + + {ch.division_id || '—'} + + …{ch.discord_channel_id.slice(-7)} + + {gameDate || '—'} + + {activityLabel} +
    +
    + )} + + {channelsLoading && channels === null && ( +

    Fetching channels…

    + )} +
    diff --git a/src/components/division/DivisionResults.jsx b/src/components/division/DivisionResults.jsx index 4276bbc..40533e8 100644 --- a/src/components/division/DivisionResults.jsx +++ b/src/components/division/DivisionResults.jsx @@ -1041,6 +1041,12 @@ export default function DivisionResults({ division, updateDivision, updateAnyDiv
    + {gameData.date && ( + <> + 📅 {gameData.date.replace(/\s*\+\d{4}$/, '').trim()} + {' '}·{' '} + + )} Submitted by {sub.submitted_by_name} {' '}·{' '} {new Date(sub.created_at).toLocaleString()}