From eb2db1b834ad345eef03b6d453d153397006e476 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 20:31:48 +0000 Subject: [PATCH 1/2] Add channel status view to Discord integration panel - New GET /api/channels endpoint queries tournament_channels table and enriches each row with the latest submission date from match_submissions - TournamentInfo Discord Integration section now shows a Registered Channels table with tournament ID, division, channel tail, last game date, and days-ago activity indicator (green/yellow/red) - Current tournament's channel is highlighted with a green dot - Table auto-loads on mount with a manual Refresh button https://claude.ai/code/session_01TPpkZUCnqL8e7DAEqMbsN2 --- api/channels.mjs | 57 +++++++++++++++ src/components/TournamentInfo.jsx | 111 +++++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 api/channels.mjs 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…

    + )} +
    From 16cdefd205055581e536c08711e2bf569d77234b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 10:36:48 +0000 Subject: [PATCH 2/2] Show game date on Discord submission cards Each Discord submission now displays the actual game date/time from the ktxstats JSON (gameData.date) alongside the submission timestamp, so admins can see when the match was played rather than just when it was submitted to Discord. The timezone offset (+0000) is stripped for cleaner display. https://claude.ai/code/session_01TPpkZUCnqL8e7DAEqMbsN2 --- src/components/division/DivisionResults.jsx | 6 ++++++ 1 file changed, 6 insertions(+) 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()}