Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel

feat: New design for website and dashboard + 8ball fix

+2 -1
.gitignore
··· 1 1 node_modules 2 2 .env 3 3 dist 4 - logs 4 + logs 5 + .env.web
+18 -7
src/commands/fun/8ball.ts
··· 11 11 type MessageActionRowComponentBuilder, 12 12 } from 'discord.js'; 13 13 import { validateCommandOptions, sanitizeInput } from '@/utils/validation'; 14 - import { SlashCommandProps } from '@/types/command'; 15 14 import { random } from '@/utils/misc'; 16 15 import { 17 16 createCooldownManager, ··· 21 20 } from '@/utils/cooldown'; 22 21 import { createCommandLogger } from '@/utils/commandLogger'; 23 22 import { createErrorHandler } from '@/utils/errorHandler'; 23 + import { createMemoryManager } from '@/utils/memoryManager'; 24 24 25 25 const responses = [ 26 26 'itiscertain', ··· 49 49 const commandLogger = createCommandLogger('8ball'); 50 50 const errorHandler = createErrorHandler('8ball'); 51 51 52 - export default { 52 + const questionStorage = createMemoryManager<string, string>({ 53 + maxSize: 1000, 54 + maxAge: 5 * 60 * 1000, 55 + cleanupInterval: 60 * 1000, 56 + }); 57 + 58 + const eightBallCommand = { 53 59 data: new SlashCommandBuilder() 54 60 .setName('8ball') 55 61 .setNameLocalizations({ ··· 87 93 ]) 88 94 .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 89 95 90 - execute: async (client, interaction) => { 96 + questionStorage, 97 + 98 + execute: async (client: any, interaction: any) => { 91 99 try { 92 100 const cooldownCheck = await checkCooldown( 93 101 cooldownManager, ··· 125 133 await client.getLocaleText('commands.8ball.askagain', interaction.locale), 126 134 ]); 127 135 136 + const interactionId = `${interaction.user.id}_${Date.now()}`; 137 + questionStorage.set(interactionId, question); 138 + 128 139 const container = new ContainerBuilder() 129 140 .setAccentColor(0x8b5cf6) 130 141 .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔮 ${title}`)) ··· 139 150 .setStyle(ButtonStyle.Primary) 140 151 .setLabel(askAgainLabel) 141 152 .setEmoji({ name: '🎱' }) 142 - .setCustomId( 143 - `8ball_reroll_${interaction.user.id}_${Date.now()}_${encodeURIComponent(question)}` 144 - ) 153 + .setCustomId(`8ball_reroll_${interactionId}`) 145 154 ) 146 155 ); 147 156 ··· 159 168 }); 160 169 } 161 170 }, 162 - } as SlashCommandProps; 171 + }; 172 + 173 + export default eightBallCommand;
+17 -12
src/events/interactionCreate.ts
··· 197 197 }); 198 198 } 199 199 } else if (i.customId.startsWith('8ball_reroll_')) { 200 - const customIdParts = i.customId.split('_'); 201 - const originalUserId = customIdParts[2]; 200 + const interactionId = i.customId.replace('8ball_reroll_', ''); 201 + const originalUserId = interactionId.split('_')[0]; 202 202 203 203 if (originalUserId !== i.user.id) { 204 204 return await i.reply({ ··· 207 207 }); 208 208 } 209 209 210 + const eightBallCommand = this.client.commands.get('8ball'); 210 211 let question = 'What will happen?'; 211 212 212 - try { 213 - const customIdParts = i.customId.split('_'); 214 - if (customIdParts.length >= 5) { 215 - const encodedQuestion = customIdParts.slice(4).join('_'); 216 - question = decodeURIComponent(encodedQuestion); 213 + if (eightBallCommand && 'questionStorage' in eightBallCommand) { 214 + const storedQuestion = ( 215 + eightBallCommand as { questionStorage: { get: (id: string) => string | undefined } } 216 + ).questionStorage.get(interactionId); 217 + if (storedQuestion) { 218 + question = storedQuestion; 217 219 } 218 - } catch (error) { 219 - console.log('Error extracting question:', error); 220 220 } 221 221 222 222 const responses = [ ··· 260 260 i.locale 261 261 ); 262 262 263 + const newInteractionId = `${i.user.id}_${Date.now()}`; 264 + if (eightBallCommand && 'questionStorage' in eightBallCommand) { 265 + ( 266 + eightBallCommand as { questionStorage: { set: (id: string, value: string) => void } } 267 + ).questionStorage.set(newInteractionId, question); 268 + } 269 + 263 270 const container = new ContainerBuilder() 264 271 .setAccentColor(0x8b5cf6) 265 272 .addTextDisplayComponents(new TextDisplayBuilder().setContent(`# 🔮 ${title}`)) ··· 274 281 .setStyle(ButtonStyle.Primary) 275 282 .setLabel(askAgainLabel) 276 283 .setEmoji({ name: '🎱' }) 277 - .setCustomId( 278 - `8ball_reroll_${i.user.id}_${Date.now()}_${encodeURIComponent(question)}` 279 - ) 284 + .setCustomId(`8ball_reroll_${newInteractionId}`) 280 285 ) 281 286 ); 282 287
+1 -1
src/index.ts
··· 34 34 ? ALLOWED_ORIGINS.split(',') 35 35 : ['http://localhost:3000', 'http://localhost:8080'], 36 36 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 37 - allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization'], 37 + allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization', 'Cache-Control', 'Pragma'], 38 38 credentials: true, 39 39 maxAge: 86400, 40 40 })
+1 -1
src/routes/auth.ts
··· 11 11 const DISCORD_REDIRECT_URI = 12 12 process.env.DISCORD_REDIRECT_URI || 'http://localhost:8080/api/auth/discord/callback'; 13 13 const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 14 - const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'; 14 + const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:2020'; 15 15 16 16 interface DiscordUser { 17 17 id: string;
+4 -1
web/index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>Aethel Dashboard</title> 7 + <title>Aethel</title> 8 + <meta name="description" content="A powerful and cute Discord bot that brings AI chat, weather updates, Wikipedia search, reminders, and fun commands to your DMs and guilds!" /> 9 + <meta name="keywords" content="discord bot, ai chat, weather bot, reminder bot, discord commands, wikipedia search, pet images" /> 10 + <meta name="author" content="Aethel Labs and scanash00" /> 8 11 <link rel="preconnect" href="https://fonts.googleapis.com"> 9 12 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10 13 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+3
web/package.json
··· 9 9 "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" 10 10 }, 11 11 "dependencies": { 12 + "@heroicons/react": "^2.2.0", 12 13 "@tanstack/react-query": "^5.83.0", 13 14 "axios": "^1.10.0", 15 + "dotenv": "^17.2.0", 14 16 "lucide-react": "^0.294.0", 15 17 "react": "^18.3.1", 16 18 "react-dom": "^18.3.1", 19 + "react-icons": "^5.5.0", 17 20 "react-router-dom": "^6.30.1", 18 21 "sonner": "^1.7.4", 19 22 "zustand": "^4.5.7"
+33
web/pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 dependencies: 11 + '@heroicons/react': 12 + specifier: ^2.2.0 13 + version: 2.2.0(react@18.3.1) 11 14 '@tanstack/react-query': 12 15 specifier: ^5.83.0 13 16 version: 5.83.0(react@18.3.1) 14 17 axios: 15 18 specifier: ^1.10.0 16 19 version: 1.10.0 20 + dotenv: 21 + specifier: ^17.2.0 22 + version: 17.2.0 17 23 lucide-react: 18 24 specifier: ^0.294.0 19 25 version: 0.294.0(react@18.3.1) ··· 23 29 react-dom: 24 30 specifier: ^18.3.1 25 31 version: 18.3.1(react@18.3.1) 32 + react-icons: 33 + specifier: ^5.5.0 34 + version: 5.5.0(react@18.3.1) 26 35 react-router-dom: 27 36 specifier: ^6.30.1 28 37 version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ··· 315 324 '@eslint/js@8.57.1': 316 325 resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} 317 326 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 327 + 328 + '@heroicons/react@2.2.0': 329 + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} 330 + peerDependencies: 331 + react: '>= 16 || ^19.0.0-rc' 318 332 319 333 '@humanwhocodes/config-array@0.13.0': 320 334 resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} ··· 640 654 resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} 641 655 engines: {node: '>=6.0.0'} 642 656 657 + dotenv@17.2.0: 658 + resolution: {integrity: sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==} 659 + engines: {node: '>=12'} 660 + 643 661 dunder-proto@1.0.1: 644 662 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 645 663 engines: {node: '>= 0.4'} ··· 1162 1180 peerDependencies: 1163 1181 react: ^18.3.1 1164 1182 1183 + react-icons@5.5.0: 1184 + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} 1185 + peerDependencies: 1186 + react: '*' 1187 + 1165 1188 react-refresh@0.17.0: 1166 1189 resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} 1167 1190 engines: {node: '>=0.10.0'} ··· 1630 1653 1631 1654 '@eslint/js@8.57.1': {} 1632 1655 1656 + '@heroicons/react@2.2.0(react@18.3.1)': 1657 + dependencies: 1658 + react: 18.3.1 1659 + 1633 1660 '@humanwhocodes/config-array@0.13.0': 1634 1661 dependencies: 1635 1662 '@humanwhocodes/object-schema': 2.0.3 ··· 1980 2007 doctrine@3.0.0: 1981 2008 dependencies: 1982 2009 esutils: 2.0.3 2010 + 2011 + dotenv@17.2.0: {} 1983 2012 1984 2013 dunder-proto@1.0.1: 1985 2014 dependencies: ··· 2499 2528 loose-envify: 1.4.0 2500 2529 react: 18.3.1 2501 2530 scheduler: 0.23.2 2531 + 2532 + react-icons@5.5.0(react@18.3.1): 2533 + dependencies: 2534 + react: 18.3.1 2502 2535 2503 2536 react-refresh@0.17.0: {} 2504 2537
web/public/bot_icon.png

This is a binary file and will not be displayed.

