Files
memory-infrastructure-palace/code/websites/pokedex.online/src/views/ChallongeTest.vue
FragginWagon 1944b43af8 feat: implement multi-state tournament querying for Challonge API v2.1
- Add tournament-query.js utility with queryAllTournaments() and helper functions
  * Makes 3 parallel API calls (pending, in_progress, ended states)
  * Uses Promise.all() to wait for all requests
  * Deduplicates results by tournament ID using Map
  * Replaces invalid state: 'all' parameter (API doesn't support 'all' value)

- Implement 5 convenience functions:
  * queryAllTournaments() - Query all states with custom options
  * queryUserTournaments() - Query user's tournaments (shorthand)
  * queryCommunityTournaments() - Query community tournaments
  * queryActiveTournaments() - Query pending + in_progress only
  * queryCompletedTournaments() - Query ended tournaments only
  * queryTournamentsByStates() - Query custom state combinations

- Update ChallongeTest.vue to use queryAllTournaments()
  * Replace invalid state: 'all' with proper multi-state query
  * Now correctly fetches tournaments from all states
  * Update console logging to show all 3 states being queried

- Add comprehensive TOURNAMENT_QUERY_GUIDE.md documentation
  * Explains the problem and solution
  * API reference for all functions
  * Implementation details and performance notes
  * Testing instructions
  * Future enhancement ideas
2026-01-28 18:10:29 +00:00

1615 lines
34 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="challonge-test">
<div class="container">
<div class="header">
<router-link to="/" class="back-button"> Back Home </router-link>
<h1>Challonge API Test</h1>
</div>
<p class="description">
Test your Challonge API connection and verify your configuration.
</p>
<!-- API Version & Settings Controls -->
<div v-if="apiKey" class="section controls-section">
<div class="controls-grid">
<!-- API Version Selector -->
<div class="control-group">
<label class="control-label">API Version:</label>
<div class="radio-group">
<label class="radio-option">
<input type="radio" v-model="apiVersion" value="v1" />
<span>v1 (Legacy)</span>
</label>
<label class="radio-option">
<input type="radio" v-model="apiVersion" value="v2.1" />
<span>v2.1 (Current)</span>
</label>
</div>
<span
class="version-badge"
:class="'badge-' + apiVersion.replace('.', '-')"
>
Using API {{ apiVersion }}
</span>
</div>
<!-- Results Per Page (v2.1 only) -->
<div v-if="apiVersion === 'v2.1'" class="control-group">
<label class="control-label">Results per page:</label>
<select
v-model.number="perPage"
@change="changePerPage(perPage)"
class="select-input"
>
<option :value="10">10</option>
<option :value="25">25</option>
<option :value="50">50</option>
<option :value="100">100</option>
</select>
</div>
<!-- Tournament Scope (v2.1 only) -->
<div v-if="apiVersion === 'v2.1'" class="control-group">
<div class="info-badge" v-if="isAuthenticated">
OAuth Connected - showing created and admin tournaments
</div>
<div class="info-badge warning" v-else>
API Key Mode - showing only created tournaments
</div>
<span class="scope-hint">
Shows tournaments you created and tournaments where you're an admin
</span>
</div>
<!-- OAuth Authentication (v2.1 only) -->
<div v-if="apiVersion === 'v2.1'" class="control-group oauth-section">
<label class="control-label">OAuth Authentication:</label>
<div v-if="isAuthenticated" class="oauth-status">
<span class="status-badge status-connected">✓ Connected</span>
<button @click="oauthLogout" class="btn btn-secondary btn-sm">
Logout
</button>
</div>
<div v-else class="oauth-status">
<span class="status-badge status-disconnected"
>○ Not Connected</span
>
<button
@click="oauthLogin('me tournaments:read tournaments:write')"
class="btn btn-primary btn-sm"
:disabled="oauthLoading"
>
{{ oauthLoading ? 'Connecting...' : 'Connect with OAuth' }}
</button>
</div>
<span class="oauth-hint">
{{
isAuthenticated
? 'Using OAuth - APPLICATION scope available'
: 'Connect to enable APPLICATION scope'
}}
</span>
</div>
</div>
</div>
<!-- No API Key Stored -->
<div v-if="!apiKey" class="section warning-section no-key-section">
<div class="warning-content">
<h2>⚠️ No API Key Found</h2>
<p class="warning-text">
Please store your Challonge API key in the API Key Manager to get
started.
</p>
<router-link to="/api-key-manager" class="btn btn-primary btn-lg">
Go to API Key Manager
</router-link>
<p class="hint-text">
The API Key Manager securely stores your key in your browser. Set it
up once and use it across all tools.
</p>
</div>
</div>
<div v-if="apiKey" class="section">
<h2>1. API Key Configuration</h2>
<div class="status success">✅ API Key Loaded: {{ maskedApiKey }}</div>
<router-link to="/api-key-manager" class="btn-link">
Manage your API key
</router-link>
<p class="api-note">💡 Your API v1 key works with both API versions</p>
</div>
<!-- List Tournaments Test -->
<div v-if="apiKey" class="section">
<h2>2. Test API Connection</h2>
<button
@click="testListTournaments()"
:disabled="loading"
class="btn btn-primary"
>
{{ loading ? 'Loading...' : 'List My Tournaments' }}
</button>
<div v-if="error" class="error-box">
<strong>Errors:</strong>
<ul class="error-list">
<li
v-for="(err, index) in Array.isArray(error) ? error : [error]"
:key="index"
>
<span class="error-status">{{ err.status || 'Error' }}:</span>
<span class="error-message">{{
err.message || err.detail || err
}}</span>
<span v-if="err.field" class="error-field"
>({{ err.field }})</span
>
</li>
</ul>
<div class="help-text">
<p>Common issues:</p>
<ul>
<li>
<strong>401 Unauthorized:</strong> Invalid API key - check API
Key Manager
</li>
<li><strong>403 Forbidden:</strong> Insufficient permissions</li>
<li>
<strong>404 Not Found:</strong> Tournament may have been deleted
</li>
<li>
<strong>Network Error:</strong> Connection problems or CORS
issues
</li>
</ul>
</div>
</div>
<div v-if="tournaments" class="results">
<div class="results-header">
<h3>✅ Success!</h3>
<p class="pagination-info">{{ paginationInfo }}</p>
</div>
<div v-if="tournaments.length === 0" class="info-box">
No tournaments found. Create one at
<a href="https://challonge.com" target="_blank">Challonge.com</a>
</div>
<div v-else>
<!-- Search Filter -->
<div class="search-box">
<input
v-model="searchQuery"
type="text"
placeholder="🔍 Search tournaments by name (client-side)..."
class="search-input"
/>
<span v-if="searchQuery" class="search-info">
Showing {{ filteredTournaments.length }} of
{{ tournaments.length }} tournaments
</span>
</div>
<div class="tournament-list">
<div
v-for="tournament in filteredTournaments"
:key="getTournamentId(tournament)"
class="tournament-card"
>
<div class="tournament-header">
<h4>{{ getTournamentName(tournament) }}</h4>
<span
class="tournament-state"
:class="getTournamentProp(tournament, 'state')"
>
{{ getTournamentProp(tournament, 'state') }}
</span>
</div>
<div class="tournament-details">
<p>
<strong>URL:</strong>
{{ getTournamentProp(tournament, 'url') }}
</p>
<p>
<strong>Type:</strong>
{{ getTournamentProp(tournament, 'tournament_type') }}
</p>
<p>
<strong>Participants:</strong>
{{ getTournamentProp(tournament, 'participants_count') }}
</p>
<p v-if="getTournamentProp(tournament, 'started_at')">
<strong>Started:</strong>
{{
formatDate(getTournamentProp(tournament, 'started_at'))
}}
</p>
</div>
<button
@click="toggleTournamentDetails(getTournamentId(tournament))"
class="btn btn-small"
:class="{
'btn-active':
expandedTournamentId === getTournamentId(tournament)
}"
>
{{
expandedTournamentId === getTournamentId(tournament)
? 'Hide Details'
: 'Load Details'
}}
</button>
<!-- Collapsible Details Section -->
<div
v-if="
expandedTournamentId === getTournamentId(tournament) &&
tournamentDetails
"
class="tournament-details-inline"
>
<div class="details-header">
<h4>Full Tournament Details</h4>
</div>
<pre class="details-content">{{
JSON.stringify(tournamentDetails, null, 2)
}}</pre>
</div>
</div>
</div>
<!-- Load More Button (v2.1 only) -->
<div
v-if="apiVersion === 'v2.1' && hasNextPage"
class="load-more-section"
>
<button
@click="loadMoreTournaments"
:disabled="loadingMore"
class="btn btn-secondary"
>
{{ loadingMore ? 'Loading...' : 'Load More Tournaments' }}
</button>
</div>
</div>
</div>
</div>
<!-- Configuration Instructions -->
<div v-if="!apiKey" class="section info-section">
<h2> How to Set Up Your API Key</h2>
<div class="info-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h3>Get Your API Key</h3>
<p>
Visit
<a
href="https://challonge.com/settings/developer"
target="_blank"
>
Challonge Developer Settings
</a>
and copy your API key
</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h3>Store in API Key Manager</h3>
<p>
Go to the API Key Manager and paste your key. It will be saved
securely in your browser.
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h3>Start Testing</h3>
<p>
Return to this page and test your API connection with the stored
key.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useChallongeApiKey } from '../composables/useChallongeApiKey.js';
import { useChallongeOAuth } from '../composables/useChallongeOAuth.js';
import {
createChallongeV1Client,
createChallongeV2Client,
AuthType,
ScopeType
} from '../services/challonge.service.js';
import { queryAllTournaments } from '../utilities/tournament-query.js';
const { getApiKey } = useChallongeApiKey();
const {
isAuthenticated,
accessToken,
login: oauthLogin,
logout: oauthLogout,
loading: oauthLoading
} = useChallongeOAuth();
// API Configuration
const apiVersion = ref('v2.1'); // 'v1' or 'v2.1'
// State
const loading = ref(false);
const loadingMore = ref(false);
const error = ref(null);
const tournaments = ref(null);
const searchQuery = ref('');
const expandedTournamentId = ref(null);
const tournamentDetails = ref(null);
// Pagination
const currentPage = ref(1);
const perPage = ref(10);
const totalTournaments = ref(0);
const hasNextPage = ref(false);
// v2.1 Tournament Scope
const showAllTournaments = ref(false); // Show all tournaments vs user-only (requires OAuth)
// Debug mode (can be enabled via localStorage.setItem('DEBUG_CHALLONGE', 'true'))
const debugMode = ref(false);
// Make apiKey reactive
const apiKey = computed(() => getApiKey());
const maskedApiKey = computed(() => {
if (!apiKey.value) return '';
return apiKey.value.slice(0, 4) + '' + apiKey.value.slice(-4);
});
// Create API client reactively based on version, key, and OAuth status
const client = computed(() => {
if (apiVersion.value === 'v1') {
// v1 only supports API key
if (!apiKey.value) return null;
return createChallongeV1Client(apiKey.value);
} else {
// v2.1 supports both OAuth and API key
if (isAuthenticated.value && accessToken.value) {
// Use OAuth token if authenticated
return createChallongeV2Client(
{ token: accessToken.value, type: AuthType.OAUTH },
{ debug: debugMode.value }
);
} else if (apiKey.value) {
// Fall back to API key
return createChallongeV2Client(
{ token: apiKey.value, type: AuthType.API_KEY },
{ debug: debugMode.value }
);
}
return null;
}
});
// Pagination info
const paginationInfo = computed(() => {
if (!tournaments.value) return '';
const start = (currentPage.value - 1) * perPage.value + 1;
const end = Math.min(
start + tournaments.value.length - 1,
totalTournaments.value || tournaments.value.length
);
const total = totalTournaments.value || tournaments.value.length;
return `Showing ${start}-${end} of ${total}`;
});
// Filter tournaments (client-side for now, can be moved to server-side)
const filteredTournaments = computed(() => {
if (!tournaments.value) return null;
if (!searchQuery.value.trim()) return tournaments.value;
const query = searchQuery.value.toLowerCase();
return tournaments.value.filter(t => {
const name = getTournamentName(t).toLowerCase();
return name.includes(query);
});
});
// Helper to get tournament name (handles both v1 and v2.1 response structures)
function getTournamentName(tournament) {
return tournament.tournament?.name || tournament.name || '';
}
// Helper to get tournament ID
function getTournamentId(tournament) {
return tournament.tournament?.id || tournament.id;
}
// Helper to get tournament property
function getTournamentProp(tournament, prop) {
return tournament.tournament?.[prop] || tournament[prop];
}
async function testListTournaments(resetPagination = true) {
loading.value = true;
error.value = null;
if (resetPagination) {
currentPage.value = 1;
tournaments.value = null;
searchQuery.value = '';
expandedTournamentId.value = null;
tournamentDetails.value = null;
}
try {
if (apiVersion.value === 'v1') {
// v1 doesn't support pagination
const result = await client.value.tournaments.list();
tournaments.value = result;
totalTournaments.value = result.length;
hasNextPage.value = false;
} else {
// v2.1 - Query all tournament states (pending, in_progress, ended) in parallel
// USER scope returns tournaments you have access to:
// - Tournaments you created
// - Tournaments where you're added as an admin
const result = await queryAllTournaments(client.value, {
page: currentPage.value,
per_page: 100,
scopeType: ScopeType.USER
});
console.log('📊 Tournament API Response (All States):', {
page: currentPage.value,
perPage: 100,
scope: 'USER',
states: ['pending', 'in_progress', 'ended'],
resultsCount: result.length,
isAuthenticated: isAuthenticated.value,
authType: isAuthenticated.value ? 'OAuth' : 'API Key',
results: result
});
tournaments.value = result;
totalTournaments.value = result.length;
hasNextPage.value = result.length >= 100;
}
} catch (err) {
handleError(err);
} finally {
loading.value = false;
}
}
async function loadMoreTournaments() {
if (apiVersion.value === 'v1') return; // v1 doesn't support pagination
loadingMore.value = true;
currentPage.value++;
try {
const result = await queryAllTournaments(client.value, {
page: currentPage.value,
per_page: 100,
scopeType: ScopeType.USER
});
tournaments.value = [...tournaments.value, ...result];
hasNextPage.value = result.length === perPage.value;
} catch (err) {
currentPage.value--; // Revert on error
handleError(err);
} finally {
loadingMore.value = false;
}
}
async function changePerPage(newLimit) {
perPage.value = newLimit;
await testListTournaments(true);
}
async function toggleTournamentDetails(tournamentId) {
if (expandedTournamentId.value === tournamentId) {
expandedTournamentId.value = null;
tournamentDetails.value = null;
return;
}
expandedTournamentId.value = tournamentId;
tournamentDetails.value = null;
try {
if (apiVersion.value === 'v1') {
const result = await client.value.tournaments.get(tournamentId, {
includeParticipants: true,
includeMatches: true
});
tournamentDetails.value = result;
} else {
// v2.1 get tournament
const result = await client.value.tournaments.get(tournamentId);
tournamentDetails.value = result;
}
} catch (err) {
handleError(err);
expandedTournamentId.value = null;
}
}
function handleError(err) {
console.error('Challonge API Error:', err);
if (err.errors && Array.isArray(err.errors)) {
// JSON:API error format (v2.1) - already formatted
error.value = err.errors;
} else if (err.status) {
// HTTP error with status code
error.value = [
{
status: err.status,
message: err.message || 'Unknown error',
field: null
}
];
} else if (err.message) {
// Generic error with message
error.value = [
{
status: 'Error',
message: err.message,
field: null
}
];
} else {
// Fallback for unknown error formats
error.value = [
{
status: 'Error',
message: 'An unexpected error occurred. Check console for details.',
field: null
}
];
}
}
async function switchApiVersion() {
// Clear state when switching versions
tournaments.value = null;
error.value = null;
searchQuery.value = '';
expandedTournamentId.value = null;
tournamentDetails.value = null;
currentPage.value = 1;
}
function formatDate(dateString) {
if (!dateString) return '';
return new Date(dateString).toLocaleString();
}
// Watch for API version changes
watch(apiVersion, switchApiVersion);
// Check for debug mode on mount
onMounted(() => {
debugMode.value = localStorage.getItem('DEBUG_CHALLONGE') === 'true';
});
</script>
<style scoped>
.challonge-test {
min-height: 100vh;
padding: 2rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.back-button {
padding: 0.5rem 1rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
display: inline-block;
}
.back-button:hover {
background: #5568d3;
transform: translateX(-2px);
}
h1 {
color: #333;
margin: 0;
font-size: 2.5rem;
}
.description {
color: #666;
font-size: 1.1rem;
margin-bottom: 2rem;
}
.section {
margin: 2rem 0;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.section.warning-section {
background: #fef3c7;
border: 2px solid #fcd34d;
}
.no-key-section {
text-align: center;
padding: 3rem 2rem;
}
.warning-content h2 {
font-size: 1.75rem;
color: #92400e;
margin-bottom: 0.5rem;
}
.warning-text {
font-size: 1.1rem;
color: #78350f;
margin-bottom: 1.5rem;
font-weight: 500;
}
.btn-lg {
padding: 0.875rem 2rem;
font-size: 1.05rem;
display: inline-block;
margin: 0 auto 1.5rem;
}
.hint-text {
font-size: 0.95rem;
color: #78350f;
margin: 0;
font-style: italic;
opacity: 0.9;
}
h2 {
color: #495057;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.status {
padding: 1rem;
border-radius: 6px;
font-weight: 500;
}
.status.success {
background: #d1fae5;
color: #065f46;
border-left: 4px solid #10b981;
}
.status.error {
background: #fee;
color: #c33;
border-left: 4px solid #c33;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-small {
padding: 0.5rem 1rem;
font-size: 0.875rem;
background: #667eea;
color: white;
margin-top: 0.5rem;
transition: all 0.3s ease;
}
.btn-small:hover {
background: #5568d3;
}
.btn-small.btn-active {
background: #f59e0b;
}
.btn-small.btn-active:hover {
background: #d97706;
}
.tournament-details-inline {
margin-top: 1rem;
padding-top: 1rem;
border-top: 2px solid #e9ecef;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 2000px;
transform: translateY(0);
}
}
.details-header {
margin-bottom: 0.75rem;
}
.details-header h4 {
color: #667eea;
margin: 0;
font-size: 1.1rem;
}
.details-content {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.5;
max-height: 500px;
overflow-y: auto;
border: 1px solid #e9ecef;
}
.btn-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 0;
border-bottom: 2px solid #667eea;
transition: all 0.3s ease;
}
.btn-link:hover {
color: #5568d3;
border-bottom-color: #5568d3;
}
.error-box {
margin-top: 1rem;
padding: 1rem;
background: #fee;
color: #c33;
border-radius: 6px;
border-left: 4px solid #c33;
}
.info-box {
margin-top: 1rem;
padding: 1rem;
background: #e7f3ff;
color: #0c4a6e;
border-radius: 6px;
border-left: 4px solid #0284c7;
}
.results {
margin-top: 1rem;
}
.results h3 {
color: #065f46;
margin-bottom: 1rem;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.results-header h3 {
margin: 0;
}
.pagination-info {
color: #666;
font-size: 0.95rem;
margin: 0;
}
/* API Controls Section */
.controls-section {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 2px solid #667eea;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-label {
font-weight: 600;
color: #495057;
font-size: 0.95rem;
}
.radio-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.radio-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.5rem 1rem;
background: white;
border: 2px solid #dee2e6;
border-radius: 6px;
transition: all 0.3s ease;
}
.radio-option:hover {
border-color: #667eea;
background: #f8f9ff;
}
.radio-option input[type='radio'] {
cursor: pointer;
width: 18px;
height: 18px;
}
.radio-option span {
font-weight: 500;
color: #495057;
}
.version-badge {
display: inline-block;
padding: 0.375rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
margin-top: 0.5rem;
}
.badge-v1 {
background: #fef3c7;
color: #92400e;
border: 1px solid #fbbf24;
}
.badge-v2-1 {
background: #d1fae5;
color: #065f46;
border: 1px solid #10b981;
}
.select-input {
padding: 0.5rem 0.75rem;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 0.95rem;
background: white;
cursor: pointer;
transition: all 0.3s ease;
max-width: 200px;
}
.select-input:hover {
border-color: #667eea;
}
.select-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.text-input,
.select-input {
padding: 0.5rem 0.75rem;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 0.95rem;
background: white;
transition: all 0.3s ease;
width: 100%;
max-width: 400px;
}
.text-input:hover,
.select-input:hover {
border-color: #667eea;
}
.text-input:focus,
.select-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.text-input::placeholder {
color: #9ca3af;
}
.select-input {
cursor: pointer;
margin-top: 0.5rem;
}
/* OAuth Section */
.oauth-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.oauth-status {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: white;
border: 2px solid #e9ecef;
border-radius: 6px;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.status-connected {
background: #d1fae5;
color: #065f46;
}
.status-disconnected {
background: #fee2e2;
color: #991b1b;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:hover {
background: #d1d5db;
}
.btn-sm {
padding: 0.375rem 0.875rem;
font-size: 0.8125rem;
}
/* OAuth Toggle (legacy - can be removed) */
.oauth-label {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
background: white;
border: 2px dashed #dee2e6;
border-radius: 6px;
cursor: not-allowed;
opacity: 0.6;
}
.checkbox-input {
cursor: not-allowed;
width: 18px;
height: 18px;
margin-top: 2px;
}
.oauth-text {
font-weight: 500;
color: #495057;
}
.oauth-hint {
display: block;
font-size: 0.85rem;
color: #6c757d;
font-weight: 400;
margin-top: 0.25rem;
}
.scope-info {
display: block;
font-size: 0.85rem;
color: #0284c7;
font-weight: 500;
margin-top: 0.25rem;
font-style: italic;
}
.info-badge {
padding: 0.5rem 0.75rem;
background: #d1fae5;
color: #065f46;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.info-badge.warning {
background: #fef3c7;
color: #92400e;
}
.scope-hint {
display: block;
font-size: 0.8125rem;
color: #6b7280;
margin-top: 0.5rem;
font-style: italic;
}
.api-note {
margin-top: 0.75rem;
padding: 0.75rem;
background: #e7f3ff;
border-left: 3px solid #0284c7;
border-radius: 4px;
font-size: 0.95rem;
color: #0c4a6e;
}
/* Error Improvements */
.error-list {
list-style: none;
padding: 0;
margin: 0.75rem 0;
}
.error-list li {
margin-bottom: 0.5rem;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
}
.error-status {
font-weight: 700;
margin-right: 0.5rem;
}
.error-message {
color: #991b1b;
}
.error-field {
font-style: italic;
color: #b91c1c;
margin-left: 0.5rem;
}
.help-text {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.help-text p {
margin: 0.5rem 0;
font-weight: 600;
}
.help-text ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.help-text li {
margin: 0.25rem 0;
}
/* Load More Section */
.load-more-section {
margin-top: 2rem;
text-align: center;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #5a6268;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
}
/* Search Box */
.search-box {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-info {
color: #6c757d;
font-size: 0.9rem;
font-style: italic;
}
/* Tournament List */
.tournament-list {
display: grid;
gap: 1.5rem;
}
.tournament-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.tournament-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
.tournament-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
flex-wrap: wrap;
}
.tournament-header h4 {
margin: 0;
color: #333;
font-size: 1.25rem;
flex: 1;
}
.tournament-state {
padding: 0.375rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
text-transform: capitalize;
}
.tournament-state.pending {
background: #fef3c7;
color: #92400e;
}
.tournament-state.underway,
.tournament-state.in_progress {
background: #dbeafe;
color: #1e40af;
}
.tournament-state.complete {
background: #d1fae5;
color: #065f46;
}
.tournament-details p {
margin: 0.5rem 0;
color: #495057;
font-size: 0.95rem;
}
.tournament-details strong {
color: #333;
}
/* Info Section */
.info-section {
background: linear-gradient(135deg, #e7f3ff 0%, #dbeafe 100%);
border: 2px solid #0284c7;
}
.info-steps {
display: grid;
gap: 1.5rem;
margin-top: 1.5rem;
}
.step {
display: flex;
gap: 1rem;
align-items: flex-start;
background: white;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #0284c7;
}
.step-number {
flex-shrink: 0;
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.25rem;
}
.step-content h3 {
margin: 0 0 0.5rem 0;
color: #333;
font-size: 1.1rem;
}
.step-content p {
margin: 0;
color: #495057;
line-height: 1.6;
}
.step-content a {
color: #667eea;
text-decoration: none;
font-weight: 600;
border-bottom: 1px solid #667eea;
}
.step-content a:hover {
color: #5568d3;
border-bottom-color: #5568d3;
}
/* Responsive Design */
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.controls-grid {
grid-template-columns: 1fr;
}
.tournament-header {
flex-direction: column;
align-items: flex-start;
}
.step {
flex-direction: column;
}
.step-number {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
}
.search-box {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-info {
color: #666;
font-size: 0.9rem;
font-style: italic;
}
.tournament-list {
display: grid;
gap: 1rem;
margin-top: 1rem;
}
.tournament-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tournament-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.tournament-header h4 {
margin: 0;
color: #333;
font-size: 1.25rem;
}
.tournament-state {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.tournament-state.pending {
background: #fef3c7;
color: #92400e;
}
.tournament-state.underway {
background: #dbeafe;
color: #1e40af;
}
.tournament-state.complete {
background: #d1fae5;
color: #065f46;
}
.tournament-details p {
margin: 0.5rem 0;
color: #666;
font-size: 0.95rem;
}
.tournament-details-box {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tournament-details-box pre {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.5;
}
.info-section {
background: #e7f3ff;
border-color: #667eea;
}
.info-section h2 {
color: #667eea;
}
.info-steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.step {
display: flex;
gap: 1rem;
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: #667eea;
color: white;
font-weight: 700;
font-size: 1.1rem;
flex-shrink: 0;
}
.step-content h3 {
margin: 0 0 0.5rem 0;
color: #333;
font-size: 1.1rem;
}
.step-content p {
margin: 0;
color: #666;
font-size: 0.95rem;
line-height: 1.5;
}
.step-content a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.step-content a:hover {
text-decoration: underline;
}
.code-block {
background: #f8f9fa;
padding: 0.75rem;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
overflow-x: auto;
margin: 0.5rem 0;
}
code {
background: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.help-text {
margin-top: 0.5rem;
font-size: 0.9rem;
opacity: 0.9;
}
.help-text ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.help-text li {
margin: 0.25rem 0;
}
</style>