Files
memory-infrastructure-palace/code/websites/pokedex.online/src/views/ApiKeyManager.vue

578 lines
12 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="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 -->
<BaseModal
v-model="showDeleteConfirm"
title="Delete API Key?"
size="small"
:close-on-overlay="true"
>
<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>
<template #footer>
<button @click="showDeleteConfirm = false" class="btn btn-secondary">
Cancel
</button>
<button @click="handleDeleteKey" class="btn btn-danger">Delete</button>
</template>
</BaseModal>
<!-- 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 { BaseModal } from '../components/shared/index.js';
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>