+115
web/src/components/Footer.tsx
··· 1 + import { Link } from 'react-router-dom'; 2 + import { FaGithub } from 'react-icons/fa'; 3 + 4 + export default function Footer() { 5 + const currentYear = new Date().getFullYear(); 6 + 7 + const navigation = { 8 + main: [ 9 + { name: 'Home', href: '/' }, 10 + { name: 'Status', href: '/status' }, 11 + ], 12 + legal: [ 13 + { name: 'Privacy', href: '/legal/privacy' }, 14 + { name: 'Terms', href: '/legal/terms' }, 15 + ], 16 + social: [ 17 + { 18 + name: 'GitHub', 19 + href: 'https://github.com/aethel-labs/aethel', 20 + icon: FaGithub, 21 + }, 22 + ], 23 + }; 24 + 25 + return ( 26 + <footer className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 mt-16"> 27 + <div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8"> 28 + <div className="xl:grid xl:grid-cols-3 xl:gap-8"> 29 + <div className="space-y-8 xl:col-span-1"> 30 + <div className="flex items-center space-x-3"> 31 + <div className="w-10 h-10 rounded-lg overflow-hidden"> 32 + <img 33 + src="/bot_icon.png" 34 + alt="Bot Icon" 35 + className="w-full h-full object-cover" 36 + width={40} 37 + height={40} 38 + /> 39 + </div> 40 + <span className="text-xl font-bold text-gray-900 dark:text-white">Aethel</span> 41 + </div> 42 + <p className="text-gray-500 dark:text-gray-400 text-base"> 43 + A feature-rich Discord bot for your account. 44 + </p> 45 + <div className="flex space-x-6"> 46 + {navigation.social.map((item) => ( 47 + <a 48 + key={item.name} 49 + href={item.href} 50 + target="_blank" 51 + rel="noopener noreferrer" 52 + className="text-gray-400 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" 53 + > 54 + <span className="sr-only">{item.name}</span> 55 + <item.icon className="h-6 w-6" aria-hidden="true" /> 56 + </a> 57 + ))} 58 + </div> 59 + </div> 60 + <div className="mt-12 grid grid-cols-2 gap-8 xl:mt-0 xl:col-span-2"> 61 + <div className="md:grid md:grid-cols-2 md:gap-8"> 62 + <div> 63 + <h3 className="text-sm font-semibold text-gray-900 dark:text-white tracking-wider uppercase"> 64 + Navigation 65 + </h3> 66 + <ul role="list" className="mt-4 space-y-4"> 67 + {navigation.main.map((item) => ( 68 + <li key={item.name}> 69 + <Link 70 + to={item.href} 71 + className="text-base text-gray-500 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" 72 + > 73 + {item.name} 74 + </Link> 75 + </li> 76 + ))} 77 + </ul> 78 + </div> 79 + <div className="mt-12 md:mt-0"> 80 + <h3 className="text-sm font-semibold text-gray-900 dark:text-white tracking-wider uppercase"> 81 + Legal 82 + </h3> 83 + <ul role="list" className="mt-4 space-y-4"> 84 + {navigation.legal.map((item) => ( 85 + <li key={item.name}> 86 + <Link 87 + to={item.href} 88 + className="text-base text-gray-500 hover:text-sky-600 dark:hover:text-sky-400 transition-colors" 89 + > 90 + {item.name} 91 + </Link> 92 + </li> 93 + ))} 94 + </ul> 95 + </div> 96 + </div> 97 + </div> 98 + </div> 99 + <div className="mt-12 border-t border-gray-200 dark:border-gray-800 pt-8"> 100 + <p className="text-base text-gray-500 dark:text-gray-400 text-center"> 101 + &copy; {currentYear} Aethel Labs.{' '} 102 + <a 103 + href="https://github.com/Aethel-Labs/aethel/blob/main/LICENSE" 104 + target="_blank" 105 + rel="noopener noreferrer" 106 + className="text-sky-600 hover:text-sky-700 dark:text-sky-400 dark:hover:text-sky-300 transition-colors" 107 + > 108 + MIT License 109 + </a> 110 + </p> 111 + </div> 112 + </div> 113 + </footer> 114 + ); 115 + }
+78 -50
web/src/components/Layout.tsx
··· 7 7 Bell, 8 8 Menu, 9 9 X, 10 - LogOut, 11 - User 10 + LogOut 12 11 } from 'lucide-react' 13 12 import { useAuthStore } from '../stores/authStore' 14 13 import { toast } from 'sonner' 14 + 15 15 16 16 interface LayoutProps { 17 17 children: ReactNode ··· 35 35 } 36 36 37 37 return ( 38 - <div className="min-h-screen bg-[#0A0A0A]"> 39 - {/* Mobile sidebar */} 40 - <div className={`fixed inset-0 z-50 lg:hidden ${ 41 - sidebarOpen ? 'block' : 'hidden' 38 + <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> 39 + <div className={`fixed inset-0 z-50 lg:hidden transition-opacity duration-300 ${ 40 + sidebarOpen ? 'opacity-100' : 'opacity-0 pointer-events-none' 42 41 }`}> 43 - <div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} /> 44 - <div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-gray-900/50 border-r border-gray-700"> 45 - <div className="flex h-16 items-center justify-between px-4"> 46 - <h1 className="text-xl font-bold text-white">Aethel</h1> 42 + <div className="fixed inset-0 bg-black/70" onClick={() => setSidebarOpen(false)} /> 43 + <div className={`fixed inset-y-0 left-0 flex w-64 flex-col bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 shadow-xl transform transition-transform duration-300 ease-out ${ 44 + sidebarOpen ? 'translate-x-0' : '-translate-x-full' 45 + }`}> 46 + <div className="flex h-14 items-center justify-between px-5 border-b border-slate-200/50 dark:border-slate-700/50"> 47 + <div className="flex items-center space-x-3"> 48 + <img 49 + className="w-7 h-7 rounded-lg" 50 + src="/bot_icon.png" 51 + alt="Aethel Bot" 52 + /> 53 + <h1 className="text-lg font-bold text-slate-900 dark:text-white">Aethel</h1> 54 + </div> 47 55 <button 48 56 onClick={() => setSidebarOpen(false)} 49 - className="text-gray-400 hover:text-gray-600" 57 + className="p-2 text-slate-500 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors" 50 58 > 51 - <X className="h-6 w-6" /> 59 + <X className="h-4 w-4" /> 52 60 </button> 53 61 </div> 54 - <nav className="flex-1 space-y-1 px-2 py-4"> 62 + <nav className="flex-1 space-y-1 px-3 py-4"> 55 63 {navigation.map((item) => { 56 64 const Icon = item.icon 57 65 const isActive = location.pathname === item.href ··· 60 68 key={item.name} 61 69 to={item.href} 62 70 onClick={() => setSidebarOpen(false)} 63 - className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${ 64 - isActive 65 - ? 'bg-gray-800 text-white' 66 - : 'text-gray-300 hover:bg-gray-700 hover:text-white' 67 - }`} 71 + className={`group flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 ${ 72 + isActive 73 + ? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-md' 74 + : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white' 75 + }`} 68 76 > 69 - <Icon className="mr-3 h-5 w-5" /> 77 + <Icon className={`mr-3 h-4 w-4 flex-shrink-0 ${ 78 + isActive ? 'text-white' : 'text-slate-500 dark:text-slate-400 group-hover:text-blue-500' 79 + }`} /> 70 80 {item.name} 71 81 </Link> 72 82 ) ··· 75 85 </div> 76 86 </div> 77 87 78 - {/* Desktop sidebar */} 79 88 <div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col"> 80 - <div className="flex flex-col flex-grow bg-gray-900/50 border-r border-gray-700"> 81 - <div className="flex h-16 items-center px-4"> 82 - <h1 className="text-xl font-bold text-white">Aethel Dashboard</h1> 89 + <div className="flex flex-col flex-grow bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50"> 90 + <div className="flex h-14 items-center px-5 border-b border-slate-200/50 dark:border-slate-700/50"> 91 + <div className="flex items-center space-x-3"> 92 + <img 93 + className="w-7 h-7 rounded-lg" 94 + src="/bot_icon.png" 95 + alt="Aethel Bot" 96 + /> 97 + <h1 className="text-lg font-bold text-slate-900 dark:text-white">Aethel</h1> 98 + </div> 83 99 </div> 84 - <nav className="flex-1 space-y-1 px-2 py-4"> 100 + <nav className="flex-1 space-y-1 px-3 py-4"> 85 101 {navigation.map((item) => { 86 102 const Icon = item.icon 87 103 const isActive = location.pathname === item.href ··· 89 105 <Link 90 106 key={item.name} 91 107 to={item.href} 92 - className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${ 93 - isActive 94 - ? 'bg-gray-800 text-white' 95 - : 'text-gray-300 hover:bg-gray-700 hover:text-white' 96 - }`} 108 + className={`group flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-200 ${ 109 + isActive 110 + ? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-md' 111 + : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white' 112 + }`} 97 113 > 98 - <Icon className="mr-3 h-5 w-5" /> 114 + <Icon className={`mr-3 h-4 w-4 flex-shrink-0 ${ 115 + isActive ? 'text-white' : 'text-slate-500 dark:text-slate-400 group-hover:text-blue-500' 116 + }`} /> 99 117 {item.name} 100 118 </Link> 101 119 ) 102 120 })} 103 121 </nav> 104 122 105 - {/* User info and logout */} 106 - <div className="border-t border-gray-700 p-4"> 107 - <div className="flex items-center mb-3"> 123 + <div className="border-t border-slate-200/50 dark:border-slate-700/50 p-4"> 124 + <div className="flex items-center mb-4 p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"> 108 125 <div className="flex-shrink-0"> 109 126 {user?.avatar ? ( 110 127 <img 111 - className="h-8 w-8 rounded-full" 128 + className="h-9 w-9 rounded-full ring-2 ring-slate-200 dark:ring-slate-600" 112 129 src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`} 113 130 alt={user.username} 114 131 /> 115 132 ) : ( 116 - <div className="h-8 w-8 rounded-full bg-gray-800 flex items-center justify-center"> 117 - <User className="h-4 w-4 text-white" /> 133 + <div className="h-9 w-9 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center ring-2 ring-slate-200 dark:ring-slate-600"> 134 + <img 135 + className="h-5 w-5" 136 + src="/bot_icon.png" 137 + alt="Bot Icon" 138 + /> 118 139 </div> 119 140 )} 120 141 </div> 121 - <div className="ml-3"> 122 - <p className="text-sm font-medium text-gray-300"> 142 + <div className="ml-3 flex-1 min-w-0"> 143 + <p className="text-sm font-medium text-slate-900 dark:text-white truncate"> 123 144 {user?.discriminator && user.discriminator !== '0' 124 145 ? `${user.username}#${user.discriminator}` 125 146 : user?.username} 147 + </p> 148 + <p className="text-xs text-slate-500 dark:text-slate-400 truncate"> 149 + {user?.email} 126 150 </p> 127 151 </div> 128 152 </div> 129 153 <button 130 154 onClick={handleLogout} 131 - className="flex w-full items-center px-2 py-2 text-sm font-medium text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white" 155 + className="flex w-full items-center px-3 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 transition-all duration-200 group" 132 156 > 133 - <LogOut className="mr-3 h-5 w-5" /> 157 + <LogOut className="mr-3 h-4 w-4 flex-shrink-0 group-hover:text-red-500" /> 134 158 Logout 135 159 </button> 136 160 </div> 137 161 </div> 138 162 </div> 139 163 140 - {/* Main content */} 141 164 <div className="lg:pl-64"> 142 - {/* Mobile header */} 143 - <div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-700 bg-gray-900/50 px-4 shadow-sm lg:hidden"> 165 + <div className="sticky top-0 z-40 flex h-14 shrink-0 items-center gap-x-4 border-b border-slate-200/50 dark:border-slate-700/50 bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm px-4 lg:hidden"> 144 166 <button 145 167 type="button" 146 - className="-m-2.5 p-2.5 text-gray-300 lg:hidden" 168 + className="p-2 text-slate-500 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors lg:hidden" 147 169 onClick={() => setSidebarOpen(true)} 148 170 > 149 - <Menu className="h-6 w-6" /> 171 + <Menu className="h-5 w-5" /> 150 172 </button> 151 - <div className="flex-1 text-sm font-semibold leading-6 text-white"> 152 - Aethel Dashboard 173 + <div className="flex-1 flex items-center space-x-3"> 174 + <img 175 + className="w-7 h-7 rounded-lg" 176 + src="/bot_icon.png" 177 + alt="Aethel Bot" 178 + /> 179 + <span className="text-lg font-bold text-slate-900 dark:text-white"> 180 + Aethel 181 + </span> 153 182 </div> 154 183 </div> 155 184 156 - {/* Page content */} 157 - <main className="pt-20 pb-12"> 158 - <div className="mx-auto max-w-6xl px-12 sm:px-16 lg:px-20"> 185 + <main className="p-4 lg:p-6"> 186 + <div className="mx-auto max-w-7xl"> 159 187 {children} 160 188 </div> 161 189 </main>
+42
web/src/components/LegalLayout.tsx
··· 1 + import { ReactNode } from 'react'; 2 + import { Link } from 'react-router-dom'; 3 + 4 + interface LegalLayoutProps { 5 + title: string; 6 + lastUpdated: string; 7 + children: ReactNode; 8 + } 9 + 10 + export function LegalLayout({ title, lastUpdated, children }: LegalLayoutProps) { 11 + return ( 12 + <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8"> 13 + <div className="max-w-5xl mx-auto"> 14 + <div className="text-center mb-8"> 15 + <Link 16 + to="/" 17 + className="inline-flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors mb-6" 18 + > 19 + <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 20 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /> 21 + </svg> 22 + Back to Home 23 + </Link> 24 + <h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">{title}</h1> 25 + <div className="w-24 h-1 bg-sky-500 mx-auto my-4 rounded-full"></div> 26 + <p className="text-sm text-gray-500 dark:text-gray-400">Last Updated: {lastUpdated}</p> 27 + </div> 28 + 29 + <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden"> 30 + <div className="p-8 sm:p-10 lg:p-12"> 31 + <div className="prose dark:prose-invert max-w-none"> 32 + {children} 33 + </div> 34 + </div> 35 + </div> 36 + 37 + </div> 38 + </div> 39 + ); 40 + } 41 + 42 + export default LegalLayout;
+261 -167
web/src/pages/DashboardPage.tsx
··· 1 1 import { useQuery } from '@tanstack/react-query' 2 - import { CheckSquare, Key, Clock, TrendingUp, Bell, AlertCircle } from 'lucide-react' 2 + import { CheckSquare, Key, Clock, TrendingUp, Bell, AlertCircle, User } from 'lucide-react' 3 3 import { todosAPI, apiKeysAPI, remindersAPI } from '../lib/api' 4 4 import { useAuthStore } from '../stores/authStore' 5 5 import { useEffect, useState } from 'react' ··· 8 8 const DashboardPage = () => { 9 9 const { user } = useAuthStore() 10 10 const [notifications, setNotifications] = useState<any[]>([]) 11 + 11 12 12 13 const { data: todos } = useQuery({ 13 14 queryKey: ['todos'], ··· 27 28 const { data: activeReminders } = useQuery({ 28 29 queryKey: ['active-reminders'], 29 30 queryFn: () => remindersAPI.getActiveReminders().then(res => res.data.reminders), 30 - refetchInterval: 30000, // Check every 30 seconds 31 + refetchInterval: 30000, 31 32 }) 32 33 33 34 const completedTodos = todos?.filter((todo: any) => todo.done).length || 0 ··· 45 46 name: 'Total Todos', 46 47 value: totalTodos, 47 48 icon: CheckSquare, 48 - color: 'text-blue-600', 49 - bgColor: 'bg-blue-100', 50 49 }, 51 50 { 52 51 name: 'Completed', 53 52 value: completedTodos, 54 53 icon: TrendingUp, 55 - color: 'text-green-600', 56 - bgColor: 'bg-green-100', 57 54 }, 58 55 { 59 56 name: 'Pending', 60 57 value: pendingTodos, 61 58 icon: Clock, 62 - color: 'text-yellow-600', 63 - bgColor: 'bg-yellow-100', 64 59 }, 65 60 { 66 61 name: 'Active Reminders', 67 62 value: activeRemindersCount, 68 63 icon: Bell, 69 - color: overdueReminders.length > 0 ? 'text-red-600' : 'text-blue-600', 70 - bgColor: overdueReminders.length > 0 ? 'bg-red-100' : 'bg-blue-100', 71 64 }, 72 65 { 73 66 name: 'API Key', 74 67 value: hasApiKey ? 'Configured' : 'Not Set', 75 68 icon: Key, 76 - color: hasApiKey ? 'text-green-600' : 'text-red-600', 77 - bgColor: hasApiKey ? 'bg-green-100' : 'bg-red-100', 78 69 }, 79 70 ] 80 71 ··· 99 90 } 100 91 }, [overdueReminders]) 101 92 93 + 94 + 102 95 const handleCompleteReminder = async (id: string) => { 103 96 try { 104 97 await remindersAPI.completeReminder(id) ··· 109 102 } 110 103 } 111 104 112 - const formatDate = (dateString: string) => { 113 - const date = new Date(dateString) 114 - return date.toLocaleString() 115 - } 105 + 116 106 117 - const isExpired = (dateString: string) => { 118 - return new Date(dateString) < new Date() 119 - } 120 107 121 108 return ( 122 109 <div className="space-y-6"> 123 - {/* Header */} 124 - <div> 125 - <h1 className="text-2xl font-bold text-white"> 126 - Welcome back, {user?.username}! 127 - </h1> 128 - <p className="text-gray-400"> 129 - Here's an overview of your todos and settings. 130 - </p> 110 + <div className="relative bg-white dark:bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300"> 111 + <div className="flex items-center justify-between"> 112 + <div className="flex items-center space-x-4"> 113 + <div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center"> 114 + <User className="h-6 w-6 text-white" /> 115 + </div> 116 + <div> 117 + <h1 className="text-2xl font-semibold text-slate-900 dark:text-white"> 118 + Welcome back, {user?.username}! 119 + </h1> 120 + <p className="text-slate-600 dark:text-slate-400 text-sm"> 121 + Here's your productivity overview for today. 122 + </p> 123 + </div> 124 + </div> 125 + <div className="hidden lg:block"> 126 + <div className="text-right"> 127 + <p className="text-slate-500 dark:text-slate-400 text-xs">{new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p> 128 + <p className="text-slate-700 dark:text-slate-300 text-lg font-semibold">{new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</p> 129 + </div> 130 + </div> 131 + </div> 131 132 </div> 132 133 133 - {/* Stats Grid */} 134 - <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4"> 135 - {stats.map((stat) => { 134 + <div className="grid grid-cols-2 lg:grid-cols-5 gap-4"> 135 + {stats.map((stat, index) => { 136 136 const Icon = stat.icon 137 + const colors = [ 138 + 'bg-blue-600', 139 + 'bg-emerald-600', 140 + 'bg-orange-500', 141 + 'bg-purple-600', 142 + 'bg-indigo-600' 143 + ] 144 + 137 145 return ( 138 - <div key={stat.name} className="bg-gray-900/50 border border-gray-700 rounded-lg p-5"> 139 - <div className="flex items-center"> 140 - <div className="flex-shrink-0"> 141 - <div className={`p-3 rounded-lg ${stat.bgColor}`}> 142 - <Icon className={`h-6 w-6 ${stat.color}`} /> 143 - </div> 146 + <div 147 + key={stat.name} 148 + className="relative bg-white/80 dark:bg-slate-800/50 backdrop-blur-sm p-4 rounded-xl border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300" 149 + > 150 + <div className="flex items-center justify-between mb-3"> 151 + <div className={`w-10 h-10 ${colors[index]} rounded-lg flex items-center justify-center`}> 152 + <Icon className="h-5 w-5 text-white" /> 144 153 </div> 145 - <div className="ml-5 w-0 flex-1"> 146 - <dl> 147 - <dt className="text-sm font-medium text-gray-400 truncate"> 148 - {stat.name} 149 - </dt> 150 - <dd className="text-lg font-medium text-white"> 151 - {stat.value} 152 - </dd> 153 - </dl> 154 - </div> 154 + </div> 155 + <div> 156 + <p className="text-2xl font-bold text-slate-900 dark:text-white mb-1">{stat.value}</p> 157 + <p className="text-xs font-medium text-slate-600 dark:text-slate-400">{stat.name}</p> 155 158 </div> 156 159 </div> 157 160 ) 158 161 })} 159 162 </div> 160 163 161 - {/* Overdue Reminders Alert */} 162 164 {overdueReminders.length > 0 && ( 163 - <div className="bg-red-900/20 border border-red-700 rounded-lg p-4"> 164 - <div className="flex items-center"> 165 - <AlertCircle className="h-5 w-5 text-red-600 mr-2" /> 166 - <h3 className="text-sm font-medium text-red-300"> 167 - You have {overdueReminders.length} overdue reminder{overdueReminders.length > 1 ? 's' : ''} 168 - </h3> 165 + <div className="bg-red-900/20 border border-red-600 p-6 rounded-lg"> 166 + <div className="flex items-center mb-4"> 167 + <div className="w-12 h-12 bg-red-600 rounded-lg flex items-center justify-center mr-4"> 168 + <AlertCircle className="h-6 w-6 text-white" /> 169 + </div> 170 + <div> 171 + <h3 className="text-xl font-bold text-white mb-1"> 172 + Overdue Reminders 173 + </h3> 174 + <p className="text-red-200 text-sm"> 175 + You have {overdueReminders.length} reminder{overdueReminders.length > 1 ? 's' : ''} that need immediate attention 176 + </p> 177 + </div> 169 178 </div> 170 - <div className="mt-2 space-y-1"> 179 + <div className="space-y-3"> 171 180 {overdueReminders.slice(0, 3).map((reminder: any) => ( 172 - <div key={reminder.reminder_id} className="flex items-center justify-between"> 173 - <p className="text-sm text-red-200 truncate">{reminder.message}</p> 174 - <button 175 - onClick={() => handleCompleteReminder(reminder.reminder_id)} 176 - className="text-xs bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded transition-colors" 177 - > 178 - Complete 179 - </button> 181 + <div key={reminder.reminder_id} className="bg-slate-800 border border-red-600 rounded-lg p-4 hover:bg-slate-700 transition-colors"> 182 + <div className="flex items-center justify-between"> 183 + <div className="flex-1 mr-4"> 184 + <p className="text-white font-semibold mb-1">{reminder.message}</p> 185 + <div className="flex items-center space-x-4 text-sm"> 186 + <p className="text-red-300"> 187 + Due: {new Date(reminder.expires_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} 188 + </p> 189 + <span className="px-2 py-1 bg-red-600 text-white rounded text-xs"> 190 + {Math.ceil((Date.now() - new Date(reminder.expires_at).getTime()) / (1000 * 60 * 60 * 24))} days overdue 191 + </span> 192 + </div> 193 + </div> 194 + <button 195 + onClick={() => handleCompleteReminder(reminder.reminder_id)} 196 + className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors font-medium" 197 + > 198 + ✓ Complete 199 + </button> 200 + </div> 180 201 </div> 181 202 ))} 182 203 {overdueReminders.length > 3 && ( 183 - <p className="text-xs text-red-400">And {overdueReminders.length - 3} more...</p> 204 + <div className="text-center pt-3"> 205 + <p className="text-red-300 text-sm">And {overdueReminders.length - 3} more overdue reminders...</p> 206 + <a href="/reminders" className="mt-2 text-red-400 hover:text-red-300 text-sm underline"> 207 + View all overdue reminders → 208 + </a> 209 + </div> 184 210 )} 185 211 </div> 186 212 </div> 187 213 )} 188 214 189 - {/* Recent Activity */} 190 - <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> 191 - {/* Recent Todos */} 192 - <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 215 + <div className="relative bg-white/80 dark:bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300"> 216 + <h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4"> 217 + Quick Actions 218 + </h3> 219 + <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> 220 + <a 221 + href="/todos" 222 + className="group bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg p-4 hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200" 223 + > 224 + <div className="flex items-center"> 225 + <div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center mr-3"> 226 + <CheckSquare className="h-5 w-5 text-white" /> 227 + </div> 228 + <div> 229 + <p className="font-semibold text-slate-900 dark:text-white">Manage Todos</p> 230 + <p className="text-xs text-slate-600 dark:text-slate-400">View and organize tasks</p> 231 + </div> 232 + </div> 233 + </a> 234 + <a 235 + href="/reminders" 236 + className="group bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg p-4 hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200" 237 + > 238 + <div className="flex items-center"> 239 + <div className="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center mr-3"> 240 + <Bell className="h-5 w-5 text-white" /> 241 + </div> 242 + <div> 243 + <p className="font-semibold text-slate-900 dark:text-white">Set Reminder</p> 244 + <p className="text-xs text-slate-600 dark:text-slate-400">Schedule notifications</p> 245 + </div> 246 + </div> 247 + </a> 248 + <a 249 + href="/api-keys" 250 + className="group bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg p-4 hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200" 251 + > 252 + <div className="flex items-center"> 253 + <div className="w-10 h-10 bg-purple-600 rounded-lg flex items-center justify-center mr-3"> 254 + <Key className="h-5 w-5 text-white" /> 255 + </div> 256 + <div> 257 + <p className="font-semibold text-slate-900 dark:text-white">AI Config</p> 258 + <p className="text-xs text-slate-600 dark:text-slate-400">Configure AI settings</p> 259 + </div> 260 + </div> 261 + </a> 262 + </div> 263 + </div> 264 + 265 + <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> 266 + <div className="relative bg-white/80 dark:bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300"> 193 267 <div className="flex items-center justify-between mb-4"> 194 - <h2 className="text-lg font-medium text-white">Recent Todos</h2> 268 + <h3 className="text-lg font-semibold text-slate-900 dark:text-white flex items-center"> 269 + <div className="w-6 h-6 bg-blue-600 rounded-md flex items-center justify-center mr-2"> 270 + <CheckSquare className="h-3 w-3 text-white" /> 271 + </div> 272 + Recent Todos 273 + </h3> 195 274 <a 196 275 href="/todos" 197 - className="text-sm text-white hover:text-gray-300" 276 + className="text-blue-500 hover:text-blue-600 text-xs font-medium transition-colors" 198 277 > 199 - View all 278 + View all → 200 279 </a> 201 280 </div> 202 - {recentTodos.length > 0 ? ( 281 + {recentTodos.length === 0 ? ( 282 + <div className="text-center py-8"> 283 + <div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center mx-auto mb-3"> 284 + <CheckSquare className="h-6 w-6 text-white" /> 285 + </div> 286 + <p className="text-slate-600 dark:text-slate-400 text-sm mb-3">No todos yet</p> 287 + <a 288 + href="/todos" 289 + className="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-medium transition-all duration-200" 290 + > 291 + Create your first todo 292 + </a> 293 + </div> 294 + ) : ( 203 295 <div className="space-y-3"> 204 296 {recentTodos.map((todo: any) => ( 205 - <div key={todo.id} className="flex items-center space-x-3"> 206 - <div className={`flex-shrink-0 w-2 h-2 rounded-full ${ 207 - todo.done ? 'bg-green-400' : 'bg-yellow-400' 208 - }`} /> 209 - <span className={`text-sm ${ 210 - todo.done ? 'text-gray-500 line-through' : 'text-white' 297 + <div key={todo.id} className="flex items-center justify-between p-3 bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200"> 298 + <div className="flex items-center space-x-3"> 299 + <div className={`w-4 h-4 rounded-full border-2 transition-colors ${ 300 + todo.done ? 'bg-emerald-500 border-emerald-500' : 'bg-white dark:bg-slate-600 border-slate-300 dark:border-slate-400' 301 + }`}></div> 302 + <div> 303 + <p className={`font-medium text-sm ${ 304 + todo.done ? 'text-slate-500 dark:text-slate-400 line-through' : 'text-slate-900 dark:text-white' 305 + }`}> 306 + {todo.item} 307 + </p> 308 + </div> 309 + </div> 310 + <span className={`px-2 py-1 rounded-md text-xs font-medium ${ 311 + todo.done 312 + ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' 313 + : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' 211 314 }`}> 212 - {todo.item} 315 + {todo.done ? 'Done' : 'Pending'} 213 316 </span> 214 317 </div> 215 318 ))} 216 319 </div> 217 - ) : ( 218 - <p className="text-gray-400 text-sm">No todos yet. Create your first one!</p> 219 320 )} 220 321 </div> 221 322 222 - {/* Recent Reminders */} 223 - <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 323 + <div className="relative bg-white/80 dark:bg-slate-800/50 backdrop-blur-sm rounded-xl p-6 border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300"> 224 324 <div className="flex items-center justify-between mb-4"> 225 - <h2 className="text-lg font-medium text-white">Recent Reminders</h2> 325 + <h3 className="text-lg font-semibold text-slate-900 dark:text-white flex items-center"> 326 + <div className="w-6 h-6 bg-emerald-600 rounded-md flex items-center justify-center mr-2"> 327 + <Bell className="h-3 w-3 text-white" /> 328 + </div> 329 + Recent Reminders 330 + </h3> 226 331 <a 227 332 href="/reminders" 228 - className="text-sm text-white hover:text-gray-300" 333 + className="text-emerald-500 hover:text-emerald-600 text-xs font-medium transition-colors" 229 334 > 230 - View all 335 + View all → 231 336 </a> 232 337 </div> 233 - {recentReminders.length > 0 ? ( 338 + {recentReminders.length === 0 ? ( 339 + <div className="text-center py-8"> 340 + <div className="w-12 h-12 bg-emerald-600 rounded-xl flex items-center justify-center mx-auto mb-3"> 341 + <Bell className="h-6 w-6 text-white" /> 342 + </div> 343 + <p className="text-slate-600 dark:text-slate-400 text-sm mb-3">No reminders yet</p> 344 + <a 345 + href="/reminders" 346 + className="inline-flex items-center px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-medium transition-all duration-200" 347 + > 348 + Set your first reminder 349 + </a> 350 + </div> 351 + ) : ( 234 352 <div className="space-y-3"> 235 353 {recentReminders.map((reminder: any) => ( 236 - <div key={reminder.reminder_id} className="flex items-start space-x-3"> 237 - <div className={`flex-shrink-0 w-2 h-2 rounded-full mt-2 ${ 238 - reminder.is_completed 239 - ? 'bg-green-400' 240 - : isExpired(reminder.expires_at) 241 - ? 'bg-red-400' 242 - : 'bg-blue-400' 243 - }`} /> 244 - <div className="flex-1 min-w-0"> 245 - <p className={`text-sm ${ 246 - reminder.is_completed ? 'text-gray-500 line-through' : 'text-white' 354 + <div key={reminder.reminder_id} className="p-3 bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200"> 355 + <div className="flex items-center justify-between mb-2"> 356 + <p className="font-medium text-slate-900 dark:text-white text-sm">{reminder.message}</p> 357 + <span className={`px-2 py-1 rounded-md text-xs font-medium ${ 358 + new Date(reminder.expires_at) < new Date() 359 + ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' 360 + : 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400' 247 361 }`}> 248 - {reminder.message} 249 - </p> 250 - <p className="text-xs text-gray-400 mt-1"> 251 - {formatDate(reminder.expires_at)} 252 - </p> 362 + {new Date(reminder.expires_at) < new Date() ? 'Overdue' : 'Active'} 363 + </span> 253 364 </div> 365 + <p className="text-xs text-slate-600 dark:text-slate-400"> 366 + Due: {new Date(reminder.expires_at).toLocaleDateString()} 367 + </p> 254 368 </div> 255 369 ))} 256 370 </div> 257 - ) : ( 258 - <p className="text-gray-400 text-sm">No reminders yet. Create your first one!</p> 259 371 )} 260 372 </div> 373 + </div> 261 374 262 - {/* API Key Status */} 263 - <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 264 - <div className="flex items-center justify-between mb-4"> 265 - <h2 className="text-lg font-medium text-white">AI Configuration</h2> 375 + <div className="relative bg-white/80 dark:bg-slate-800/50 backdrop-blur-sm rounded-xl p-5 border border-slate-200/50 dark:border-slate-700/50 transition-all duration-300"> 376 + <div className="flex items-center justify-between mb-4"> 377 + <h3 className="text-lg font-semibold text-slate-900 dark:text-white flex items-center"> 378 + <div className="w-6 h-6 bg-purple-600 rounded-md flex items-center justify-center mr-2"> 379 + <Key className="h-3 w-3 text-white" /> 380 + </div> 381 + AI Configuration 382 + </h3> 383 + <a 384 + href="/api-keys" 385 + className="text-purple-500 hover:text-purple-600 text-xs font-medium transition-colors" 386 + > 387 + Manage → 388 + </a> 389 + </div> 390 + {!hasApiKey ? ( 391 + <div className="text-center py-6"> 392 + <div className="w-10 h-10 bg-purple-600 rounded-xl flex items-center justify-center mx-auto mb-3"> 393 + <Key className="h-5 w-5 text-white" /> 394 + </div> 395 + <p className="text-slate-600 dark:text-slate-400 text-sm mb-3">No API keys configured</p> 266 396 <a 267 397 href="/api-keys" 268 - className="text-sm text-white hover:text-gray-300" 398 + className="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-medium transition-all duration-200" 269 399 > 270 - Manage 400 + Add API Key 271 401 </a> 272 402 </div> 273 - <div className="space-y-3"> 274 - <div className="flex items-center justify-between"> 275 - <span className="text-sm text-gray-400">API Key</span> 276 - <span className={`text-sm font-medium ${ 277 - hasApiKey ? 'text-green-600' : 'text-red-600' 278 - }`}> 279 - {hasApiKey ? 'Configured' : 'Not Set'} 280 - </span> 281 - </div> 282 - {apiKeyInfo?.model && ( 283 - <div className="flex items-center justify-between"> 284 - <span className="text-sm text-gray-400">Model</span> 285 - <span className="text-sm font-medium text-white"> 286 - {apiKeyInfo.model} 287 - </span> 403 + ) : ( 404 + <div className="p-3 bg-slate-50/80 dark:bg-slate-700/30 border border-slate-200/50 dark:border-slate-600/50 rounded-lg hover:bg-slate-100/80 dark:hover:bg-slate-700/50 transition-all duration-200"> 405 + <div className="flex items-center space-x-3"> 406 + <div className="w-8 h-8 bg-purple-600 rounded-lg flex items-center justify-center"> 407 + <Key className="h-4 w-4 text-white" /> 408 + </div> 409 + <div className="flex-1"> 410 + <p className="font-medium text-slate-900 dark:text-white text-sm">AI Configuration</p> 411 + <div className="text-xs text-slate-600 dark:text-slate-400"> 412 + {apiKeyInfo?.model && ( 413 + <span>Model: {apiKeyInfo.model}</span> 414 + )} 415 + {apiKeyInfo?.apiUrl && ( 416 + <span className="ml-3">Endpoint: {new URL(apiKeyInfo.apiUrl).hostname}</span> 417 + )} 418 + </div> 288 419 </div> 289 - )} 290 - {apiKeyInfo?.apiUrl && ( 291 - <div className="flex items-center justify-between"> 292 - <span className="text-sm text-gray-400">Endpoint</span> 293 - <span className="text-sm font-medium text-white truncate max-w-32"> 294 - {new URL(apiKeyInfo.apiUrl).hostname} 295 - </span> 420 + <div className="flex items-center space-x-2"> 421 + <div className="w-2 h-2 bg-emerald-500 rounded-full"></div> 422 + <span className="text-xs font-medium text-emerald-600 dark:text-emerald-400">Active</span> 296 423 </div> 297 - )} 298 - {!hasApiKey && ( 299 - <p className="text-sm text-gray-400"> 300 - Configure your AI API key to use custom models and endpoints. 301 - </p> 302 - )} 424 + </div> 303 425 </div> 304 - </div> 305 - </div> 306 - 307 - {/* Quick Actions */} 308 - <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 309 - <h2 className="text-lg font-medium text-white mb-4">Quick Actions</h2> 310 - <div className="flex flex-wrap gap-3"> 311 - <a 312 - href="/todos" 313 - className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 314 - > 315 - <CheckSquare className="h-4 w-4 mr-2" /> 316 - Manage Todos 317 - </a> 318 - <a 319 - href="/reminders" 320 - className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 321 - > 322 - <Bell className="h-4 w-4 mr-2" /> 323 - Manage Reminders 324 - </a> 325 - <a 326 - href="/api-keys" 327 - className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 328 - > 329 - <Key className="h-4 w-4 mr-2" /> 330 - Configure AI 331 - </a> 332 - </div> 426 + )} 333 427 </div> 334 428 </div> 335 429 )
+95 -136
web/src/pages/LandingPage.tsx
··· 1 - import { Bot, MessageSquare, Cloud, Bell, Shield, Zap } from 'lucide-react'; 2 - import { Link } from 'react-router-dom'; 1 + import React from "react"; 2 + import { 3 + ChatBubbleLeftRightIcon, 4 + BookOpenIcon, 5 + CloudIcon, 6 + FaceSmileIcon, 7 + BellAlertIcon, 8 + PhotoIcon, 9 + SparklesIcon, 10 + GlobeAltIcon 11 + } from '@heroicons/react/24/outline'; 12 + import Footer from '../components/Footer'; 3 13 4 - const LandingPage = () => { 14 + export default function Home() { 5 15 return ( 6 - <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 - {/* Header */} 8 - <header> 9 - <div className="max-w-6xl mx-auto px-6 py-4"> 10 - <div className="flex justify-between items-center"> 11 - <div className="flex items-center space-x-3"> 12 - <span className="text-xl font-semibold text-white">Aethel</span> 16 + <div className="min-h-screen bg-gray-50 dark:bg-gray-900"> 17 + <div className="max-w-6xl mx-auto px-6 py-12"> 18 + 19 + = <div className="flex justify-between items-center mb-16"> 20 + <div className="flex items-center space-x-4"> 21 + <div className="w-12 h-12 flex items-center justify-center rounded-lg overflow-hidden"> 22 + <img 23 + src="/bot_icon.png" 24 + alt="Bot Icon" 25 + className="w-full h-full object-cover" 26 + width={48} 27 + height={48} 28 + /> 13 29 </div> 14 - 15 - <nav className="hidden md:flex items-center space-x-8"> 16 - <a href="#features" className="text-gray-400 hover:text-white transition-colors"> 17 - Features 18 - </a> 19 - <Link 20 - to="/status" 21 - className="text-gray-400 hover:text-white transition-colors" 22 - > 23 - Status 24 - </Link> 25 - </nav> 26 - 27 - <Link 28 - to="/login" 29 - className="bg-white text-black px-4 py-2 rounded-lg font-medium hover:bg-gray-100 transition-colors" 30 - > 31 - Dashboard 32 - </Link> 30 + <h1 className="text-2xl font-bold text-gray-900 dark:text-white">Aethel</h1> 33 31 </div> 34 - </div> 35 - </header> 36 - 37 - {/* Hero Section */} 38 - <section className="py-32 px-6"> 39 - <div className="max-w-4xl mx-auto text-center"> 40 - <h1 className="text-5xl md:text-6xl font-bold mb-6 text-white"> 41 - A useful Discord user bot 42 - <span className="block text-gray-400 mt-2">for your account</span> 43 - </h1> 44 - 45 - <p className="text-xl text-gray-400 mb-12 max-w-2xl mx-auto"> 46 - Enhance your Discord experience with AI chat, weather updates, reminders, and more useful features. 47 - </p> 48 - 49 32 <a 50 - href={`https://discord.com/api/oauth2/authorize?client_id=${import.meta.env.VITE_DISCORD_CLIENT_ID}`} 33 + href="https://github.com/aethel-labs/aethel" 51 34 target="_blank" 52 35 rel="noopener noreferrer" 53 - className="inline-flex items-center space-x-3 bg-[#5865F2] text-white px-8 py-4 rounded-lg hover:bg-[#4752C4] transition-colors font-medium" 36 + className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" 54 37 > 55 - <svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"> 56 - <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/> 38 + <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"> 39 + <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" /> 57 40 </svg> 58 - <span>Add to Discord</span> 59 41 </a> 60 42 </div> 61 - </section> 62 43 63 - {/* Features Section */} 64 - <section id="features" className="py-24 px-6"> 65 - <div className="max-w-6xl mx-auto"> 66 - <div className="text-center mb-16"> 67 - <h2 className="text-4xl font-bold text-white mb-4">Features</h2> 68 - <p className="text-xl text-gray-400 max-w-2xl mx-auto"> 69 - Everything you need to enhance your Discord experience 70 - </p> 44 + = <div className="text-center mb-20"> 45 + <h2 className="text-5xl font-bold text-gray-900 dark:text-white mb-6"> 46 + A useful Discord user bot for your account 47 + </h2> 48 + <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto"> 49 + Enhance your Discord experience with AI chat, weather updates, reminders, and more useful features. 50 + </p> 51 + <div className="flex flex-col sm:flex-row gap-4 justify-center"> 52 + <a 53 + href="https://discord.com/oauth2/authorize?client_id=1371031984230371369" 54 + className="bg-[#5865F2] hover:bg-[#4752c4] text-white font-medium py-3 px-8 rounded-lg transition-colors inline-flex items-center justify-center gap-2" 55 + target="_blank" 56 + rel="noopener noreferrer" 57 + > 58 + <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> 59 + <path d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0 12.64 12.64 0 00-.617-1.25.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 00-.041-.1 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.292.074.074 0 01.077-.01c3.928 1.8 8.18 1.8 12.061 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.891.077.077 0 00-.041.1c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028 19.84 19.84 0 006.002-3.03.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.942-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.957 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.943-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.957 2.418-2.157 2.418z"/> 60 + </svg> 61 + Add to Discord 62 + </a> 63 + <a 64 + href="/status" 65 + className="bg-white hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 font-medium py-3 px-8 rounded-lg transition-colors" 66 + > 67 + View Status 68 + </a> 71 69 </div> 72 - 73 - <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> 74 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 75 - <div className="bg-blue-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 76 - <MessageSquare className="h-6 w-6 text-blue-400" /> 77 - </div> 78 - <h3 className="text-xl font-semibold text-white mb-4">AI Chat Assistant</h3> 79 - <p className="text-gray-400 leading-relaxed"> 80 - Get intelligent responses and assistance with our advanced AI chat system. 81 - </p> 82 - </div> 83 - 84 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 85 - <div className="bg-green-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 86 - <Cloud className="h-6 w-6 text-green-400" /> 87 - </div> 88 - <h3 className="text-xl font-semibold text-white mb-4">Weather Updates</h3> 89 - <p className="text-gray-400 leading-relaxed"> 90 - Stay informed with real-time weather information for any location. 91 - </p> 92 - </div> 93 - 94 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 95 - <div className="bg-yellow-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 96 - <Bell className="h-6 w-6 text-yellow-400" /> 70 + </div> 71 + 72 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> 73 + {[ 74 + { icon: <ChatBubbleLeftRightIcon className="w-6 h-6" />, name: "/ai", description: "Chat with AI, ask questions, get answers" }, 75 + { icon: <BookOpenIcon className="w-6 h-6" />, name: "/wiki", description: "Get answers directly from Wikipedia" }, 76 + { icon: <CloudIcon className="w-6 h-6" />, name: "/weather", description: "Get local weather information" }, 77 + { icon: <FaceSmileIcon className="w-6 h-6" />, name: "/joke", description: "Get random jokes to brighten your day" }, 78 + { icon: <BellAlertIcon className="w-6 h-6" />, name: "/remind", description: "Set reminders for important messages" }, 79 + { icon: <PhotoIcon className="w-6 h-6" />, name: "/dog & /cat", description: "Get random cute pet images" }, 80 + { icon: <SparklesIcon className="w-6 h-6" />, name: "/8ball", description: "Ask the magic 8ball questions" }, 81 + { icon: <GlobeAltIcon className="w-6 h-6" />, name: "/whois", description: "Lookup domain or IP information" } 82 + ].map((command, index) => ( 83 + <div 84 + key={index} 85 + className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow" 86 + > 87 + <div className="flex items-center"> 88 + <div className="p-2 rounded-lg bg-sky-100 dark:bg-sky-900/30 mr-3"> 89 + {React.cloneElement(command.icon, { className: 'w-6 h-6 text-gray-600 dark:text-white' })} 90 + </div> 91 + <h3 className="font-semibold text-gray-900 dark:text-white"> 92 + {command.name} 93 + </h3> 97 94 </div> 98 - <h3 className="text-xl font-semibold text-white mb-4">Smart Reminders</h3> 99 - <p className="text-gray-400 leading-relaxed"> 100 - Never miss important events with our intelligent reminder system. 95 + <p className="text-gray-600 dark:text-gray-300 text-sm mt-3 leading-relaxed"> 96 + {command.description} 101 97 </p> 102 98 </div> 103 - 104 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 105 - <div className="bg-purple-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 106 - <Shield className="h-6 w-6 text-purple-400" /> 107 - </div> 108 - <h3 className="text-xl font-semibold text-white mb-4">Secure & Private</h3> 109 - <p className="text-gray-400 leading-relaxed"> 110 - Your data is protected with enterprise-grade security measures. 111 - </p> 112 - </div> 113 - 114 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 115 - <div className="bg-orange-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 116 - <Zap className="h-6 w-6 text-orange-400" /> 117 - </div> 118 - <h3 className="text-xl font-semibold text-white mb-4">Lightning Fast</h3> 119 - <p className="text-gray-400 leading-relaxed"> 120 - Experience blazing fast response times and seamless performance. 121 - </p> 122 - </div> 123 - 124 - <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 125 - <div className="bg-indigo-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 126 - <Bot className="h-6 w-6 text-indigo-400" /> 127 - </div> 128 - <h3 className="text-xl font-semibold text-white mb-4">Discord Native</h3> 129 - <p className="text-gray-400 leading-relaxed"> 130 - Built specifically for Discord with seamless integration. 131 - </p> 132 - </div> 133 - </div> 99 + ))} 134 100 </div> 135 - </section> 136 - 137 - {/* Footer */} 138 - <footer className="py-12 px-6"> 139 - <div className="max-w-6xl mx-auto text-center"> 140 - <div className="flex items-center justify-center space-x-3 mb-4"> 141 - <span className="text-lg font-semibold text-white">Aethel</span> 142 - </div> 143 - <p className="text-gray-400 mb-6"> 144 - A useful Discord user bot for your account 101 + 102 + <div className="mt-16 text-center"> 103 + <p className="text-gray-600 dark:text-gray-400 mb-8"> 104 + Works in DMs and servers that allow external applications 145 105 </p> 146 - <div className="flex justify-center space-x-8 text-sm text-gray-400"> 147 - <a href="#features" className="hover:text-white transition-colors">Features</a> 148 - <a href="/status" className="hover:text-white transition-colors">Status</a> 149 - </div> 106 + <p className="text-gray-500 dark:text-gray-500"> 107 + Made with ♥ by scanash and the Aethel Labs team 108 + </p> 150 109 </div> 151 - </footer> 110 + </div> 111 + 112 + <Footer /> 152 113 </div> 153 - ) 154 - } 155 - 156 - export default LandingPage 114 + ); 115 + }
+31 -47
web/src/pages/PrivacyPage.tsx
··· 1 - import { Link } from 'react-router-dom'; 2 - import { ArrowLeft } from 'lucide-react'; 1 + import { LegalLayout } from '../components/LegalLayout'; 2 + import Footer from '../components/Footer'; 3 3 4 4 export default function PrivacyPolicy() { 5 5 return ( 6 - <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 - {/* Header */} 8 - <header className="border-b border-gray-800"> 9 - <div className="max-w-4xl mx-auto px-6 py-4"> 10 - <Link 11 - to="/" 12 - className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors" 13 - > 14 - <ArrowLeft className="w-4 h-4" /> 15 - Back to Home 16 - </Link> 17 - </div> 18 - </header> 19 - 20 - {/* Content */} 21 - <main className="max-w-4xl mx-auto px-6 py-12"> 22 - <div className="mb-8"> 23 - <h1 className="text-4xl font-bold text-white mb-2">Privacy Policy</h1> 24 - <p className="text-gray-400">Last Updated: July 21, 2025</p> 25 - </div> 6 + <> 7 + <LegalLayout title="Privacy Policy" lastUpdated="July 21, 2025"> 26 8 <div className="space-y-8"> 27 9 <section> 28 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Information We Collect</h2> 29 - <p className="text-gray-400 leading-relaxed"> 10 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">1. Information We Collect</h2> 11 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 30 12 The bot (&quot;the Bot&quot;) collects the following information: 31 13 </p> 32 - <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 14 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300 mt-2"> 33 15 <li>Discord user IDs for command processing and functionality</li> 34 16 <li>Server IDs where the Bot is used</li> 35 17 <li>Channel IDs where commands are used</li> ··· 41 23 </section> 42 24 43 25 <section> 44 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. How We Use Your Information</h2> 45 - <p className="text-gray-400 leading-relaxed"> 26 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">2. How We Use Your Information</h2> 27 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 46 28 We use the collected information to provide, maintain, and improve our Bot&apos;s services, including: 47 29 </p> 48 - <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 30 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300 mt-2"> 49 31 <li>Provide and maintain the Bot&apos;s functionality</li> 50 32 <li>Process commands and provide responses</li> 51 33 <li>Improve the Bot&apos;s performance and features</li> ··· 54 36 </section> 55 37 56 38 <section> 57 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. Data Storage</h2> 58 - <p className="text-gray-400 leading-relaxed"> 39 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">3. Data Storage</h2> 40 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 59 41 We take your privacy seriously: 60 42 </p> 61 - <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 43 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300 mt-2"> 62 44 <li>API keys are securely hashed using industry-standard encryption before being stored in our database</li> 63 - <li>Your custom API keys and model preferences are stored until you choose to remove them using the <code className="bg-gray-800 px-2 py-0.5 rounded text-sm font-mono text-gray-200">/ai use_custom_api:false</code> command</li> 45 + <li>Your custom API keys and model preferences are stored until you choose to remove them using the <code className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-sm font-mono text-gray-800 dark:text-gray-200">/ai use_custom_api:false</code> command</li> 64 46 <li>We log all message content (like Wiki searches, reminders, and 8-ball queries) for monitoring purposes.</li> 65 47 <li>We do not sell or share your personal information with third parties</li> 66 - <li>You can delete your stored API key and preferences at any time by running <code className="bg-gray-800 px-1 rounded">/ai use_custom_api:false</code></li> 48 + <li>You can delete your stored API key and preferences at any time by running <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">/ai use_custom_api:false</code></li> 67 49 </ul> 68 50 </section> 69 51 70 52 <section> 71 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. Third-Party Services</h2> 72 - <p className="text-gray-400 leading-relaxed"> 53 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">4. Third-Party Services</h2> 54 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 73 55 Our Bot may contain links to third-party websites or services that are not operated by us. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. 74 56 </p> 75 - <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 57 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300 mt-2"> 76 58 <li>Discord&apos;s Privacy Policy for user and server data</li> 77 59 <li>OpenRouter&apos;s Privacy Policy for AI chat functionality</li> 78 60 <li>Weather API providers for weather information</li> ··· 81 63 </section> 82 64 83 65 <section> 84 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Data Security</h2> 85 - <p className="text-gray-400 leading-relaxed"> 66 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">5. Data Security</h2> 67 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 86 68 We implement reasonable security measures to protect your information, but no method of transmission over the internet is 100% secure. 87 69 </p> 88 70 </section> 89 71 90 72 <section> 91 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Children&apos;s Privacy</h2> 92 - <p className="text-gray-400 leading-relaxed"> 73 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">6. Children&apos;s Privacy</h2> 74 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 93 75 Our Bot is not intended for use by children under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us. 94 76 </p> 95 77 </section> 96 78 97 79 <section> 98 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Changes to This Policy</h2> 99 - <p className="text-gray-400 leading-relaxed"> 80 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">7. Changes to This Policy</h2> 81 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 100 82 We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 101 83 </p> 102 84 </section> 103 85 104 86 <section> 105 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">8. Contact Us</h2> 106 - <p className="text-gray-400 leading-relaxed"> 107 - If you have any questions about this Privacy Policy, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>. 87 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">8. Contact Us</h2> 88 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 89 + If you have any questions about this Privacy Policy, please contact us at <a href="mailto:scan@scanash.com" className="text-sky-600 dark:text-sky-400 hover:underline font-medium">scan@scanash.com</a>. 108 90 </p> 109 91 </section> 110 92 </div> 111 - </main> 112 - </div> 93 + </LegalLayout> 94 + 95 + <Footer /> 96 + </> 113 97 ); 114 98 }
+283 -282
web/src/pages/StatusPage.tsx
··· 1 - import { useEffect, useState } from 'react' 2 - import { Link } from 'react-router-dom' 3 - import { 4 - Activity, 5 - Clock, 6 - GitBranch, 7 - Home, 8 - RefreshCw, 9 - Server, 10 - Wifi, 11 - WifiOff, 12 - AlertCircle 13 - } from 'lucide-react' 1 + import { useState, useEffect } from 'react'; 2 + import { Link } from "react-router-dom"; 3 + import { CheckCircleIcon, XCircleIcon, ServerIcon, CpuChipIcon, SignalIcon, ClockIcon } from '@heroicons/react/24/outline'; 4 + import Footer from '../components/Footer'; 14 5 15 - interface StatusData { 16 - status: string 17 - uptime: { 18 - days: number 19 - hours: number 20 - minutes: number 21 - seconds: number 6 + async function getGitCommitHash() { 7 + try { 8 + const response = await fetch(`${import.meta.env.VITE_BOT_API_URL || 'https://bot-api.pur.cat'}/api/status`, { 9 + headers: { 10 + 'X-API-Key': import.meta.env.VITE_STATUS_API_KEY || '' 11 + } 12 + }); 13 + 14 + if (!response.ok) { 15 + if (import.meta.env.NODE_ENV !== 'production') { 16 + console.error('Failed to fetch status from bot API:', response.status); 17 + } 18 + return null; 19 + } 20 + 21 + const data = await response.json(); 22 + return data.commitHash || data.version || data.commit || null; 23 + } catch (error) { 24 + if (import.meta.env.NODE_ENV !== 'production') { 25 + console.error('Error fetching from bot API:', error); 26 + } 27 + return null; 22 28 } 23 - botStatus: string 24 - ping: number 25 - lastReady: string | null 26 - commitHash: string 27 29 } 28 30 29 - const StatusPage = () => { 30 - const [statusData, setStatusData] = useState<StatusData | null>(null) 31 - const [loading, setLoading] = useState(true) 32 - const [error, setError] = useState<string | null>(null) 33 - const [lastUpdated, setLastUpdated] = useState<Date>(new Date()) 34 - 35 - const fetchStatus = async () => { 31 + async function getBotStatus() { 32 + try { 33 + const baseUrl = import.meta.env.VITE_BOT_API_URL || "http://localhost:3000"; 34 + const url = `${baseUrl}/api/status`; 35 + 36 + const controller = new AbortController(); 37 + const timeout = 8000; 38 + const timeoutId = setTimeout(() => controller.abort(), timeout); 39 + 36 40 try { 37 - setError(null) 38 - const response = await fetch(`${import.meta.env.VITE_FRONTEND_URL}/api/status`, { 41 + const res = await fetch(url, { 42 + cache: "no-store", 43 + signal: controller.signal, 39 44 headers: { 45 + 'Cache-Control': 'no-cache', 46 + 'Pragma': 'no-cache', 40 47 'X-API-Key': import.meta.env.VITE_STATUS_API_KEY || '' 41 48 } 42 - }) 49 + }); 50 + 51 + clearTimeout(timeoutId); 43 52 44 - if (!response.ok) { 45 - throw new Error(`HTTP ${response.status}: ${response.statusText}`) 53 + if (!res.ok) { 54 + console.error(`Bot API responded with status: ${res.status}`); 55 + return { 56 + status: "offline", 57 + botStatus: "disconnected", 58 + error: `API Error: ${res.status} ${res.statusText}`, 59 + lastChecked: new Date().toISOString() 60 + }; 46 61 } 47 62 48 - const data = await response.json() 49 - setStatusData(data) 50 - setLastUpdated(new Date()) 51 - } catch (err) { 52 - setError(err instanceof Error ? err.message : 'Failed to fetch status') 53 - } finally { 54 - setLoading(false) 63 + const data = await res.json(); 64 + return { ...data, lastChecked: new Date().toISOString() }; 65 + 66 + } catch (fetchError: unknown) { 67 + clearTimeout(timeoutId); 68 + throw fetchError; 55 69 } 70 + 71 + } catch (error: unknown) { 72 + console.error('Error fetching bot status:', error); 73 + const errorMessage = error instanceof Error && error.name === 'AbortError' 74 + ? 'Connection timed out (8s)' 75 + : 'Could not connect to bot service'; 76 + return { 77 + status: "offline", 78 + botStatus: "disconnected", 79 + error: errorMessage, 80 + lastChecked: new Date().toISOString() 81 + }; 56 82 } 83 + } 84 + 85 + export default function Status() { 86 + const [status, setStatus] = useState<any>(null); 87 + const [commitHash, setCommitHash] = useState<string | null>(null); 88 + const [loading, setLoading] = useState(true); 57 89 58 90 useEffect(() => { 59 - fetchStatus() 60 - 61 - const interval = setInterval(fetchStatus, 30000) 62 - return () => clearInterval(interval) 63 - }, []) 91 + const fetchData = async () => { 92 + try { 93 + const [statusData, hashData] = await Promise.all([ 94 + getBotStatus(), 95 + getGitCommitHash() 96 + ]); 97 + setStatus(statusData); 98 + setCommitHash(hashData); 99 + } catch (error) { 100 + console.error('Error fetching data:', error); 101 + } finally { 102 + setLoading(false); 103 + } 104 + }; 64 105 65 - const formatUptime = (uptime: StatusData['uptime']) => { 66 - const parts = [] 67 - if (uptime.days > 0) parts.push(`${uptime.days}d`) 68 - if (uptime.hours > 0) parts.push(`${uptime.hours}h`) 69 - if (uptime.minutes > 0) parts.push(`${uptime.minutes}m`) 70 - if (uptime.seconds > 0 || parts.length === 0) parts.push(`${uptime.seconds}s`) 71 - return parts.join(' ') 72 - } 73 - 74 - const getStatusColor = (status: string) => { 75 - switch (status.toLowerCase()) { 76 - case 'online': 77 - case 'connected': 78 - return 'text-green-400 bg-green-500/20' 79 - case 'offline': 80 - case 'disconnected': 81 - return 'text-red-400 bg-red-500/20' 82 - default: 83 - return 'text-yellow-400 bg-yellow-500/20' 106 + fetchData(); 107 + }, []); 108 + 109 + const isOnline = status?.status === "online"; 110 + 111 + const getUptime = (): { days: number; hours: number; minutes: number; seconds: number } => { 112 + try { 113 + if (!status || !status.uptime) { 114 + return { days: 0, hours: 0, minutes: 0, seconds: 0 }; 115 + } 116 + if (status.uptime && typeof status.uptime === 'number') { 117 + const totalSeconds = status.uptime; 118 + return { 119 + days: Math.floor(totalSeconds / 86400), 120 + hours: Math.floor((totalSeconds % 86400) / 3600), 121 + minutes: Math.floor((totalSeconds % 3600) / 60), 122 + seconds: Math.floor(totalSeconds % 60) 123 + }; 124 + } 125 + else if (status.uptime && typeof status.uptime === 'object') { 126 + return { 127 + days: status.uptime.days || 0, 128 + hours: status.uptime.hours || 0, 129 + minutes: status.uptime.minutes || 0, 130 + seconds: status.uptime.seconds || 0 131 + }; 132 + } 133 + else if (status.uptime && typeof status.uptime === 'string' && !isNaN(parseInt(status.uptime))) { 134 + const uptime = parseInt(status.uptime); 135 + return { 136 + days: Math.floor(uptime / 86400), 137 + hours: Math.floor((uptime % 86400) / 3600), 138 + minutes: Math.floor((uptime % 3600) / 60), 139 + seconds: Math.floor(uptime % 60) 140 + }; 141 + } 142 + return { days: 0, hours: 0, minutes: 0, seconds: 0 }; 143 + } catch (error) { 144 + console.error('Error parsing uptime:', error); 145 + return { days: 0, hours: 0, minutes: 0, seconds: 0 }; 84 146 } 85 - } 147 + }; 148 + 149 + const uptime = getUptime(); 86 150 87 - const getPingColor = (ping: number) => { 88 - if (ping < 100) return 'text-green-400' 89 - if (ping < 300) return 'text-yellow-400' 90 - return 'text-red-400' 151 + if (loading) { 152 + return ( 153 + <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center"> 154 + <div className="text-center"> 155 + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-sky-600 mx-auto mb-4"></div> 156 + <p className="text-gray-600 dark:text-gray-300">Loading status...</p> 157 + </div> 158 + </div> 159 + ); 91 160 } 92 161 93 162 return ( 94 - <div className="min-h-screen bg-[#0A0A0A] text-white"> 95 - {/* Header */} 96 - <header className="border-b border-gray-800"> 97 - <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 98 - <div className="flex justify-between items-center py-6"> 99 - <div className="flex items-center space-x-4"> 100 - <Link to="/" className="flex items-center space-x-2 text-gray-400 hover:text-white transition-colors"> 101 - <Home className="h-5 w-5" /> 102 - <span>Back to Home</span> 103 - </Link> 163 + <div className="min-h-screen bg-gray-50 dark:bg-gray-900"> 164 + <div className="max-w-6xl mx-auto px-6 py-12"> 165 + 166 + <div className="flex justify-between items-center mb-16"> 167 + <div className="flex items-center space-x-4"> 168 + <div className="w-12 h-12 flex items-center justify-center rounded-lg overflow-hidden"> 169 + <img 170 + src="/bot_icon.png" 171 + alt="Bot Icon" 172 + className="w-full h-full object-cover" 173 + width={48} 174 + height={48} 175 + /> 104 176 </div> 105 - <div className="flex items-center space-x-3"> 106 - <span className="text-2xl font-bold text-white">Aethel Status</span> 107 - </div> 108 - <button 109 - onClick={fetchStatus} 110 - disabled={loading} 111 - className="flex items-center space-x-2 px-4 py-2 bg-white text-black rounded-lg hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium" 112 - > 113 - <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} /> 114 - <span>Refresh</span> 115 - </button> 177 + <h1 className="text-2xl font-bold text-gray-900 dark:text-white">Aethel</h1> 116 178 </div> 179 + <a 180 + href="https://github.com/aethel-labs/aethel" 181 + target="_blank" 182 + rel="noopener noreferrer" 183 + className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors" 184 + > 185 + <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"> 186 + <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" /> 187 + </svg> 188 + </a> 117 189 </div> 118 - </header> 119 190 120 - <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 121 - {error && ( 122 - <div className="mb-8 bg-red-900/20 border border-red-800 rounded-lg p-4"> 123 - <div className="flex items-center space-x-2"> 124 - <AlertCircle className="h-5 w-5 text-red-400" /> 125 - <span className="text-red-300 font-medium">Error loading status</span> 126 - </div> 127 - <p className="text-red-400 mt-2">{error}</p> 191 + <div className="text-center mb-20"> 192 + <h2 className="text-5xl font-bold text-gray-900 dark:text-white mb-6"> 193 + Bot Status 194 + </h2> 195 + <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto"> 196 + Monitor the current status and performance of all bot services in real-time. 197 + </p> 198 + <div className="inline-flex items-center px-6 py-3 rounded-lg font-semibold mb-8 bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800"> 199 + {isOnline ? ( 200 + <> 201 + <CheckCircleIcon className="w-5 h-5 text-green-500 mr-2" /> 202 + <span className="text-gray-900 dark:text-white">All Systems Operational</span> 203 + </> 204 + ) : ( 205 + <> 206 + <XCircleIcon className="w-5 h-5 text-red-500 mr-2" /> 207 + <span className="text-gray-900 dark:text-white">Service Disruption</span> 208 + </> 209 + )} 128 210 </div> 129 - )} 211 + </div> 130 212 131 - {loading && !statusData ? ( 132 - <div className="flex items-center justify-center py-12"> 133 - <div className="flex items-center space-x-3"> 134 - <RefreshCw className="h-6 w-6 animate-spin text-white" /> 135 - <span className="text-lg text-gray-400">Loading status...</span> 136 - </div> 137 - </div> 138 - ) : statusData ? ( 139 - <div className="space-y-8"> 140 - {/* Overall Status */} 141 - <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-8"> 142 - <div className="flex items-center justify-between mb-6"> 143 - <h2 className="text-3xl font-bold text-white">System Status</h2> 144 - <div className="text-sm text-gray-400"> 145 - Last updated: {lastUpdated.toLocaleTimeString()} 213 + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16"> 214 + {[ 215 + { 216 + icon: <ServerIcon className="w-6 h-6" />, 217 + title: "Bot Status", 218 + value: isOnline ? 'Online' : 'Offline', 219 + status: isOnline, 220 + description: isOnline ? 'Bot is operational and responding to commands' : (status.error || 'Bot is currently unavailable') 221 + }, 222 + { 223 + icon: <CpuChipIcon className="w-6 h-6" />, 224 + title: "Bot API", 225 + value: status.botStatus === 'connected' ? 'Connected' : 'Disconnected', 226 + status: status.botStatus === 'connected', 227 + description: `API connection is ${status.botStatus === 'connected' ? 'active and stable' : 'currently unavailable'}` 228 + }, 229 + { 230 + icon: <SignalIcon className="w-6 h-6" />, 231 + title: "Response Time", 232 + value: status.ping ? `${status.ping}ms` : 'N/A', 233 + status: status.ping ? status.ping < 200 : false, 234 + description: status.ping ? 235 + (status.ping < 100 ? 'Excellent performance' : status.ping < 200 ? 'Good performance' : 'Slow response') : 236 + 'Response time unavailable' 237 + } 238 + ].map((card, index) => ( 239 + <div key={index} className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> 240 + <div className="flex items-center mb-3"> 241 + <div className={`w-10 h-10 rounded-lg flex items-center justify-center mr-3 ${ 242 + card.status ? 'bg-sky-100 dark:bg-sky-900 text-sky-600 dark:text-sky-400' 243 + : 'bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400' 244 + }`}> 245 + {card.icon} 146 246 </div> 247 + <h3 className="font-semibold text-gray-900 dark:text-white">{card.title}</h3> 147 248 </div> 148 - 149 - <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> 150 - <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 151 - <div className="flex items-center space-x-4"> 152 - <div className={`p-3 rounded-lg ${getStatusColor(statusData.status)}`}> 153 - <Server className="h-6 w-6" /> 154 - </div> 155 - <div> 156 - <p className="text-sm text-gray-400 mb-1">Server Status</p> 157 - <p className="font-semibold text-white text-lg capitalize">{statusData.status}</p> 158 - </div> 159 - </div> 160 - </div> 161 - 162 - <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 163 - <div className="flex items-center space-x-4"> 164 - <div className={`p-3 rounded-lg ${getStatusColor(statusData.botStatus)}`}> 165 - {statusData.botStatus === 'connected' ? ( 166 - <Wifi className="h-6 w-6" /> 167 - ) : ( 168 - <WifiOff className="h-6 w-6" /> 169 - )} 170 - </div> 171 - <div> 172 - <p className="text-sm text-gray-400 mb-1">Bot Status</p> 173 - <p className="font-semibold text-white text-lg capitalize">{statusData.botStatus}</p> 174 - </div> 175 - </div> 176 - </div> 177 - 178 - <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 179 - <div className="flex items-center space-x-4"> 180 - <div className="p-3 rounded-lg bg-blue-500/20 text-blue-400"> 181 - <Activity className="h-6 w-6" /> 182 - </div> 183 - <div> 184 - <p className="text-sm text-gray-400 mb-1">Ping</p> 185 - <p className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}> 186 - {statusData.ping}ms 187 - </p> 188 - </div> 189 - </div> 190 - </div> 191 - 192 - <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 193 - <div className="flex items-center space-x-4"> 194 - <div className="p-3 rounded-lg bg-purple-500/20 text-purple-400"> 195 - <Clock className="h-6 w-6" /> 196 - </div> 197 - <div> 198 - <p className="text-sm text-gray-400 mb-1">Uptime</p> 199 - <p className="font-semibold text-white text-lg"> 200 - {formatUptime(statusData.uptime)} 201 - </p> 202 - </div> 203 - </div> 204 - </div> 249 + <div className="mb-2"> 250 + <span className="text-2xl font-bold text-gray-900 dark:text-white">{card.value}</span> 205 251 </div> 252 + <p className="text-gray-600 dark:text-gray-300 text-sm">{card.description}</p> 206 253 </div> 254 + ))} 255 + </div> 207 256 208 - {/* Detailed Information */} 209 - <div className="grid md:grid-cols-2 gap-6"> 210 - <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 211 - <h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2"> 212 - <Clock className="h-6 w-6 text-purple-400" /> 213 - <span>Uptime Details</span> 214 - </h3> 215 - <div className="space-y-4"> 216 - <div className="flex justify-between items-center"> 217 - <span className="text-gray-400">Days:</span> 218 - <span className="font-semibold text-white text-lg">{statusData.uptime.days}</span> 219 - </div> 220 - <div className="flex justify-between items-center"> 221 - <span className="text-gray-400">Hours:</span> 222 - <span className="font-semibold text-white text-lg">{statusData.uptime.hours}</span> 223 - </div> 224 - <div className="flex justify-between items-center"> 225 - <span className="text-gray-400">Minutes:</span> 226 - <span className="font-semibold text-white text-lg">{statusData.uptime.minutes}</span> 227 - </div> 228 - <div className="flex justify-between items-center"> 229 - <span className="text-gray-400">Seconds:</span> 230 - <span className="font-semibold text-white text-lg">{statusData.uptime.seconds}</span> 231 - </div> 232 - </div> 257 + {isOnline && (uptime.days > 0 || uptime.hours > 0 || uptime.minutes > 0 || uptime.seconds > 0) && ( 258 + <div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow mb-16"> 259 + <div className="flex items-center mb-4"> 260 + <div className="w-10 h-10 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center text-gray-600 dark:text-gray-300 mr-3"> 261 + <ClockIcon className="w-6 h-6" /> 233 262 </div> 234 - 235 - <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 236 - <h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2"> 237 - <GitBranch className="h-6 w-6 text-blue-400" /> 238 - <span>System Information</span> 239 - </h3> 240 - <div className="space-y-4"> 241 - <div className="flex justify-between items-center"> 242 - <span className="text-gray-400">Commit Hash:</span> 243 - <span className="font-mono text-sm bg-gray-800 text-gray-300 px-3 py-1 rounded-lg"> 244 - {statusData.commitHash || 'Unknown'} 245 - </span> 246 - </div> 247 - <div className="flex justify-between items-center"> 248 - <span className="text-gray-400">Last Ready:</span> 249 - <span className="font-medium text-white"> 250 - {statusData.lastReady 251 - ? new Date(statusData.lastReady).toLocaleString() 252 - : 'Never' 253 - } 254 - </span> 255 - </div> 256 - <div className="flex justify-between items-center"> 257 - <span className="text-gray-400">Response Time:</span> 258 - <span className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}> 259 - {statusData.ping}ms 260 - </span> 261 - </div> 262 - </div> 263 + <h3 className="font-semibold text-gray-900 dark:text-white">System Uptime</h3> 264 + </div> 265 + <div className="text-center"> 266 + <div className="text-2xl font-bold text-gray-900 dark:text-white"> 267 + {uptime.days > 0 && `${uptime.days}d `} 268 + {String(uptime.hours).padStart(2, '0')}h {String(uptime.minutes).padStart(2, '0')}m {String(uptime.seconds).padStart(2, '0')}s 263 269 </div> 264 270 </div> 271 + </div> 272 + )} 265 273 266 - {/* Status Indicators */} 267 - <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 268 - <h3 className="text-xl font-semibold text-white mb-6">Service Health</h3> 269 - <div className="grid md:grid-cols-3 gap-4"> 270 - <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 271 - <div className={`w-4 h-4 rounded-full ${ 272 - statusData.status === 'online' ? 'bg-green-500' : 'bg-red-500' 273 - }`}></div> 274 - <span className="text-gray-300 font-medium">API Server</span> 275 - <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 276 - statusData.status === 'online' 277 - ? 'bg-green-500/20 text-green-400' 278 - : 'bg-red-500/20 text-red-400' 279 - }`}> 280 - {statusData.status} 281 - </span> 282 - </div> 283 - 284 - <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 285 - <div className={`w-4 h-4 rounded-full ${ 286 - statusData.botStatus === 'connected' ? 'bg-green-500' : 'bg-red-500' 287 - }`}></div> 288 - <span className="text-gray-300 font-medium">Discord Bot</span> 289 - <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 290 - statusData.botStatus === 'connected' 291 - ? 'bg-green-500/20 text-green-400' 292 - : 'bg-red-500/20 text-red-400' 293 - }`}> 294 - {statusData.botStatus} 295 - </span> 296 - </div> 297 - 298 - <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 299 - <div className={`w-4 h-4 rounded-full ${ 300 - statusData.ping < 300 ? 'bg-green-500' : 'bg-yellow-500' 301 - }`}></div> 302 - <span className="text-gray-300 font-medium">Network</span> 303 - <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 304 - statusData.ping < 100 305 - ? 'bg-green-500/20 text-green-400' 306 - : statusData.ping < 300 307 - ? 'bg-yellow-500/20 text-yellow-400' 308 - : 'bg-red-500/20 text-red-400' 309 - }`}> 310 - {statusData.ping}ms 311 - </span> 312 - </div> 313 - </div> 274 + <div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow mb-16"> 275 + <h3 className="font-semibold text-gray-900 dark:text-white mb-4">Version Information</h3> 276 + <div className="flex flex-col sm:flex-row items-start sm:items-center space-y-4 sm:space-y-0 sm:space-x-6"> 277 + <div className="flex items-center bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg px-4 py-2"> 278 + <svg className="w-5 h-5 text-gray-500 dark:text-gray-400 mr-3" fill="currentColor" viewBox="0 0 24 24"> 279 + <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" /> 280 + </svg> 281 + {commitHash ? ( 282 + <a 283 + href={`https://github.com/scanash00/bot/commit/${commitHash}`} 284 + target="_blank" 285 + rel="noopener noreferrer" 286 + className="font-mono text-sm font-medium text-gray-700 dark:text-gray-200 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" 287 + title={commitHash} 288 + > 289 + {commitHash.substring(0, 7)} 290 + </a> 291 + ) : ( 292 + <span className="font-mono text-sm font-medium text-gray-700 dark:text-gray-200">unknown</span> 293 + )} 314 294 </div> 295 + <span className="text-sm text-gray-600 dark:text-gray-300"> 296 + Last Updated: {status.lastReady ? new Date(status.lastReady).toLocaleString(undefined, { 297 + month: 'short', 298 + day: 'numeric', 299 + hour: '2-digit', 300 + minute: '2-digit', 301 + hour12: false 302 + }) : 'Unknown'} 303 + </span> 315 304 </div> 316 - ) : null} 305 + </div> 306 + 307 + <div className="text-center"> 308 + <Link 309 + to="/" 310 + className="bg-sky-600 hover:bg-sky-700 text-white font-semibold py-3 px-8 rounded-lg transition-colors inline-flex items-center" 311 + > 312 + <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 313 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /> 314 + </svg> 315 + Back to Home 316 + </Link> 317 + </div> 317 318 </div> 319 + 320 + <Footer /> 318 321 </div> 319 - ) 320 - } 321 - 322 - export default StatusPage 322 + ); 323 + }
+26 -42
web/src/pages/TermsPage.tsx
··· 1 - import { Link } from 'react-router-dom'; 2 - import { ArrowLeft } from 'lucide-react'; 1 + import { LegalLayout } from '../components/LegalLayout'; 2 + import Footer from '../components/Footer'; 3 3 4 4 export default function TermsOfService() { 5 5 return ( 6 - <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 - {/* Header */} 8 - <header className="border-b border-gray-800"> 9 - <div className="max-w-4xl mx-auto px-6 py-4"> 10 - <Link 11 - to="/" 12 - className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors" 13 - > 14 - <ArrowLeft className="w-4 h-4" /> 15 - Back to Home 16 - </Link> 17 - </div> 18 - </header> 19 - 20 - {/* Content */} 21 - <main className="max-w-4xl mx-auto px-6 py-12"> 22 - <div className="mb-8"> 23 - <h1 className="text-4xl font-bold text-white mb-2">Terms of Service</h1> 24 - <p className="text-gray-400">Last Updated: June 16, 2025</p> 25 - </div> 6 + <> 7 + <LegalLayout title="Terms of Service" lastUpdated="June 16, 2025"> 26 8 <div className="space-y-8"> 27 9 <section> 28 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Acceptance of Terms</h2> 29 - <p className="text-gray-400 leading-relaxed"> 10 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">1. Acceptance of Terms</h2> 11 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 30 12 By using the Bot, you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the Bot. 31 13 </p> 32 14 </section> 33 15 34 16 <section> 35 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. Description of Service</h2> 36 - <p className="text-gray-400 leading-relaxed mb-4"> 17 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">2. Description of Service</h2> 18 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-4"> 37 19 The Bot provides various Discord utilities including but not limited to: reminders, random cat and dog images, weather information, wiki lookups, and fun commands. You agree to use the Bot in accordance with Discord&apos;s Terms of Service and Community Guidelines. 38 20 </p> 39 - <ul className="list-disc pl-6 space-y-3 text-gray-400"> 21 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300"> 40 22 <li>Reminder system</li> 41 23 <li>Random cat and dog images</li> 42 24 <li>Weather information</li> ··· 46 28 </section> 47 29 48 30 <section> 49 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. User Responsibilities</h2> 50 - <p className="text-gray-400 leading-relaxed mb-4"> 31 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">3. User Responsibilities</h2> 32 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-4"> 51 33 When using the Bot, you agree not to: 52 34 </p> 53 - <ul className="list-disc pl-6 space-y-3 text-gray-400"> 35 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300"> 54 36 <li>Use the Bot for any illegal or unauthorized purpose</li> 55 37 <li>Violate any laws in your jurisdiction</li> 56 38 <li>Attempt to disrupt or interfere with the Bot&apos;s operation</li> ··· 60 42 </section> 61 43 62 44 <section> 63 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. API Usage</h2> 64 - <p className="text-gray-400 leading-relaxed"> 45 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">4. API Usage</h2> 46 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 65 47 The Bot may use third-party APIs and services (&quot;Third-Party Services&quot;). Your use of these services is subject to their respective terms and privacy policies. 66 48 </p> 67 - <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 49 + <ul className="list-disc pl-6 space-y-3 text-gray-600 dark:text-gray-300 mt-2"> 68 50 <li>You are responsible for the security of your API keys</li> 69 51 <li>We do not store your API keys permanently - they are only kept in memory during your active session</li> 70 52 <li>You must comply with the terms of service of any third-party APIs you use with the Bot</li> ··· 73 55 </section> 74 56 75 57 <section> 76 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Limitation of Liability</h2> 77 - <p className="text-gray-400 leading-relaxed"> 58 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">5. Limitation of Liability</h2> 59 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 78 60 The Bot is provided &quot;as is&quot; without any warranties. We are not responsible for any direct, indirect, incidental, or consequential damages resulting from the use of the Bot. 79 61 </p> 80 62 </section> 81 63 82 64 <section> 83 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Changes to Terms</h2> 84 - <p className="text-gray-400 leading-relaxed"> 65 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">6. Changes to Terms</h2> 66 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 85 67 We reserve the right to modify these terms at any time. Continued use of the Bot after changes constitutes acceptance of the new terms. 86 68 </p> 87 69 </section> 88 70 89 71 <section> 90 - <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Contact</h2> 91 - <p className="text-gray-400 leading-relaxed"> 92 - If you have any questions about these Terms of Service, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>. 72 + <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-4 pt-2 border-t border-gray-100 dark:border-gray-700 first:border-t-0 first:pt-0">7. Contact</h2> 73 + <p className="text-gray-600 dark:text-gray-300 leading-relaxed"> 74 + If you have any questions about these Terms of Service, please contact us at <a href="mailto:scan@scanash.com" className="text-sky-600 dark:text-sky-400 hover:underline font-medium">scan@scanash.com</a>. 93 75 </p> 94 76 </section> 95 77 </div> 96 - </main> 97 - </div> 78 + </LegalLayout> 79 + 80 + <Footer /> 81 + </> 98 82 ); 99 83 }
+18 -8
web/vite.config.ts
··· 1 - import { defineConfig, loadEnv } from 'vite'; 1 + import { defineConfig } from 'vite'; 2 2 import react from '@vitejs/plugin-react'; 3 3 import path from 'path'; 4 + import fs from 'fs'; 5 + import dotenv from 'dotenv'; 4 6 5 7 export default defineConfig(() => { 8 + const envWebPath = path.resolve(__dirname, '..', '.env.web'); 9 + const envPath = path.resolve(__dirname, '..', '.env'); 10 + 11 + if (fs.existsSync(envWebPath)) { 12 + dotenv.config({ path: envWebPath }); 13 + } else if (fs.existsSync(envPath)) { 14 + dotenv.config({ path: envPath }); 15 + } 16 + 6 17 return { 7 18 plugins: [react()], 8 19 envPrefix: 'VITE_', 9 - envDir: path.resolve(__dirname, '..'), 10 - resolve: { 11 - alias: { 12 - '@': path.resolve(__dirname, './src'), 13 - }, 14 - }, 15 20 server: { 16 21 port: 3000, 17 22 proxy: { 18 23 '/api': { 19 - target: process.env.FRONTEND_URL || 'http://localhost:2020', 24 + target: process.env.VITE_BOT_API_URL || 'http://localhost:2020', 20 25 changeOrigin: true, 21 26 }, 27 + }, 28 + }, 29 + resolve: { 30 + alias: { 31 + '@': path.resolve(__dirname, './src'), 22 32 }, 23 33 }, 24 34 build: {