Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
1import { ReactNode, useState } from 'react';
2import { Link, useLocation } from 'react-router-dom';
3import { LayoutDashboard, CheckSquare, Key, Bell, Menu, X, LogOut, User } from 'lucide-react';
4import { useAuthStore } from '../stores/authStore';
5import { toast } from 'sonner';
6import ThemeToggle from './ThemeToggle';
7
8interface LayoutProps {
9 children: ReactNode;
10}
11
12const Layout = ({ children }: LayoutProps) => {
13 const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
14 const location = useLocation();
15 const { user, logout } = useAuthStore();
16
17 const navigation = [
18 { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
19 { name: 'Todos', href: '/todos', icon: CheckSquare },
20 { name: 'Reminders', href: '/reminders', icon: Bell },
21 { name: 'API Keys', href: '/api-keys', icon: Key },
22 ];
23
24 const handleLogout = () => {
25 logout();
26 toast.success('Logged out successfully');
27 };
28
29 return (
30 <div className="min-h-screen relative overflow-hidden bg-gradient-to-b from-white to-gray-50 dark:from-gray-900 dark:to-black text-gray-900 dark:text-gray-100 font-inter transition-colors duration-300">
31 <div className="pointer-events-none absolute -top-32 -right-32 h-96 w-96 rounded-full blur-xl md:blur-3xl opacity-30 bg-gradient-to-tr from-pink-400 to-purple-500 dark:opacity-20" />
32 <div className="pointer-events-none absolute -bottom-32 -left-32 h-[28rem] w-[28rem] rounded-full blur-xl md:blur-3xl opacity-30 bg-gradient-to-tr from-indigo-400 to-sky-500 dark:opacity-20" />
33
34 <div
35 className={`fixed inset-0 z-50 lg:hidden transition-opacity duration-200 ${
36 mobileMenuOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
37 }`}
38 >
39 <div
40 className="fixed inset-0 bg-black/70"
41 onClick={() => setMobileMenuOpen(false)}
42 />
43 <div
44 className={`fixed inset-y-0 left-0 flex w-72 flex-col bg-white dark:bg-gray-800 backdrop-blur-md shadow-xl transform transition-transform duration-200 ease-out ${
45 mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
46 }`}
47 >
48 <div className="flex h-16 items-center justify-between px-6 border-b border-gray-200 dark:border-gray-700">
49 <div className="flex items-center space-x-3">
50 <img
51 className="w-8 h-8 rounded-lg object-contain"
52 src="/bot_icon.png"
53 alt="Aethel Bot"
54 style={{ imageRendering: 'crisp-edges' }}
55 />
56 <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Aethel</h1>
57 </div>
58 <div className="flex items-center space-x-2">
59 <ThemeToggle className="shadow-md" />
60 <button
61 onClick={() => setMobileMenuOpen(false)}
62 className="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors duration-150"
63 >
64 <X className="h-5 w-5" />
65 </button>
66 </div>
67 </div>
68 <nav className="flex-1 px-4 py-6">
69 <div className="space-y-2">
70 {navigation.map((item) => {
71 const Icon = item.icon;
72 const isActive = location.pathname === item.href;
73 return (
74 <Link
75 key={item.name}
76 to={item.href}
77 onClick={() => setMobileMenuOpen(false)}
78 className={`group flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-150 ${
79 isActive
80 ? 'bg-blue-600 text-white'
81 : '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'
82 }`}
83 >
84 <Icon
85 className={`mr-3 h-5 w-5 flex-shrink-0 ${
86 isActive
87 ? 'text-white'
88 : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-100'
89 }`}
90 />
91 {item.name}
92 </Link>
93 );
94 })}
95 </div>
96 </nav>
97
98 <div className="border-t border-gray-200 dark:border-gray-700 p-4">
99 <div className="flex items-center mb-4 p-3 rounded-xl">
100 <div className="flex-shrink-0">
101 {user?.avatar ? (
102 <img
103 className="h-10 w-10 rounded-full"
104 src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`}
105 alt={user.username}
106 />
107 ) : (
108 <div className="h-10 w-10 rounded-full bg-blue-600 flex items-center justify-center">
109 <User className="h-5 w-5 text-white" />
110 </div>
111 )}
112 </div>
113 <div className="ml-3 flex-1 min-w-0">
114 <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
115 {user?.discriminator && user.discriminator !== '0'
116 ? `${user.username}#${user.discriminator}`
117 : user?.username}
118 </p>
119 <p className="text-xs text-gray-500 dark:text-gray-400 truncate">{user?.email}</p>
120 </div>
121 </div>
122 <button
123 onClick={handleLogout}
124 className="flex w-full items-center px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-300 rounded-xl hover:bg-red-600 hover:text-white transition-all duration-150 group"
125 >
126 <LogOut className="mr-3 h-4 w-4 flex-shrink-0" />
127 Logout
128 </button>
129 </div>
130 </div>
131 </div>
132
133 <header className="fixed top-4 left-8 right-8 lg:left-16 lg:right-16 xl:left-32 xl:right-32 z-40 bg-white dark:bg-gray-800 backdrop-blur-xl border border-gray-200 dark:border-gray-700 shadow-2xl rounded-2xl transition-colors duration-300">
134 <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
135 <div className="flex h-16 items-center justify-between">
136 <div className="flex items-center space-x-4">
137 <button
138 type="button"
139 className="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors duration-150 lg:hidden"
140 onClick={() => setMobileMenuOpen(true)}
141 >
142 <Menu className="h-5 w-5" />
143 </button>
144 <div className="flex items-center space-x-3">
145 <img
146 className="w-8 h-8 rounded-lg object-contain"
147 src="/bot_icon.png"
148 alt="Aethel Bot"
149 style={{ imageRendering: 'crisp-edges' }}
150 />
151 <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Aethel</h1>
152 </div>
153 </div>
154
155 <nav className="hidden lg:flex lg:space-x-2">
156 {navigation.map((item) => {
157 const Icon = item.icon;
158 const isActive = location.pathname === item.href;
159 return (
160 <Link
161 key={item.name}
162 to={item.href}
163 className={`group flex items-center px-4 py-2 text-sm font-medium rounded-full transition-all duration-150 ${
164 isActive
165 ? 'bg-blue-600 text-white'
166 : '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'
167 }`}
168 >
169 <Icon
170 className={`mr-2 h-4 w-4 flex-shrink-0 ${
171 isActive
172 ? 'text-white'
173 : 'text-gray-500 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-100'
174 }`}
175 />
176 {item.name}
177 </Link>
178 );
179 })}
180 </nav>
181
182 <div className="flex items-center space-x-4">
183 <div className="hidden lg:flex items-center space-x-3 px-3 py-2 rounded-xl">
184 <div className="flex-shrink-0">
185 {user?.avatar ? (
186 <img
187 className="h-8 w-8 rounded-full"
188 src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`}
189 alt={user.username}
190 />
191 ) : (
192 <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center">
193 <User className="h-4 w-4 text-white" />
194 </div>
195 )}
196 </div>
197 <div className="hidden xl:block">
198 <p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
199 {user?.discriminator && user.discriminator !== '0'
200 ? `${user.username}#${user.discriminator}`
201 : user?.username}
202 </p>
203 </div>
204 </div>
205 <ThemeToggle />
206 <button
207 onClick={handleLogout}
208 className="hidden lg:flex items-center px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 rounded-full hover:bg-red-600 hover:text-white transition-all duration-150"
209 >
210 <LogOut className="h-4 w-4" />
211 </button>
212 </div>
213 </div>
214 </div>
215 </header>
216
217 <main className="flex-1 pt-24">
218 <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">{children}</div>
219 </main>
220 </div>
221 );
222};
223
224export default Layout;