moxie-site/dashboard.html
Z User 17bfefdbfa Add media section with YouTube Short embed before footer
- New 'See Moxie in Action' section with heading and subtitle
- Embedded YouTube Short (b4ly_hECE_I) in a responsive 16:9 container
- Decorative emerald glow effects and rounded card styling
- 'Watch on YouTube' link with YouTube icon below the embed
- Scroll-animate integration for reveal on scroll
2026-05-12 20:43:06 +00:00

497 lines
21 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard • Moxiegen</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600&display=swap');
:root {
--dark-background: #0F172A;
--dark-surface: #1E293B;
--dark-primary-text: #F8FAFC;
--dark-secondary-text: #94A3B8;
--dark-accent-primary: #38BDF8;
--dark-accent-neural: #A5B4FC;
--dark-border: #334155;
}
body {
background: var(--dark-background);
color: var(--dark-primary-text);
font-family: 'Inter', sans-serif;
}
.text-gradient {
background: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-neural));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn-primary {
background: linear-gradient(135deg, var(--dark-accent-primary), var(--dark-accent-neural));
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.4);
}
.card {
background: var(--dark-surface);
border: 1px solid var(--dark-border);
transition: all 0.3s ease;
}
.card:hover {
border-color: var(--dark-accent-primary);
}
.loading-skeleton {
background: linear-gradient(90deg, var(--dark-surface) 0%, var(--dark-border) 50%, var(--dark-surface) 100%);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
}
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.api-key-mask {
font-family: monospace;
letter-spacing: 2px;
}
.copy-btn {
transition: all 0.2s ease;
}
.copy-btn:hover {
background: var(--dark-accent-primary);
color: var(--dark-background);
}
.toast {
animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.7s;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</head>
<body class="min-h-screen">
<!-- Navigation -->
<nav class="border-b border-slate-800 bg-slate-950/80 backdrop-blur-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="index.html">
<img src="logo.svg" alt="Moxiegen" class="h-10 w-auto" onerror="this.src='logo.png'">
</a>
<span class="font-semibold text-2xl" style="font-family: 'Space Grotesk', sans-serif;">Moxiegen</span>
</div>
<div class="flex items-center gap-4">
<div id="userInfo" class="flex items-center gap-3">
<div class="text-right">
<div id="userName" class="font-medium"></div>
<div id="userEmail" class="text-sm text-slate-400"></div>
</div>
<div id="userAvatar" class="w-10 h-10 rounded-full bg-gradient-to-br from-[#38BDF8] to-[#A5B4FC] flex items-center justify-center font-semibold text-slate-950"></div>
</div>
<a id="adminBtn" href="admin.html" class="hidden px-4 py-2 text-sm font-medium bg-gradient-to-r from-[#38BDF8] to-[#A5B4FC] text-slate-950 rounded-lg hover:opacity-90 transition-all">
Admin
</a>
<button onclick="logout()" class="px-4 py-2 text-sm font-medium text-slate-400 hover:text-white border border-slate-700 hover:border-slate-500 rounded-lg transition-all">
Logout
</button>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-6 py-8">
<!-- Welcome Section -->
<div class="mb-8">
<h1 class="text-3xl font-semibold mb-2">Welcome back, <span id="welcomeName" class="text-gradient">User</span></h1>
<p class="text-slate-400">Manage your API keys, credits, and account settings.</p>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Credits -->
<div class="card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">Credits</span>
<span class="text-2xl">💰</span>
</div>
<div id="creditsBalance" class="text-4xl font-bold text-gradient">--</div>
<p class="text-slate-400 text-sm mt-2">Available balance</p>
</div>
<!-- API Keys -->
<div class="card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">API Keys</span>
<span class="text-2xl">🔑</span>
</div>
<div id="apiKeyCount" class="text-4xl font-bold text-gradient">--</div>
<p class="text-slate-400 text-sm mt-2">Active keys</p>
</div>
<!-- Subscription -->
<div class="card rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<span class="text-slate-400 text-sm font-medium uppercase tracking-wider">Plan</span>
<span class="text-2xl"></span>
</div>
<div id="subscriptionPlan" class="text-4xl font-bold text-gradient capitalize">--</div>
<p id="subscriptionStatus" class="text-slate-400 text-sm mt-2">--</p>
</div>
</div>
<!-- Main Panels -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- API Keys Panel -->
<div class="card rounded-2xl p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold">API Keys</h2>
<button onclick="createApiKey()" class="btn-primary px-4 py-2 text-sm font-medium text-slate-950 rounded-lg flex items-center gap-2">
<span>+</span> Create Key
</button>
</div>
<div id="apiKeysList" class="space-y-3">
<div class="loading-skeleton h-16 rounded-lg"></div>
<div class="loading-skeleton h-16 rounded-lg"></div>
</div>
<div id="noApiKeys" class="hidden text-center py-8 text-slate-400">
<p class="mb-4">No API keys yet</p>
<button onclick="createApiKey()" class="text-[#38BDF8] hover:underline">Create your first API key</button>
</div>
</div>
<!-- Recent Activity Panel -->
<div class="card rounded-2xl p-6">
<h2 class="text-xl font-semibold mb-6">Recent Activity</h2>
<div id="activityList" class="space-y-3">
<div class="loading-skeleton h-14 rounded-lg"></div>
<div class="loading-skeleton h-14 rounded-lg"></div>
<div class="loading-skeleton h-14 rounded-lg"></div>
</div>
<div id="noActivity" class="hidden text-center py-8 text-slate-400">
<p>No recent activity</p>
</div>
</div>
</div>
<!-- Account Settings -->
<div class="card rounded-2xl p-6 mt-6">
<h2 class="text-xl font-semibold mb-6">Account Settings</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm text-slate-400 mb-2">Display Name</label>
<input type="text" id="settingsName" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 outline-none focus:border-[#38BDF8] transition-colors" placeholder="Your name">
</div>
<div>
<label class="block text-sm text-slate-400 mb-2">Email</label>
<input type="email" id="settingsEmail" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-4 py-3 outline-none focus:border-[#38BDF8] transition-colors" disabled>
</div>
</div>
<div class="flex items-center gap-4 mt-6">
<button onclick="saveSettings()" class="btn-primary px-6 py-2 text-sm font-medium text-slate-950 rounded-lg">
Save Changes
</button>
<button onclick="deactivateAccount()" class="px-6 py-2 text-sm font-medium text-red-400 hover:text-red-300 border border-red-400/30 hover:border-red-400 rounded-lg transition-colors">
Deactivate Account
</button>
</div>
</div>
</main>
<!-- Toast Container -->
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 space-y-2"></div>
<!-- Scripts -->
<script src="js/config.js"></script>
<script src="js/auth.js"></script>
<script src="js/api.js"></script>
<script>
// Initialize on page load
document.addEventListener('DOMContentLoaded', async () => {
try {
// Initialize auth
await moxieAuth.init();
// Check if authenticated
if (!moxieAuth.isAuthenticated) {
// Redirect to login
await moxieAuth.login(window.location.href);
return;
}
// Load user data
await loadDashboard();
} catch (error) {
console.error('Initialization error:', error);
showToast('Failed to initialize. Please refresh.', 'error');
}
});
async function loadDashboard() {
try {
// Get user profile from API
const profileResponse = await moxieAPI.getProfile();
const user = profileResponse.data.user;
// Update UI with user info
updateUserUI(user);
// Load credits
const creditsResponse = await moxieAPI.getCredits(1, 5);
document.getElementById('creditsBalance').textContent = creditsResponse.data.credits.toLocaleString();
// Update activity list
updateActivityList(creditsResponse.data.transactions);
// Load API keys
const keysResponse = await moxieAPI.getApiKeys();
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
updateApiKeysList(keysResponse.data.keys);
// Update subscription
document.getElementById('subscriptionPlan').textContent = user.subscription_tier || 'Free';
document.getElementById('subscriptionStatus').textContent = user.subscription_status || 'No active subscription';
// Update settings form
document.getElementById('settingsName').value = user.name || '';
document.getElementById('settingsEmail').value = user.email || '';
} catch (error) {
console.error('Failed to load dashboard:', error);
showToast('Failed to load dashboard data', 'error');
}
}
function updateUserUI(user) {
document.getElementById('userName').textContent = user.name || 'User';
document.getElementById('userEmail').textContent = user.email || '';
document.getElementById('welcomeName').textContent = user.name || 'User';
// Show admin button if user is admin
if (user.role === 'admin') {
document.getElementById('adminBtn').classList.remove('hidden');
}
// Avatar
const avatar = document.getElementById('userAvatar');
if (user.picture) {
avatar.innerHTML = `<img src="${user.picture}" class="w-full h-full rounded-full object-cover">`;
} else {
avatar.textContent = (user.name || 'U').charAt(0).toUpperCase();
}
}
function updateApiKeysList(keys) {
const container = document.getElementById('apiKeysList');
const noKeys = document.getElementById('noApiKeys');
if (keys.length === 0) {
container.innerHTML = '';
noKeys.classList.remove('hidden');
return;
}
noKeys.classList.add('hidden');
container.innerHTML = keys.map(key => `
<div class="flex items-center justify-between bg-slate-800/50 rounded-lg p-4">
<div>
<div class="font-medium">${escapeHtml(key.name)}</div>
<div class="text-sm text-slate-400">
Created ${new Date(key.created_at).toLocaleDateString()}
${key.last_used ? ` • Last used ${new Date(key.last_used).toLocaleDateString()}` : ''}
</div>
</div>
<button onclick="revokeKey('${key.id}')" class="px-3 py-1 text-sm text-red-400 hover:text-red-300 hover:bg-red-400/10 rounded transition-colors">
Revoke
</button>
</div>
`).join('');
}
function updateActivityList(transactions) {
const container = document.getElementById('activityList');
const noActivity = document.getElementById('noActivity');
if (!transactions || transactions.length === 0) {
container.innerHTML = '';
noActivity.classList.remove('hidden');
return;
}
noActivity.classList.add('hidden');
container.innerHTML = transactions.slice(0, 5).map(tx => `
<div class="flex items-center justify-between bg-slate-800/50 rounded-lg p-4">
<div>
<div class="font-medium">${escapeHtml(tx.description || 'Credit transaction')}</div>
<div class="text-sm text-slate-400">${new Date(tx.created_at).toLocaleString()}</div>
</div>
<div class="font-mono ${tx.amount > 0 ? 'text-green-400' : 'text-red-400'}">
${tx.amount > 0 ? '+' : ''}${tx.amount}
</div>
</div>
`).join('');
}
async function createApiKey() {
const name = prompt('Enter a name for this API key:', 'My API Key');
if (!name) return;
try {
const response = await moxieAPI.createApiKey(name);
const key = response.data.key;
// Show the key (only time it will be visible)
showToast('API key created! Copy it now - it won\'t be shown again.', 'success');
// Show modal with key
showApiKeyModal(key);
// Refresh list
const keysResponse = await moxieAPI.getApiKeys();
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
updateApiKeysList(keysResponse.data.keys);
} catch (error) {
showToast('Failed to create API key: ' + error.message, 'error');
}
}
function showApiKeyModal(key) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4';
modal.innerHTML = `
<div class="bg-slate-800 rounded-2xl p-6 max-w-lg w-full">
<h3 class="text-xl font-semibold mb-4">API Key Created</h3>
<p class="text-slate-400 mb-4">Copy this key now. It won't be shown again.</p>
<div class="bg-slate-900 rounded-lg p-4 mb-4">
<code class="text-[#38BDF8] break-all text-sm">${escapeHtml(key.key)}</code>
</div>
<div class="flex gap-3">
<button onclick="navigator.clipboard.writeText('${escapeHtml(key.key)}'); this.textContent='Copied!';"
class="btn-primary px-4 py-2 text-sm font-medium text-slate-950 rounded-lg">
Copy to Clipboard
</button>
<button onclick="this.closest('.fixed').remove()"
class="px-4 py-2 text-sm font-medium text-slate-400 hover:text-white border border-slate-700 rounded-lg">
Close
</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
async function revokeKey(keyId) {
if (!confirm('Are you sure you want to revoke this API key? This action cannot be undone.')) return;
try {
await moxieAPI.revokeApiKey(keyId);
showToast('API key revoked', 'success');
// Refresh list
const keysResponse = await moxieAPI.getApiKeys();
document.getElementById('apiKeyCount').textContent = keysResponse.data.keys.length;
updateApiKeysList(keysResponse.data.keys);
} catch (error) {
showToast('Failed to revoke API key: ' + error.message, 'error');
}
}
async function saveSettings() {
const name = document.getElementById('settingsName').value.trim();
try {
await moxieAPI.updateProfile({ name });
showToast('Settings saved', 'success');
// Update UI
document.getElementById('userName').textContent = name || 'User';
document.getElementById('welcomeName').textContent = name || 'User';
} catch (error) {
showToast('Failed to save settings: ' + error.message, 'error');
}
}
async function deactivateAccount() {
if (!confirm('Are you sure you want to deactivate your account? This action cannot be undone.')) return;
if (!confirm('This will permanently delete your account and all associated data. Continue?')) return;
try {
await moxieAPI.deactivateAccount();
showToast('Account deactivated', 'success');
await logout();
} catch (error) {
showToast('Failed to deactivate account: ' + error.message, 'error');
}
}
async function logout() {
try {
await moxieAuth.logout();
} catch (error) {
console.error('Logout error:', error);
window.location.href = 'index.html';
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
const bgColor = type === 'error' ? 'bg-red-500/20 border-red-500/50' :
type === 'success' ? 'bg-green-500/20 border-green-500/50' :
'bg-slate-800 border-slate-700';
toast.className = `toast ${bgColor} border rounded-lg px-4 py-3 text-sm max-w-sm`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>