🎨 Improve code readability by reformatting and updating function definitions and comments

This commit is contained in:
2026-01-28 18:18:55 +00:00
parent 1944b43af8
commit a24f766e37
154 changed files with 7261 additions and 117 deletions

View File

@@ -0,0 +1,580 @@
<template>
<div class="api-key-manager">
<div class="container">
<div class="header">
<router-link to="/" class="back-button"> Back Home </router-link>
<h1>API Key Manager</h1>
</div>
<!-- Current Status -->
<div class="section">
<h2>Current Status</h2>
<div v-if="isKeyStored" class="status success">
<div class="status-icon"></div>
<div class="status-content">
<p><strong>API Key Stored</strong></p>
<p class="key-display">{{ maskedKey }}</p>
<button @click="showDeleteConfirm = true" class="btn btn-danger">
Clear Stored Key
</button>
</div>
</div>
<div v-else class="status warning">
<div class="status-icon"></div>
<div class="status-content">
<p><strong>No API Key Stored</strong></p>
<p>Add your Challonge API key below to get started</p>
</div>
</div>
</div>
<!-- Add/Update Key -->
<div class="section">
<div class="section-header">
<h2>{{ isKeyStored ? 'Update' : 'Add' }} Challonge API Key</h2>
<button
@click="showGuide = true"
class="help-btn"
title="How to get a Challonge API key"
>
Need Help?
</button>
</div>
<div class="form-group">
<label for="api-key">Challonge API Key</label>
<div class="input-wrapper">
<input
id="api-key"
v-model="inputKey"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your Challonge API key"
class="form-input"
/>
<button
@click="showPassword = !showPassword"
class="toggle-password"
:title="showPassword ? 'Hide' : 'Show'"
>
{{ showPassword ? '👁️' : '👁️‍🗨️' }}
</button>
</div>
<p class="help-text">
Get your API key from
<a
href="https://challonge.com/settings/developer"
target="_blank"
rel="noopener"
>
Challonge Developer Settings
</a>
</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<button
@click="handleSaveKey"
:disabled="!inputKey || saving"
class="btn btn-primary"
>
{{ saving ? 'Saving...' : isKeyStored ? 'Update Key' : 'Save Key' }}
</button>
<div v-if="successMessage" class="success-message">
{{ successMessage }}
</div>
</div>
<!-- Information -->
<div class="section info-section">
<h2> How It Works</h2>
<ul>
<li>
<strong>Secure Storage:</strong> Your API key is stored locally in
your browser using localStorage. It never leaves your device.
</li>
<li>
<strong>Device Specific:</strong> Each device/browser has its own
storage. The key won't sync across devices.
</li>
<li>
<strong>Persistent:</strong> Your key will be available whenever you
use this app, even after closing the browser.
</li>
<li>
<strong>Clear Anytime:</strong> Use the "Clear Stored Key" button to
remove it whenever you want.
</li>
<li>
<strong>Works Everywhere:</strong> Compatible with desktop, mobile,
and tablet browsers.
</li>
</ul>
</div>
<!-- Security Notice -->
<div class="section warning-section">
<h2>🔒 Security Notice</h2>
<p>
⚠️ <strong>localStorage is not encrypted.</strong> Only use this on
trusted devices. If you're on a shared or public computer, clear your
API key when done.
</p>
<p>
For production use, consider using a backend proxy that handles API
keys server-side instead.
</p>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div
v-if="showDeleteConfirm"
class="modal-overlay"
@click="showDeleteConfirm = false"
>
<div class="modal" @click.stop>
<h3>Delete API Key?</h3>
<p>
Are you sure you want to clear the stored API key? You'll need to
enter it again to use the tournament tools.
</p>
<div class="modal-buttons">
<button @click="showDeleteConfirm = false" class="btn btn-secondary">
Cancel
</button>
<button @click="handleDeleteKey" class="btn btn-danger">
Delete
</button>
</div>
</div>
</div>
<!-- Challonge API Key Guide Modal -->
<ChallongeApiKeyGuide v-if="showGuide" @close="showGuide = false" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChallongeApiKeyGuide from '../components/ChallongeApiKeyGuide.vue';
import { useChallongeApiKey } from '../composables/useChallongeApiKey.js';
const { saveApiKey, clearApiKey, maskedKey, isKeyStored } =
useChallongeApiKey();
const inputKey = ref('');
const showPassword = ref(false);
const showDeleteConfirm = ref(false);
const saving = ref(false);
const error = ref('');
const successMessage = ref('');
const showGuide = ref(false);
async function handleSaveKey() {
error.value = '';
successMessage.value = '';
// Validate input
if (!inputKey.value.trim()) {
error.value = 'Please enter an API key';
return;
}
if (inputKey.value.length < 10) {
error.value = 'API key appears to be too short';
return;
}
saving.value = true;
try {
const success = saveApiKey(inputKey.value.trim());
if (success) {
successMessage.value = 'API key saved successfully!';
inputKey.value = '';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} else {
error.value = 'Failed to save API key. Please try again.';
}
} finally {
saving.value = false;
}
}
function handleDeleteKey() {
const success = clearApiKey();
if (success) {
showDeleteConfirm.value = false;
inputKey.value = '';
successMessage.value = 'API key cleared successfully';
setTimeout(() => {
successMessage.value = '';
}, 3000);
} else {
error.value = 'Failed to clear API key';
}
}
</script>
<style scoped>
.api-key-manager {
min-height: 100vh;
padding: 2rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 800px;
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: 2rem;
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);
}
.help-btn {
padding: 0.5rem 1rem;
background: #f59e0b;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
font-size: 0.95rem;
}
.help-btn:hover {
background: #d97706;
transform: translateY(-2px);
}
h1 {
color: #333;
margin: 0;
font-size: 2rem;
}
.section {
margin: 2rem 0;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.section-header h2 {
margin: 0;
}
h2 {
color: #495057;
margin-top: 0;
font-size: 1.3rem;
}
.status {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-radius: 8px;
align-items: flex-start;
}
.status.success {
background: #d1fae5;
border: 2px solid #10b981;
}
.status.warning {
background: #fef3c7;
border: 2px solid #f59e0b;
}
.status-icon {
font-size: 1.5rem;
min-width: 2rem;
}
.status-content p {
margin: 0.5rem 0;
color: #333;
}
.key-display {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #10b981;
font-size: 1.1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-input {
flex: 1;
padding: 0.75rem;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 1rem;
font-family: 'Courier New', monospace;
transition: border-color 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.toggle-password {
padding: 0.75rem;
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
transition: transform 0.2s ease;
}
.toggle-password:hover {
transform: scale(1.1);
}
.help-text {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #666;
}
.help-text a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.help-text a:hover {
text-decoration: underline;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.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-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.error-message {
margin-top: 1rem;
padding: 1rem;
background: #fee;
color: #c33;
border-radius: 6px;
border-left: 4px solid #c33;
}
.success-message {
margin-top: 1rem;
padding: 1rem;
background: #d1fae5;
color: #065f46;
border-radius: 6px;
border-left: 4px solid #10b981;
font-weight: 500;
}
.info-section {
background: #e7f3ff;
border-color: #667eea;
}
.info-section h2 {
color: #667eea;
}
.info-section ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.info-section li {
margin: 0.75rem 0;
color: #495057;
line-height: 1.6;
}
.warning-section {
background: #fef3c7;
border-color: #f59e0b;
}
.warning-section h2 {
color: #d97706;
}
.warning-section p {
color: #92400e;
line-height: 1.6;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: white;
padding: 2rem;
border-radius: 12px;
max-width: 400px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.modal h3 {
margin-top: 0;
color: #333;
font-size: 1.5rem;
}
.modal p {
color: #666;
line-height: 1.6;
}
.modal-buttons {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
.modal-buttons .btn {
margin: 0;
}
@media (max-width: 640px) {
.container {
padding: 1rem;
}
h1 {
font-size: 1.5rem;
}
.header {
flex-direction: column;
align-items: flex-start;
}
.modal-buttons {
flex-direction: column;
}
.modal-buttons .btn {
width: 100%;
}
}
</style>

View File

@@ -57,7 +57,8 @@
API Key Mode - showing only created tournaments
</div>
<span class="scope-hint">
Shows tournaments you created and tournaments where you're an admin
Shows tournaments you created and tournaments where you're an
admin
</span>
</div>
@@ -474,7 +475,17 @@ async function testListTournaments(resetPagination = true) {
page: currentPage.value,
perPage: 100,
scope: 'USER',
states: ['pending', 'in_progress', 'ended'],
states: [
'pending',
'checking_in',
'checked_in',
'accepting_predictions',
'group_stages_underway',
'group_stages_finalized',
'underway',
'awaiting_review',
'complete'
],
resultsCount: result.length,
isAuthenticated: isAuthenticated.value,
authType: isAuthenticated.value ? 'OAuth' : 'API Key',

View File

@@ -0,0 +1,421 @@
<template>
<div class="gamemaster-manager">
<div class="container">
<div class="header">
<router-link to="/" class="back-button"> Back Home </router-link>
<h1>Gamemaster Manager</h1>
</div>
<p class="description">
Fetch the latest Pokemon GO gamemaster data from PokeMiners and break it
up into separate files for easier processing.
</p>
<!-- Fetch Section -->
<div class="section">
<h2>1. Fetch Latest Gamemaster</h2>
<button
@click="fetchGamemaster"
:disabled="loading"
class="btn btn-primary"
>
{{ loading ? 'Fetching...' : 'Fetch from PokeMiners' }}
</button>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="rawGamemaster" class="success">
Fetched {{ rawGamemaster.length.toLocaleString() }} items from
gamemaster
</div>
</div>
<!-- Break Up Section -->
<div v-if="rawGamemaster" class="section">
<h2>2. Break Up Gamemaster</h2>
<button @click="processGamemaster" class="btn btn-primary">
Process & Break Up Data
</button>
<div v-if="processedData" class="stats-grid">
<div class="stat-card">
<h3>Pokemon (Filtered)</h3>
<p class="stat-number">
{{ stats.pokemonCount.toLocaleString() }}
</p>
<p class="stat-detail">{{ stats.pokemonSize }}</p>
<p class="stat-info">Base forms + regional variants</p>
</div>
<div class="stat-card">
<h3>All Forms & Costumes</h3>
<p class="stat-number">
{{ stats.allFormsCount.toLocaleString() }}
</p>
<p class="stat-detail">{{ stats.allFormsSize }}</p>
<p class="stat-info">Every variant, costume, form</p>
</div>
<div class="stat-card">
<h3>Moves</h3>
<p class="stat-number">{{ stats.movesCount.toLocaleString() }}</p>
<p class="stat-detail">{{ stats.movesSize }}</p>
<p class="stat-info">All quick & charged moves</p>
</div>
</div>
</div>
<!-- Download Section -->
<div v-if="processedData" class="section">
<h2>3. Download Files</h2>
<div class="button-group">
<button @click="downloadPokemon" class="btn btn-success">
📥 Download pokemon.json
</button>
<button @click="downloadAllForms" class="btn btn-success">
📥 Download pokemon-allFormsCostumes.json
</button>
<button @click="downloadMoves" class="btn btn-success">
📥 Download pokemon-moves.json
</button>
<button @click="downloadAll" class="btn btn-primary">
📦 Download All Files
</button>
</div>
</div>
<!-- Info Section -->
<div class="section info-section">
<h2>About This Tool</h2>
<p>
This tool fetches the latest Pokemon GO gamemaster data from
<a
href="https://github.com/PokeMiners/game_masters"
target="_blank"
rel="noopener"
>PokeMiners GitHub</a
>
and processes it into three separate files:
</p>
<ul>
<li>
<strong>pokemon.json</strong> - Base Pokemon forms plus regional
variants (Alola, Galarian, Hisuian, Paldea)
</li>
<li>
<strong>pokemon-allFormsCostumes.json</strong> - Complete dataset
including all costumes, event forms, shadows, etc.
</li>
<li>
<strong>pokemon-moves.json</strong> - All quick and charged moves
available in Pokemon GO
</li>
</ul>
<p class="note">
💡 The filtered pokemon.json is ideal for most use cases, while
allFormsCostumes is comprehensive for complete data analysis.
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import {
fetchLatestGamemaster,
breakUpGamemaster,
downloadJson,
getGamemasterStats
} from '../utilities/gamemaster-utils.js';
const loading = ref(false);
const error = ref(null);
const rawGamemaster = ref(null);
const processedData = ref(null);
const stats = computed(() => {
if (!processedData.value) return null;
return getGamemasterStats(processedData.value);
});
async function fetchGamemaster() {
loading.value = true;
error.value = null;
rawGamemaster.value = null;
processedData.value = null;
try {
const data = await fetchLatestGamemaster();
rawGamemaster.value = data;
} catch (err) {
error.value = `Failed to fetch gamemaster: ${err.message}`;
} finally {
loading.value = false;
}
}
function processGamemaster() {
if (!rawGamemaster.value) return;
try {
processedData.value = breakUpGamemaster(rawGamemaster.value);
} catch (err) {
error.value = `Failed to process gamemaster: ${err.message}`;
}
}
function downloadPokemon() {
downloadJson(processedData.value.pokemon, 'pokemon.json');
}
function downloadAllForms() {
downloadJson(
processedData.value.pokemonAllForms,
'pokemon-allFormsCostumes.json'
);
}
function downloadMoves() {
downloadJson(processedData.value.moves, 'pokemon-moves.json');
}
function downloadAll() {
downloadPokemon();
setTimeout(() => downloadAllForms(), 500);
setTimeout(() => downloadMoves(), 1000);
}
</script>
<style scoped>
.gamemaster-manager {
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;
}
h2 {
color: #495057;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.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-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.error {
margin-top: 1rem;
padding: 1rem;
background: #fee;
color: #c33;
border-radius: 6px;
border-left: 4px solid #c33;
}
.success {
margin-top: 1rem;
padding: 1rem;
background: #d1fae5;
color: #065f46;
border-radius: 6px;
border-left: 4px solid #10b981;
font-weight: 500;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-card h3 {
font-size: 1rem;
color: #666;
margin-bottom: 0.5rem;
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: #667eea;
margin: 0.5rem 0;
}
.stat-detail {
font-size: 1rem;
color: #999;
margin: 0.25rem 0;
}
.stat-info {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
}
.info-section {
background: #e7f3ff;
border-color: #667eea;
}
.info-section h2 {
color: #667eea;
}
.info-section ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.info-section li {
margin: 0.5rem 0;
color: #495057;
}
.info-section a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.info-section a:hover {
text-decoration: underline;
}
.note {
margin-top: 1rem;
padding: 0.75rem;
background: white;
border-radius: 6px;
font-size: 0.95rem;
color: #495057;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
margin-right: 0;
}
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<div class="home-view">
<div class="container">
<!-- Header -->
<div class="header-top">
<ProfessorPokeball size="150px" color="#F44336" :animate="true" />
</div>
<h1>Pokedex Online</h1>
<p class="subtitle">Your Digital Pokédex Companion</p>
<p class="description">
A modern web application for housing different apps that make a
professors life easier. Built with for Pokémon Professors everywhere.
</p>
<div class="tools-section">
<h2>Available Tools</h2>
<div class="tool-cards">
<router-link to="/api-key-manager" class="tool-card settings">
<div class="tool-icon">🔐</div>
<h3>API Key Manager</h3>
<p>Store your Challonge API key locally for easy access</p>
<span v-if="isKeyStored" class="badge">Active</span>
</router-link>
<router-link to="/gamemaster" class="tool-card">
<div class="tool-icon">📦</div>
<h3>Gamemaster Manager</h3>
<p>Fetch and process Pokemon GO gamemaster data from PokeMiners</p>
</router-link>
<router-link to="/challonge-test" class="tool-card">
<div class="tool-icon">🔑</div>
<h3>Challonge API Test</h3>
<p>Test your Challonge API connection and configuration</p>
</router-link>
<div class="tool-card disabled">
<div class="tool-icon">📝</div>
<h3>Printing Tool</h3>
<p>Generate tournament printing materials (Coming Soon)</p>
</div>
<div class="tool-card disabled">
<div class="tool-icon">🏆</div>
<h3>Tournament Manager</h3>
<p>Manage Challonge tournaments and participants (Coming Soon)</p>
</div>
</div>
</div>
<div class="status">
<strong>Status:</strong> In Development<br />
Check back soon for updates!
</div>
</div>
</div>
</template>
<script setup>
import ProfessorPokeball from '../components/shared/ProfessorPokeball.vue';
</script>
<style scoped>
.home-view {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.container {
background: white;
border-radius: 20px;
padding: 60px 40px;
max-width: 900px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
}
.header-top {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
h1 {
color: #333;
margin-bottom: 20px;
font-size: 2.5em;
}
.subtitle {
color: #667eea;
font-size: 1.2em;
margin-bottom: 30px;
}
.description {
color: #666;
line-height: 1.6;
margin-bottom: 40px;
}
.tools-section {
margin: 3rem 0;
}
.tools-section h2 {
color: #333;
margin-bottom: 1.5rem;
font-size: 1.8rem;
}
.tool-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.tool-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 12px;
text-decoration: none;
color: white;
transition: all 0.3s ease;
cursor: pointer;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
position: relative;
}
.tool-card.settings {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: 2px solid #34d399;
}
.tool-card:hover:not(.disabled) {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.tool-card.disabled {
background: linear-gradient(135deg, #999 0%, #666 100%);
opacity: 0.6;
cursor: not-allowed;
}
.tool-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.tool-card h3 {
color: white;
margin-bottom: 0.5rem;
font-size: 1.3rem;
}
.tool-card p {
color: rgba(255, 255, 255, 0.9);
font-size: 0.95rem;
line-height: 1.4;
margin: 0;
}
.badge {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(255, 255, 255, 0.2);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.status {
background: #f0f0f0;
padding: 15px;
border-radius: 10px;
color: #666;
font-size: 0.9em;
}
.status strong {
color: #667eea;
}
@media (max-width: 768px) {
.container {
padding: 40px 20px;
}
h1 {
font-size: 2em;
}
.tool-cards {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,247 @@
<template>
<div class="oauth-callback">
<div class="container">
<div class="callback-card">
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<h2>Authenticating...</h2>
<p>Please wait while we complete your OAuth login</p>
</div>
<div v-else-if="error" class="error-state">
<div class="error-icon"></div>
<h2>Authentication Failed</h2>
<p class="error-message">{{ error }}</p>
<router-link to="/challonge-test" class="btn btn-primary">
Back to Challonge Test
</router-link>
</div>
<div v-else-if="success" class="success-state">
<div class="success-icon"></div>
<h2>Authentication Successful!</h2>
<p>You're now logged in with OAuth</p>
<p class="redirect-info">Redirecting in {{ countdown }} seconds...</p>
<router-link to="/challonge-test" class="btn btn-primary">
Continue to Challonge Test
</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useChallongeOAuth } from '../composables/useChallongeOAuth.js';
const route = useRoute();
const router = useRouter();
const { exchangeCode } = useChallongeOAuth();
const loading = ref(true);
const error = ref(null);
const success = ref(false);
const countdown = ref(3);
onMounted(async () => {
// Get authorization code and state from URL
const code = route.query.code;
const state = route.query.state;
const errorParam = route.query.error;
const errorDescription = route.query.error_description;
// Handle OAuth errors
if (errorParam) {
loading.value = false;
error.value = errorDescription || `OAuth error: ${errorParam}`;
return;
}
// Validate required parameters
if (!code || !state) {
loading.value = false;
error.value = 'Missing authorization code or state parameter';
return;
}
try {
// Exchange authorization code for tokens
await exchangeCode(code, state);
loading.value = false;
success.value = true;
// Start countdown redirect
const interval = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(interval);
router.push('/challonge-test');
}
}, 1000);
} catch (err) {
loading.value = false;
error.value = err.message || 'Failed to complete OAuth authentication';
console.error('OAuth callback error:', err);
}
});
</script>
<style scoped>
.oauth-callback {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.container {
max-width: 500px;
width: 100%;
}
.callback-card {
background: white;
border-radius: 12px;
padding: 3rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
}
/* Loading State */
.loading-state {
padding: 2rem 0;
}
.spinner {
width: 64px;
height: 64px;
border: 5px solid #f3f3f3;
border-top: 5px solid #667eea;
border-radius: 50%;
margin: 0 auto 2rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Success State */
.success-state {
padding: 2rem 0;
}
.success-icon {
width: 80px;
height: 80px;
background: #10b981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
font-size: 3rem;
color: white;
animation: scaleIn 0.5s ease;
}
/* Error State */
.error-state {
padding: 2rem 0;
}
.error-icon {
width: 80px;
height: 80px;
background: #ef4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
font-size: 3rem;
color: white;
animation: scaleIn 0.5s ease;
}
@keyframes scaleIn {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1rem;
color: #1f2937;
}
p {
font-size: 1.1rem;
color: #6b7280;
margin-bottom: 1rem;
}
.error-message {
color: #ef4444;
font-weight: 500;
padding: 1rem;
background: #fee2e2;
border-radius: 8px;
margin: 1.5rem 0;
}
.redirect-info {
font-size: 0.95rem;
color: #9ca3af;
margin-top: 1rem;
}
.btn {
display: inline-block;
padding: 0.75rem 2rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
margin-top: 1.5rem;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-primary {
background: #667eea;
}
.btn-primary:hover {
background: #5568d3;
}
</style>