+18
-7
src/commands/fun/8ball.ts
+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
+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
+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
+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
-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
+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
+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
web/public/bot_icon.png
This is a binary file and will not be displayed.
+78
-50
web/src/components/Layout.tsx
+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
+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
+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
+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
+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 ("the Bot") 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'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's functionality</li>
50
32
<li>Process commands and provide responses</li>
51
33
<li>Improve the Bot'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's Privacy Policy for user and server data</li>
77
59
<li>OpenRouter'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'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'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
+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
+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'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'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 ("Third-Party Services"). 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 "as is" 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
+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: {