diff --git a/eslint.config.mjs b/eslint.config.mjs index 9db1bea..0bc92ca 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,27 @@ import { defineConfig } from "eslint/config"; export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] }, + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], + plugins: { js }, + extends: ["js/recommended"], + ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"] + }, { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } }, - tseslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-explicit-any": "warn" + } + } ]); diff --git a/package.json b/package.json index 9b52a4d..7a6ccb7 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "build": "tsc && node scripts/fix-imports.js", "migrate": "node scripts/run-migrations.js", "do": "pnpm run migrate && pnpm run build && pnpm run start", - "lint": "eslint ./src", - "lint:fix": "eslint . --config eslint.config.cjs --fix", - "lint:format": "eslint . --config eslint.config.cjs --format=codeframe", + "lint": "eslint ./src ./web/src --ext .ts,.tsx", + "lint:fix": "eslint ./src ./web/src --ext .ts,.tsx --config eslint.config.cjs --fix", + "lint:format": "eslint ./src ./web/src --ext .ts,.tsx --config eslint.config.cjs --format=codeframe", "format": "prettier --write \"**/*.{js,json,md,ts,tsx}\" --ignore-path .prettierignore", "format:check": "prettier --check \"**/*.{js,json,md}\" --ignore-path .prettierignore", "check": "pnpm run lint && pnpm run format:check" diff --git a/web/package.json b/web/package.json index 2fbaed8..8968551 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,7 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { "@heroicons/react": "^2.2.0", diff --git a/web/public/royale_logo.svg b/web/public/royale_logo.svg index 4cf7f99..13f79ec 100644 --- a/web/public/royale_logo.svg +++ b/web/public/royale_logo.svg @@ -1,2 +1,23 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/royale_logo_dark.svg b/web/public/royale_logo_dark.svg new file mode 100644 index 0000000..d764a0d --- /dev/null +++ b/web/public/royale_logo_dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index d653b2d..4d8224b 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -3,6 +3,7 @@ import { Link, useLocation } from 'react-router-dom'; import { LayoutDashboard, CheckSquare, Key, Bell, Menu, X, LogOut, User } from 'lucide-react'; import { useAuthStore } from '../stores/authStore'; import { toast } from 'sonner'; +import ThemeToggle from './ThemeToggle'; interface LayoutProps { children: ReactNode; @@ -26,10 +27,7 @@ const Layout = ({ children }: LayoutProps) => { }; return ( -
+
{ >
setMobileMenuOpen(false)} />
-
+
{ alt="Aethel Bot" style={{ imageRendering: 'crisp-edges' }} /> -

Aethel

+

Aethel

+
+
+ +
-
-
-
+
+
{user?.avatar ? ( { )}
-

+

{user?.discriminator && user.discriminator !== '0' ? `${user.username}#${user.discriminator}` : user?.username}

-

{user?.email}

+

{user?.email}

-
+
@@ -154,12 +157,14 @@ const Layout = ({ children }: LayoutProps) => { className={`group flex items-center px-4 py-2 text-sm font-medium rounded-full transition-all duration-150 ${ isActive ? 'bg-blue-600 text-white' - : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' + : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100' }`} > {item.name} @@ -169,7 +174,7 @@ const Layout = ({ children }: LayoutProps) => {
-
+
{user?.avatar ? ( { )}
-

+

{user?.discriminator && user.discriminator !== '0' ? `${user.username}#${user.discriminator}` : user?.username}

+ diff --git a/web/src/components/LegalLayout.tsx b/web/src/components/LegalLayout.tsx index e2563a8..7968124 100644 --- a/web/src/components/LegalLayout.tsx +++ b/web/src/components/LegalLayout.tsx @@ -20,13 +20,17 @@ export function LegalLayout({ title, lastUpdated, children }: LegalLayoutProps) className="mx-auto mb-8 pixel-art rounded-2xl w-40 h-40 object-cover" style={{ imageRendering: 'auto' }} /> -

{title}

+

+ {title} +

-

Last Updated: {lastUpdated}

+

+ Last Updated: {lastUpdated} +

-
-
{children}
+
+
{children}
diff --git a/web/src/components/ThemeToggle.tsx b/web/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..4e5f466 --- /dev/null +++ b/web/src/components/ThemeToggle.tsx @@ -0,0 +1,24 @@ +import { Moon, Sun } from 'lucide-react'; +import { useThemeStore } from '../stores/themeStore'; + +interface ThemeToggleProps { + className?: string; +} + +export default function ThemeToggle({ className = '' }: ThemeToggleProps) { + const { isDarkMode, toggleTheme } = useThemeStore(); + + return ( + + ); +} diff --git a/web/src/index.css b/web/src/index.css index 4ffa37a..d097b58 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -5,13 +5,25 @@ :root { --background: #ffffff; --foreground: #171717; + --gradient-light: linear-gradient(135deg, #ffffff 0%, #ffcad4 100%); + --gradient-dark: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); +} + +.dark { + --background: #0f0f23; + --foreground: #e5e7eb; } body { color: var(--foreground); - background: linear-gradient(135deg, #ffffff 0%, #ffcad4 100%); + background: var(--gradient-light); min-height: 100vh; font-family: 'Inter', Arial, sans-serif; + transition: background 0.3s ease, color 0.3s ease; +} + +body.dark { + background: var(--gradient-dark); } .btn { @@ -35,19 +47,19 @@ body { } .card { - @apply bg-white/80 rounded-lg border border-gray-200 shadow-lg; + @apply bg-white/80 dark:bg-gray-800/80 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg; } .input { - @apply flex h-10 w-full rounded-lg border border-gray-300 bg-white text-gray-900 px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50; + @apply flex h-10 w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-3 py-2 text-sm placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50; } .command-card { - @apply bg-white/80 backdrop-blur-sm rounded-lg p-6 shadow-lg; + @apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-lg p-6 shadow-lg; } .stats-card { - @apply bg-gradient-to-br from-white/90 to-white/70 backdrop-blur-sm rounded-lg p-6 shadow-lg border border-white/20; + @apply bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-lg p-6 shadow-lg border border-white/20 dark:border-gray-700/20; } .status-pulse { diff --git a/web/src/pages/ApiKeysPage.tsx b/web/src/pages/ApiKeysPage.tsx index 53a9273..e1d47be 100644 --- a/web/src/pages/ApiKeysPage.tsx +++ b/web/src/pages/ApiKeysPage.tsx @@ -56,7 +56,7 @@ const ApiKeysPage = () => { setHasPassedTest(true); toast.success('API key test successful!'); }, - onError: (error: any) => { + onError: (error: { response?: { data?: { error?: string } } }) => { const message = error.response?.data?.error || 'API key test failed'; setTestResult({ success: false, message }); setHasPassedTest(false); @@ -123,15 +123,17 @@ const ApiKeysPage = () => { return (
-

AI API Keys

-

+

AI API Keys

+

Configure your custom AI API keys and endpoints for personalized AI interactions.

-
+
-

Current Configuration

+

+ Current Configuration +

{apiKeyInfo?.hasApiKey && !isEditing && (
{isEditing && ( -
-

+
+

{apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key

-

- * You must test the API key before saving to - ensure it works correctly. +

+ * You must test the API key + before saving to ensure it works correctly.

- +
{ setTestResult(null); }} placeholder="Enter your API key" - className="input pr-10" + className="input pr-10 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" required />
-
-
@@ -274,7 +286,9 @@ const ApiKeysPage = () => { {testResult && (
{testResult.success ? ( @@ -328,25 +342,25 @@ const ApiKeysPage = () => {
)} -
-

Information

-
+
+

Information

+
- +

Your API key is encrypted and stored securely. It will only be used for AI interactions within the Discord bot.

- +

Supported providers include OpenAI, Anthropic, and any OpenAI-compatible API endpoints.

- +

Use the test function to verify your API key works before saving.

diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx index bc6188e..ceeb202 100644 --- a/web/src/pages/DashboardPage.tsx +++ b/web/src/pages/DashboardPage.tsx @@ -8,7 +8,21 @@ import { toast } from 'sonner'; const DashboardPage = () => { const { user } = useAuthStore(); const queryClient = useQueryClient(); - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); + + interface Todo { + id: number; + item: string; + done: boolean; + created_at: string; + } + + interface Reminder { + reminder_id: string; + message: string; + expires_at: string; + completed: boolean; + } const { data: todos } = useQuery({ queryKey: ['todos'], @@ -31,14 +45,15 @@ const DashboardPage = () => { refetchInterval: 30000, }); - const completedTodos = todos?.filter((todo: any) => todo.done).length || 0; - const pendingTodos = todos?.filter((todo: any) => !todo.done).length || 0; + const completedTodos = todos?.filter((todo: Todo) => todo.done).length || 0; + const pendingTodos = todos?.filter((todo: Todo) => !todo.done).length || 0; const totalTodos = todos?.length || 0; const hasApiKey = !!apiKeyInfo?.hasApiKey; const activeRemindersCount = activeReminders?.length || 0; const overdueReminders = - activeReminders?.filter((reminder: any) => new Date(reminder.expires_at) < new Date()) || []; + activeReminders?.filter((reminder: Reminder) => new Date(reminder.expires_at) < new Date()) || + []; const overviewData = { todos: { total: totalTodos, completed: completedTodos, pending: pendingTodos }, @@ -46,17 +61,17 @@ const DashboardPage = () => { aiConfigured: hasApiKey, }; - const activeTodosForDisplay = todos?.filter((todo: any) => !todo.done) || []; + const activeTodosForDisplay = todos?.filter((todo: Todo) => !todo.done) || []; const recentTodos = activeTodosForDisplay.slice(0, 5); const activeRemindersForDisplay = reminders?.filter( - (reminder: any) => !reminder.completed && new Date(reminder.expires_at) >= new Date() + (reminder: Reminder) => !reminder.completed && new Date(reminder.expires_at) >= new Date() ) || []; const recentReminders = activeRemindersForDisplay.slice(0, 5); useEffect(() => { if (overdueReminders.length > 0) { - overdueReminders.forEach((reminder: any) => { + overdueReminders.forEach((reminder: Reminder) => { const notificationId = `reminder-${reminder.reminder_id}`; if (!notifications.includes(notificationId)) { toast.error(`Reminder: ${reminder.message}`, { @@ -79,7 +94,7 @@ const DashboardPage = () => { setNotifications((prev) => prev.filter((notif) => notif !== `reminder-${id}`)); queryClient.invalidateQueries({ queryKey: ['reminders'] }); queryClient.invalidateQueries({ queryKey: ['active-reminders'] }); - } catch (error) { + } catch (_error) { toast.error('Failed to complete reminder'); } }; @@ -89,7 +104,7 @@ const DashboardPage = () => { await todosAPI.updateTodo(id, { done: true }); toast.success('Todo completed!'); queryClient.invalidateQueries({ queryKey: ['todos'] }); - } catch (error) { + } catch (_error) { toast.error('Failed to complete todo'); } }; @@ -98,10 +113,10 @@ const DashboardPage = () => {
-

+

Welcome back, {user?.username}👋

-

+

Keep track of your todos, reminders, and AI configuration all in one place.

@@ -109,8 +124,8 @@ const DashboardPage = () => {
-

Custom AI Assistant

-

+

Custom AI Assistant

+

{overviewData.aiConfigured ? 'AI is ready to assist you with tasks and questions' : 'Bring your own AI API key to unlock a limit-free AI command.'} @@ -125,8 +140,8 @@ const DashboardPage = () => {

-

Todos

-

+

Todos

+

{recentTodos.length > 0 ? `You have ${recentTodos.length} active todo${recentTodos.length > 1 ? 's' : ''} to complete` : 'No active todos. Create your first task to get started.'} @@ -141,8 +156,8 @@ const DashboardPage = () => {

-

Reminders

-

+

Reminders

+

{overdueReminders.length > 0 ? `${overdueReminders.length} overdue reminder${overdueReminders.length > 1 ? 's' : ''} need attention` : recentReminders.length > 0 @@ -167,10 +182,10 @@ const DashboardPage = () => {

{recentTodos.length > 0 ? (
- {recentTodos.slice(0, 5).map((todo: any) => ( + {recentTodos.slice(0, 5).map((todo: Todo) => (
@@ -190,12 +205,14 @@ const DashboardPage = () => {

{todo.item}

-

+

Created {new Date(todo.created_at).toLocaleDateString()}

@@ -203,7 +220,7 @@ const DashboardPage = () => { {!todo.done && ( @@ -215,8 +232,10 @@ const DashboardPage = () => { ) : (
-

No todos yet

-

+

+ No todos yet +

+

Create your first todo to start organizing your tasks

{
{recentReminders.length > 0 ? (
- {recentReminders.slice(0, 5).map((reminder: any) => { + {recentReminders.slice(0, 5).map((reminder: Reminder) => { const isOverdue = new Date(reminder.expires_at) < new Date(); const daysOverdue = isOverdue ? Math.ceil( @@ -262,13 +283,15 @@ const DashboardPage = () => { }`} >
-

{reminder.message}

+

+ {reminder.message} +

-

+

Due {new Date(reminder.expires_at).toLocaleDateString()}

{isOverdue && ( -

+

{daysOverdue} day{daysOverdue > 1 ? 's' : ''} overdue

)} @@ -279,8 +302,8 @@ const DashboardPage = () => { onClick={() => handleCompleteReminder(reminder.reminder_id)} className={`text-sm font-medium px-3 py-1 rounded transition-colors ${ isOverdue - ? 'text-red-600 hover:text-red-700 hover:bg-red-50' - : 'text-green-600 hover:text-green-700 hover:bg-green-50' + ? 'text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30' + : 'text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-900/30' }`} > Complete @@ -293,8 +316,10 @@ const DashboardPage = () => { ) : (
-

No reminders yet

-

+

+ No reminders yet +

+

Set up your first reminder to never miss important events

- - - +
+
+ + + + +
-

Aethel

-

A useful and multipurpose bot for Discord

+

+ Aethel +

+

+ A useful and multipurpose bot for Discord +

View Status @@ -71,97 +79,113 @@ export default function Home() {
-

/ai

-

Chat with AI, ask questions, get answers!

+

/ai

+

+ Chat with AI, ask questions, get answers! +

-

/wiki

-

Get answers directly from Wikipedia

+

/wiki

+

Get answers directly from Wikipedia

-

/weather

-

Get the weather forecast for any city

+

+ /weather +

+

+ Get the weather forecast for any city +

-

/joke

-

Get a random joke and bright up your day

+

/joke

+

+ Get a random joke and bright up your day +

-

/remind

-

+

/remind

+

Set a reminder for a message or something important, supports message interactions

-

/dog & /cat

-

Get random cute pet images

+

+ /dog & /cat +

+

Get random cute pet images

-

/8ball

-

+

/8ball

+

Ask the magic 8ball a question and get a mysterious answer

-

/trivia

-

Start a multiplayer trivia game with friends

+

/trivia

+

+ Start a multiplayer trivia game with friends +

-

/cobalt

-

+

/cobalt

+

Download videos and media from various platforms using Cobalt

-

/todo

-

Create and manage your todo list and tasks

+

/todo

+

+ Create and manage your todo list and tasks +

-

/whois

-

Get domain and IP information

+

/whois

+

Get domain and IP information

-

/time

-

Get the current time for any cities

+

/time

+

Get the current time for any cities

-

/help

-

Show all available commands and their usage

+

/help

+

+ Show all available commands and their usage +

-

+

You might use this bot in DMs and servers that allow external applications!

-