ChandimaPrabath commited on
Commit
cdf301a
·
1 Parent(s): 1ca0bed

nexus auth integrated

Browse files
frontend/app/Auth.js DELETED
@@ -1,322 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
-
3
- const Auth = ({
4
- username,
5
- setUsername,
6
- password,
7
- setPassword,
8
- confirmPassword,
9
- setConfirmPassword,
10
- isLoading,
11
- setIsLoading,
12
- error,
13
- setError,
14
- isSignup,
15
- setIsSignup,
16
- ws,
17
- status,
18
- setSessionToken,
19
- }) => {
20
- const [usernameValid, setUsernameValid] = useState(false);
21
- const [passwordValid, setPasswordValid] = useState(false);
22
- const [passwordsMatch, setPasswordsMatch] = useState(false);
23
- const [showPassword, setShowPassword] = useState(false);
24
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
25
-
26
- useEffect(() => {
27
- // Check for existing session token in localStorage or cookie when component loads
28
- const token = localStorage.getItem('sessionToken');
29
- if (token) {
30
- setSessionToken(token);
31
- }
32
- }, [setSessionToken]);
33
-
34
- // Handle login and signup requests
35
- const connectAndAuthenticate = (data) => {
36
- setError('');
37
- if (!username || !password) {
38
- setError('Please enter both username and password');
39
- return;
40
- }
41
-
42
- setIsLoading(true);
43
-
44
- // Send authentication data over WebSocket if connection is open
45
- if (ws && ws.readyState === WebSocket.OPEN) {
46
- ws.send(data);
47
- } else {
48
- setError('WebSocket connection is not established');
49
- setIsLoading(false);
50
- }
51
- };
52
-
53
- // Handle session token update from the server response
54
- const handleSessionToken = (response) => {
55
- const { token, success } = JSON.parse(response);
56
-
57
- if (success && token) {
58
- localStorage.setItem('sessionToken', token); // Store token in localStorage
59
- setSessionToken(token); // Update token state
60
- setIsLoading(false);
61
- } else {
62
- setError('Authentication failed');
63
- setIsLoading(false);
64
- }
65
- };
66
-
67
- // Validation for signup form
68
- const validateSignup = () => {
69
- if (!username || !password || !confirmPassword) {
70
- setError('Please fill in all fields');
71
- return false;
72
- }
73
- if (username.length < 4) {
74
- setError('Username must be at least 4 characters');
75
- return false;
76
- }
77
- if (password.length < 8) {
78
- setError('Password must be at least 8 characters');
79
- return false;
80
- }
81
- if (password !== confirmPassword) {
82
- setError('Passwords do not match');
83
- return false;
84
- }
85
- return true;
86
- };
87
-
88
- // Signup handler
89
- const handleSignup = () => {
90
- if (!validateSignup()) return;
91
-
92
- const signupData = JSON.stringify({ username, password, action: 'signup' });
93
- connectAndAuthenticate(signupData);
94
- };
95
-
96
- // Login handler
97
- const handleLogin = () => {
98
- if (!username || !password) {
99
- setError('Please enter both username and password');
100
- return;
101
- }
102
-
103
- const loginData = JSON.stringify({ username, password, action: 'login' });
104
- connectAndAuthenticate(loginData);
105
- };
106
-
107
- // Validations to dynamically update conditions
108
- const checkUsername = (value) => {
109
- setUsername(value);
110
- setUsernameValid(value.length >= 4);
111
- };
112
-
113
- const checkPassword = (value) => {
114
- setPassword(value);
115
- setPasswordValid(value.length >= 8);
116
- };
117
-
118
- const checkPasswordsMatch = (value) => {
119
- setConfirmPassword(value);
120
- setPasswordsMatch(value === password);
121
- };
122
-
123
- return (
124
- <div style={inputContainerStyle}>
125
- {isSignup ? (
126
- // Signup Form
127
- <div style={inputContainerStyle}>
128
- <h2 style={headingStyle}>Signup</h2>
129
- <input
130
- type="text"
131
- placeholder="Username"
132
- value={username}
133
- onChange={(e) => checkUsername(e.target.value)}
134
- style={inputStyle}
135
- />
136
- <div style={passwordInputContainerStyle}>
137
- <input
138
- type={showPassword ? 'text' : 'password'}
139
- placeholder="Password"
140
- value={password}
141
- onChange={(e) => checkPassword(e.target.value)}
142
- style={inputStyle}
143
- />
144
- <span
145
- onClick={() => setShowPassword(!showPassword)}
146
- style={eyeIconStyle}
147
- >
148
- 👁️
149
- </span>
150
- </div>
151
- <div style={passwordInputContainerStyle}>
152
- <input
153
- type={showConfirmPassword ? 'text' : 'password'}
154
- placeholder="Confirm Password"
155
- value={confirmPassword}
156
- onChange={(e) => checkPasswordsMatch(e.target.value)}
157
- style={inputStyle}
158
- />
159
- <span
160
- onClick={() => setShowConfirmPassword(!showConfirmPassword)}
161
- style={eyeIconStyle}
162
- >
163
- 👁️
164
- </span>
165
- </div>
166
- <div style={buttonContainerStyle}>
167
- <button
168
- onClick={handleSignup}
169
- style={loginButtonStyle}
170
- >
171
- {isLoading ? 'Processing...' : 'Signup'}
172
- </button>
173
- </div>
174
- </div>
175
- ) : (
176
- // Login Form
177
- <div style={inputContainerStyle}>
178
- <h2 style={headingStyle}>Login</h2>
179
- <input
180
- type="text"
181
- placeholder="Username"
182
- value={username}
183
- onChange={(e) => setUsername(e.target.value)}
184
- style={inputStyle}
185
- />
186
- <div style={passwordInputContainerStyle}>
187
- <input
188
- type={showPassword ? 'text' : 'password'}
189
- placeholder="Password"
190
- value={password}
191
- onChange={(e) => setPassword(e.target.value)}
192
- style={inputStyle}
193
- />
194
- <span
195
- onClick={() => setShowPassword(!showPassword)}
196
- style={eyeIconStyle}
197
- >
198
- 👁️
199
- </span>
200
- </div>
201
- <div style={buttonContainerStyle}>
202
- <button
203
- onClick={handleLogin}
204
- style={loginButtonStyle}
205
- >
206
- {isLoading ? 'Processing...' : 'Login'}
207
- </button>
208
- </div>
209
- </div>
210
- )}
211
-
212
- {isSignup && (
213
- <div style={conditionsContainerStyle}>
214
- <h3 style={conditionsHeaderStyle}>Signup Conditions</h3>
215
- <ul>
216
- <li style={{ color: usernameValid ? 'green' : 'red' }}>
217
- {usernameValid ? '✔ Username must be at least 4 characters' : '✘ Username must be at least 4 characters'}
218
- </li>
219
- <li style={{ color: passwordValid ? 'green' : 'red' }}>
220
- {passwordValid ? '✔ Password must be at least 8 characters' : '✘ Password must be at least 8 characters'}
221
- </li>
222
- <li style={{ color: passwordsMatch ? 'green' : 'red' }}>
223
- {passwordsMatch ? '✔ Passwords match' : '✘ Passwords do not match'}
224
- </li>
225
- </ul>
226
- </div>
227
- )}
228
-
229
- <div style={buttonContainerStyle}>
230
- <button
231
- onClick={() => setIsSignup(!isSignup)}
232
- style={switchButtonStyle}
233
- >
234
- {isSignup ? 'Already have an Account? Login' : 'Don\'t have an Account? Signup'}
235
- </button>
236
- </div>
237
-
238
- {error && <div style={{ color: 'red', display: 'flex', alignItems: 'center' }}>
239
- <div style={{ width: '8px', height: '8px', backgroundColor: 'red', borderRadius: '50px', marginRight: '5px' }} />
240
- {error}
241
- </div>}
242
- </div>
243
- );
244
- };
245
-
246
- // Styles (as before)
247
- const inputContainerStyle = {
248
- display: 'flex',
249
- flexDirection: 'column',
250
- gap: '15px',
251
- maxWidth: '400px',
252
- width: '100%',
253
- };
254
-
255
- const headingStyle = {
256
- color: '#d4a5ff',
257
- fontSize: '36px',
258
- };
259
-
260
- const inputStyle = {
261
- padding: '10px',
262
- fontSize: '16px',
263
- borderRadius: '5px',
264
- border: '1px solid #4b2e83',
265
- marginBottom: '10px',
266
- backgroundColor: '#2e2e4f',
267
- color: '#fff',
268
- };
269
-
270
- const passwordInputContainerStyle = {
271
- position: 'relative',
272
- display: 'grid',
273
- };
274
-
275
- const eyeIconStyle = {
276
- position: 'absolute',
277
- right: '10px',
278
- top: '50%',
279
- transform: 'translateY(-50%)',
280
- cursor: 'pointer',
281
- };
282
-
283
- const loginButtonStyle = {
284
- padding: '12px 20px',
285
- backgroundColor: '#6a4c9c',
286
- color: '#fff',
287
- border: 'none',
288
- borderRadius: '5px',
289
- fontSize: '16px',
290
- cursor: 'pointer',
291
- transition: 'background-color 0.3s',
292
- };
293
-
294
- const switchButtonStyle = {
295
- marginTop: '10px',
296
- color: '#fff',
297
- background: 'none',
298
- border: 'none',
299
- cursor: 'pointer',
300
- textDecoration: 'underline',
301
- };
302
-
303
- const buttonContainerStyle = {
304
- display: 'flex',
305
- justifyContent: 'center',
306
- };
307
-
308
- const conditionsContainerStyle = {
309
- display: 'flex',
310
- flexDirection: 'column',
311
- gap: '10px',
312
- marginTop: '15px',
313
- maxWidth: '300px',
314
- };
315
-
316
- const conditionsHeaderStyle = {
317
- color: '#d4a5ff',
318
- fontSize: '20px',
319
- marginBottom: '5px',
320
- };
321
-
322
- export default Auth;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/Sidebar.js DELETED
@@ -1,222 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
- import Image from 'next/image';
5
-
6
- export default function Sidebar({
7
- searchQuery,
8
- setSearchQuery,
9
- filteredUsers,
10
- handleUserSelect,
11
- setFilteredUsers,
12
- }) {
13
- const [isSidebarOpen, setIsSidebarOpen] = useState(false);
14
- const [me, setMe] = useState('');
15
- const [allChats, setAllChats] = useState([]);
16
-
17
- useEffect(() => {
18
- // Retrieve 'me' from localStorage
19
- const storedMe = localStorage.getItem('me');
20
- if (storedMe) {
21
- setMe(storedMe);
22
- }
23
-
24
- // Retrieve all chats stored in localStorage
25
- const allKeys = Object.keys(localStorage);
26
- const chats = allKeys.filter((key) => key !== 'me' && key !== 'sessionToken' && key !== 'ally-supports-cache').map((key) => {
27
- return {
28
- username: key,
29
- chatData: JSON.parse(localStorage.getItem(key)),
30
- };
31
- });
32
-
33
- setAllChats(chats);
34
- }, []);
35
-
36
- const toggleSidebar = () => {
37
- setIsSidebarOpen((prev) => !prev);
38
- };
39
-
40
- const handleSearchChange = async (e) => {
41
- const query = e.target.value;
42
- setSearchQuery(query);
43
-
44
- if (!query) {
45
- setFilteredUsers([]); // No search query, show nothing in search results
46
- return;
47
- }
48
-
49
- const servicesUrl = process.env.NEXT_PUBLIC_SERVICES_URL;
50
- if (!servicesUrl) {
51
- console.error('Environment variable NEXT_PUBLIC_SERVICES_URL is not defined.');
52
- return;
53
- }
54
-
55
- if (query.length > 2) {
56
- try {
57
- const response = await fetch(`${servicesUrl}/search?query=${query}`);
58
- const data = await response.json();
59
- setFilteredUsers(Array.isArray(data) ? data : []); // Ensure that filteredUsers is always an array
60
- } catch (error) {
61
- console.error('Error fetching search results:', error);
62
- setFilteredUsers([]); // In case of error, clear filteredUsers
63
- }
64
- } else {
65
- setFilteredUsers([]);
66
- }
67
- };
68
-
69
- return (
70
- <>
71
- {/* Hamburger Menu Button */}
72
- <button
73
- onClick={toggleSidebar}
74
- style={{
75
- position: 'fixed',
76
- top: '10px',
77
- left: isSidebarOpen ? '210px' : '10px',
78
- transform: isSidebarOpen ? 'rotate(90deg)' : 'none',
79
- zIndex: 1000,
80
- backgroundColor: '#352e83',
81
- color: '#fff',
82
- border: 'none',
83
- borderRadius: '10px',
84
- padding: '10px',
85
- cursor: 'pointer',
86
- transition: 'left .4s ease',
87
- }}
88
- >
89
-
90
- </button>
91
-
92
- {/* Sidebar */}
93
- <div
94
- style={{
95
- position: 'fixed',
96
- top: 0,
97
- left: isSidebarOpen ? 0 : '-260px',
98
- width: '250px',
99
- height: '100%',
100
- backgroundColor: '#333',
101
- padding: '20px',
102
- boxShadow: '2px 0 5px rgba(0, 0, 0, 0.5)',
103
- transition: 'left 0.3s ease',
104
- zIndex: 999,
105
- }}
106
- >
107
- <h3 style={{ color: '#fff', marginBottom: '20px' }}>Find Chats</h3>
108
-
109
- <input
110
- type="text"
111
- placeholder="Search Users..."
112
- value={searchQuery}
113
- onChange={handleSearchChange}
114
- style={searchInputStyle}
115
- />
116
-
117
- <div style={{ marginBottom: '20px' }}>
118
- {/* Display search results */}
119
- {searchQuery && (
120
- <div>
121
- <h4 style={{ color: '#fff', marginBottom: '10px' }}>Search Results</h4>
122
- <ul style={{ listStyleType: 'none', padding: 0 }}>
123
- {Array.isArray(filteredUsers) && filteredUsers.map((recipient, index) => (
124
- <li
125
- key={index}
126
- style={{
127
- color: '#fff',
128
- cursor: 'pointer',
129
- marginBottom: '15px',
130
- display: 'flex',
131
- alignItems: 'center',
132
- }}
133
- onClick={() => handleUserSelect(recipient)}
134
- >
135
- {/* Profile Picture */}
136
- <div
137
- style={{
138
- width: '40px',
139
- height: '40px',
140
- position: 'relative',
141
- marginRight: '10px',
142
- }}
143
- >
144
- <Image
145
- src={`https://ui-avatars.com/api/?background=random&name=${encodeURIComponent(
146
- recipient
147
- )}`}
148
- alt={`${recipient}'s avatar`}
149
- layout="fill"
150
- objectFit="cover"
151
- style={{ borderRadius: '50%' }}
152
- />
153
- </div>
154
- {/* Username */}
155
- <span style={{ textDecoration: 'none', color: '#fff', fontSize: '16px' }}>
156
- {recipient} {recipient === me ? '(Me)' : ''}
157
- </span>
158
- </li>
159
- ))}
160
- </ul>
161
- </div>
162
- )}
163
- </div>
164
-
165
- <div>
166
- {/* Display localStorage chats */}
167
- <h4 style={{ color: '#fff', marginBottom: '10px' }}>Chats</h4>
168
- <ul style={{ listStyleType: 'none', padding: 0 }}>
169
- {allChats.map((chat, index) => (
170
- <li
171
- key={index}
172
- style={{
173
- color: '#fff',
174
- cursor: 'pointer',
175
- marginBottom: '15px',
176
- display: 'flex',
177
- alignItems: 'center',
178
- }}
179
- onClick={() => handleUserSelect(chat.username)}
180
- >
181
- {/* Profile Picture */}
182
- <div
183
- style={{
184
- width: '40px',
185
- height: '40px',
186
- position: 'relative',
187
- marginRight: '10px',
188
- }}
189
- >
190
- <Image
191
- src={`https://ui-avatars.com/api/?background=random&name=${encodeURIComponent(
192
- chat.username
193
- )}`}
194
- alt={`${chat.username}'s avatar`}
195
- layout="fill"
196
- objectFit="cover"
197
- style={{ borderRadius: '50%' }}
198
- />
199
- </div>
200
- {/* Username */}
201
- <span style={{ textDecoration: 'none', color: '#fff', fontSize: '16px' }}>
202
- {chat.username} {chat.username === me ? '(Me)' : ''}
203
- </span>
204
- </li>
205
- ))}
206
- </ul>
207
- </div>
208
- </div>
209
- </>
210
- );
211
- }
212
-
213
- const searchInputStyle = {
214
- width: '100%',
215
- padding: '10px',
216
- fontSize: '16px',
217
- borderRadius: '5px',
218
- border: '1px solid #4b2e83',
219
- backgroundColor: '#2e2e4f',
220
- color: '#fff',
221
- marginBottom: '15px',
222
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/WebSocketContext.js DELETED
@@ -1,86 +0,0 @@
1
- import { createContext, useContext, useState, useEffect } from 'react';
2
-
3
- const WebSocketContext = createContext(null);
4
-
5
- export const useWebSocket = () => {
6
- return useContext(WebSocketContext);
7
- };
8
-
9
- export const WebSocketProvider = ({ children }) => {
10
- const [ws, setWs] = useState(null);
11
- const [status, setStatus] = useState('Disconnected');
12
- const [retryCount, setRetryCount] = useState(0); // For retrying WebSocket connection
13
- const [timeoutError, setTimeoutError] = useState(false); // Timeout error state
14
-
15
- const maxRetries = 5; // Max retry attempts
16
-
17
- useEffect(() => {
18
- const relayUrl = process.env.NEXT_PUBLIC_RELAY_URL;
19
-
20
- if (!relayUrl) {
21
- console.error('Environment variable NEXT_PUBLIC_RELAY_URL is not defined.');
22
- return;
23
- }
24
-
25
- const connectWebSocket = () => {
26
- const socket = new WebSocket(relayUrl);
27
-
28
- socket.onopen = () => {
29
- console.log('WebSocket connection opened.');
30
- setStatus('Connected');
31
- setRetryCount(0); // Reset retry count on successful connection
32
- setTimeoutError(false); // Reset timeout error
33
- };
34
-
35
- socket.onclose = () => {
36
- console.log('WebSocket connection closed.');
37
- setStatus('Disconnected');
38
- handleReconnection();
39
- };
40
-
41
- socket.onerror = (error) => {
42
- console.error('WebSocket error:', error);
43
- setStatus('Error');
44
- handleReconnection();
45
- };
46
-
47
- setWs(socket);
48
- };
49
-
50
- // Function to handle reconnection with retry
51
- const handleReconnection = () => {
52
- if (retryCount < maxRetries) {
53
- setRetryCount(retryCount + 1);
54
- setStatus('Reconnecting...');
55
- console.log(`Attempting reconnect ${retryCount + 1}/${maxRetries}`);
56
- setTimeout(() => connectWebSocket(), 2000 * (retryCount + 1)); // Retry with increasing delay
57
- } else {
58
- console.error('Max retries reached. WebSocket connection failed.');
59
- setStatus('Failed');
60
- }
61
- };
62
-
63
- connectWebSocket();
64
-
65
- // Timeout mechanism to handle stuck connections
66
- const timeout = setTimeout(() => {
67
- if (status === 'Connecting' || status === 'Reconnecting') {
68
- setTimeoutError(true);
69
- setStatus('Timeout - Could not connect');
70
- }
71
- }, 10000); // 10 seconds timeout
72
-
73
- return () => {
74
- clearTimeout(timeout); // Cleanup timeout on unmount
75
- if (ws) {
76
- ws.close();
77
- }
78
- };
79
- }, [status, retryCount]);
80
-
81
- return (
82
- <WebSocketContext.Provider value={{ ws, status, timeoutError }}>
83
- {children}
84
- </WebSocketContext.Provider>
85
- );
86
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/globals.css CHANGED
@@ -1,12 +1,48 @@
 
 
 
 
1
  :root {
2
- --background: #ffffff;
3
- --foreground: #171717;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
  @media (prefers-color-scheme: dark) {
7
  :root {
8
- --background: #0a0a0a;
9
- --foreground: #ededed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
  }
12
 
@@ -40,3 +76,31 @@ a {
40
  color-scheme: dark;
41
  }
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
  :root {
6
+ --background: #15171f;
7
+ /* Deeper dark background */
8
+ --foreground: #E0E0E0;
9
+ /* Light gray text */
10
+ --sidebar-background: #14141b;
11
+ /* Darker sidebar background */
12
+ --primary: #1f202a;
13
+ --secondary: #2a2b3b;
14
+ /* Soft secondary background for content */
15
+ --accent: #4a557f;
16
+ --hover-accent: #5c608a;
17
+ /* Slightly lighter accent color for hover */
18
+ --button-background: #3a3b4c;
19
+ /* Button background */
20
+ --button-hover: #2D2D38;
21
+ /* Darker button hover */
22
+ --shadow-dark: rgba(0, 0, 0, 0.15);
23
+ /* Shadow for depth */
24
  }
25
 
26
  @media (prefers-color-scheme: dark) {
27
  :root {
28
+ --background: #15171f;
29
+ /* Deeper dark background */
30
+ --foreground: #E0E0E0;
31
+ /* Light gray text */
32
+ --sidebar-background: #14141b;
33
+ /* Darker sidebar background */
34
+ --primary: #1f202a;
35
+ --secondary: #2a2b3b;
36
+ /* Soft secondary background for content */
37
+ --accent: #4a557f;
38
+ --hover-accent: #5c608a;
39
+ /* Slightly lighter accent color for hover */
40
+ --button-background: #3a3b4c;
41
+ /* Button background */
42
+ --button-hover: #2D2D38;
43
+ /* Darker button hover */
44
+ --shadow-dark: rgba(0, 0, 0, 0.15);
45
+ /* Shadow for depth */
46
  }
47
  }
48
 
 
76
  color-scheme: dark;
77
  }
78
  }
79
+
80
+
81
+
82
+ @keyframes fadeIn {
83
+ from {
84
+ opacity: 0;
85
+ }
86
+
87
+ to {
88
+ opacity: 1;
89
+ }
90
+ }
91
+
92
+ @keyframes pulse {
93
+
94
+ 0%,
95
+ 100% {
96
+ transform: scale(1);
97
+ }
98
+
99
+ 50% {
100
+ transform: scale(1.025);
101
+ }
102
+ }
103
+
104
+ .pulse-and-fade {
105
+ animation: fadeIn .5s ease-in-out, pulse .3s ease-in-out;
106
+ }
frontend/app/layout.js CHANGED
@@ -1,164 +1,43 @@
1
  'use client';
2
- import { useState, useEffect } from 'react';
3
- import { useRouter } from 'next/navigation';
4
- import { WebSocketProvider, useWebSocket } from './WebSocketContext';
5
  import './globals.css';
6
- import Sidebar from './Sidebar';
7
- import Auth from './Auth';
 
 
8
 
9
- export default function RootLayout({ children }) {
10
- const [username, setUsername] = useState('');
11
- const [password, setPassword] = useState('');
12
- const [confirmPassword, setConfirmPassword] = useState('');
13
- const [isLoading, setIsLoading] = useState(false);
14
- const [error, setError] = useState('');
15
- const [recipientList, setRecipientList] = useState([]);
16
- const [searchQuery, setSearchQuery] = useState('');
17
- const [filteredUsers, setFilteredUsers] = useState([]);
18
- const [isLoggedIn, setIsLoggedIn] = useState(false);
19
- const [isSignup, setIsSignup] = useState('');
20
- const [sessionToken, setSessionToken] = useState(''); // Initialize sessionToken state
21
 
22
- useEffect(() => {
23
- // Check if there's a valid session token in localStorage
24
- const sessionToken = localStorage.getItem('sessionToken');
25
- if (sessionToken) {
26
- setIsLoggedIn(true);
27
- setUsername(localStorage.getItem('me'));
28
- }
29
- }, []);
30
 
31
  return (
32
  <html lang="en">
33
  <body>
34
- <WebSocketProvider>
35
- <WebSocketLayout
36
- username={username}
37
- setUsername={setUsername}
38
- password={password}
39
- setPassword={setPassword}
40
- confirmPassword={confirmPassword}
41
- setConfirmPassword={setConfirmPassword}
42
- isLoading={isLoading}
43
- setIsLoading={setIsLoading}
44
- error={error}
45
- setError={setError}
46
- recipientList={recipientList}
47
- setRecipientList={setRecipientList}
48
- searchQuery={searchQuery}
49
- setSearchQuery={setSearchQuery}
50
- filteredUsers={filteredUsers}
51
- setFilteredUsers={setFilteredUsers}
52
- isLoggedIn={isLoggedIn}
53
- setIsLoggedIn={setIsLoggedIn}
54
- isSignup={isSignup}
55
- setIsSignup={setIsSignup}
56
- sessionToken={sessionToken} // Pass sessionToken down to WebSocketLayout
57
- setSessionToken={setSessionToken} // Pass setSessionToken down to WebSocketLayout
58
- >
59
  {children}
60
- </WebSocketLayout>
61
- </WebSocketProvider>
62
  </body>
63
  </html>
64
  );
65
- }
66
-
67
- function WebSocketLayout({
68
- username,
69
- setUsername,
70
- password,
71
- setPassword,
72
- confirmPassword,
73
- setConfirmPassword,
74
- isLoading,
75
- setIsLoading,
76
- error,
77
- setError,
78
- recipientList,
79
- setRecipientList,
80
- searchQuery,
81
- setSearchQuery,
82
- filteredUsers,
83
- setFilteredUsers,
84
- isLoggedIn,
85
- setIsLoggedIn,
86
- isSignup,
87
- setIsSignup,
88
- sessionToken,
89
- setSessionToken, // Receive setSessionToken
90
- children,
91
- }) {
92
- const { ws, status } = useWebSocket();
93
- const router = useRouter();
94
-
95
- const handleUserSelect = (recipient) => {
96
- router.push(`/u/${recipient}`);
97
- };
98
-
99
- useEffect(() => {
100
- if (ws) {
101
- ws.onmessage = (event) => {
102
- const data = JSON.parse(event.data);
103
- console.log('Received message:', data);
104
-
105
- if (data.status === 'success') {
106
- setIsLoading(false);
107
- setIsLoggedIn(true);
108
- setRecipientList(data.recipients || []);
109
- localStorage.setItem('me', username);
110
- localStorage.setItem('sessionToken', data.token); // Store session token
111
- setSessionToken(data.token); // Update sessionToken state
112
- } else if (data.status === 'error') {
113
- setError(data.message);
114
- setIsLoading(false);
115
- }
116
- };
117
- }
118
- }, [ws, username]);
119
-
120
- return (
121
- <div>
122
- {isLoggedIn ? (
123
- <div style={{ display: 'flex', minHeight: '100vh' }}>
124
- <Sidebar
125
- searchQuery={searchQuery}
126
- setSearchQuery={setSearchQuery}
127
- setFilteredUsers={setFilteredUsers}
128
- filteredUsers={filteredUsers}
129
- handleUserSelect={handleUserSelect}
130
- />
131
- <div style={{ flex: 1 }}>{children}</div>
132
- </div>
133
- ) : (
134
- <div style={loginContainerStyle}>
135
- <Auth
136
- username={username}
137
- setUsername={setUsername}
138
- password={password}
139
- setPassword={setPassword}
140
- confirmPassword={confirmPassword}
141
- setConfirmPassword={setConfirmPassword}
142
- isLoading={isLoading}
143
- setIsLoading={setIsLoading}
144
- error={error}
145
- setError={setError}
146
- isSignup={isSignup}
147
- setIsSignup={setIsSignup}
148
- ws={ws}
149
- status={status}
150
- setSessionToken={setSessionToken} // Pass setSessionToken to Auth
151
- />
152
- </div>
153
- )}
154
- </div>
155
- );
156
- }
157
-
158
- const loginContainerStyle = {
159
- display: 'flex',
160
- justifyContent: 'center',
161
- alignItems: 'center',
162
- height: '100vh',
163
- backgroundColor: '#1c1c37',
164
- };
 
1
  'use client';
2
+
 
 
3
  import './globals.css';
4
+ import Sidebar from '@/components/Sidebar';
5
+ import { NexusAuthWrapper } from "@components/NexusAuth";
6
+ import { ToastContainer, Flip } from 'react-toastify';
7
+ import { CheckCircleIcon, InformationCircleIcon, ExclamationCircleIcon } from '@heroicons/react/20/solid';
8
 
9
+ import { ToastProvider } from "@lib/ToastContext";
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ export default function RootLayout({ children }) {
 
 
 
 
 
 
 
12
 
13
  return (
14
  <html lang="en">
15
  <body>
16
+ <ToastProvider>
17
+ <ToastContainer
18
+ transition={Flip}
19
+ theme="dark"
20
+ icon={({ type, theme }) => {
21
+ switch (type) {
22
+ case 'info':
23
+ return <InformationCircleIcon className="text-indigo-400" />;
24
+ case 'error':
25
+ return <InformationCircleIcon className="text-red-500" />;
26
+ case 'success':
27
+ return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
28
+ case 'warning':
29
+ return <ExclamationCircleIcon className="text-yellow-500" />;
30
+ default:
31
+ return null;
32
+ }
33
+ }}
34
+ />
35
+ <NexusAuthWrapper>
36
+ <Sidebar />
 
 
 
 
37
  {children}
38
+ </NexusAuthWrapper>
39
+ </ToastProvider>
40
  </body>
41
  </html>
42
  );
43
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/not-found.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+
5
+ export default function Custom404() {
6
+ const router = useRouter();
7
+
8
+ const handleGoBack = () => {
9
+ router.back();
10
+ };
11
+
12
+ return (
13
+ <div style={styles.container}>
14
+ <h1 className='pulse-and-fade' style={styles.heading}>404 - Page Not Found</h1>
15
+ <p className='pulse-and-fade' style={styles.message}>
16
+ Oops! The page you're looking for doesn't exist. Let’s get you back on track!
17
+ </p>
18
+ <div className='pulse-and-fade' style={styles.buttons}>
19
+ <button onClick={handleGoBack} style={styles.button}>
20
+ Go Back
21
+ </button>
22
+ <button onClick={() => router.push('/')} style={styles.button}>
23
+ Go to Home
24
+ </button>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ const styles = {
31
+ container: {
32
+ display: 'flex',
33
+ flexDirection: 'column',
34
+ justifyContent: 'center',
35
+ alignItems: 'center',
36
+ height: '100vh',
37
+ backgroundColor: 'var(--background)',
38
+ color: 'var(--foreground)',
39
+ textAlign: 'center',
40
+ },
41
+ heading: {
42
+ fontSize: '48px',
43
+ fontWeight: 'bold',
44
+ marginBottom: '20px',
45
+ },
46
+ message: {
47
+ fontSize: '18px',
48
+ marginBottom: '30px',
49
+ maxWidth: '400px',
50
+ color: 'var(--foreground)',
51
+ },
52
+ buttons: {
53
+ display: 'flex',
54
+ gap: '15px',
55
+ },
56
+ button: {
57
+ padding: '10px 20px',
58
+ fontSize: '16px',
59
+ borderRadius: '5px',
60
+ border: 'none',
61
+ cursor: 'pointer',
62
+ backgroundColor: 'var(--button-background)',
63
+ color: 'var(--foreground)',
64
+ transition: 'background-color 0.3s',
65
+ },
66
+ buttonHover: {
67
+ backgroundColor: 'var(--button-hover)',
68
+ },
69
+ };
frontend/app/page.js CHANGED
@@ -1,6 +1,8 @@
1
  'use client';
2
 
3
  import { useState, useEffect } from 'react';
 
 
4
 
5
  export default function ChatPage() {
6
  const [me, setMe] = useState('');
@@ -16,16 +18,11 @@ export default function ChatPage() {
16
  }, []);
17
 
18
  return (
19
- <div style={{ width: '100%', padding: '20px', backgroundColor: 'rgb(28, 28, 47)', minHeight: '100vh' }}>
20
- <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100%', width: '100%' }}>
21
  <div style={{ textAlign: 'center' }}>
22
- <h1 style={{ color: '#4a4a4a', fontSize: '24px', marginBottom: '20px' }}>Welcome, {me || 'Stranger'} to Nexus!</h1>
23
- <p style={{ color: '#7d7d7d', fontSize: '16px', marginBottom: '20px' }}>
24
- Open the sidebar to find and start conversations with your contacts (or your future best friends).
25
- </p>
26
- <div style={{ fontSize: '14px', color: '#aaa' }}>
27
- <p>{me ? 'No chats selected yet. Choose a conversation from the sidebar to get started.' : 'Log in to start chatting!'}</p>
28
- </div>
29
  </div>
30
  </div>
31
  </div>
 
1
  'use client';
2
 
3
  import { useState, useEffect } from 'react';
4
+ import UserSearch from '@components/SearchUsers';
5
+ import RecentChats from '@/components/RecentChats';
6
 
7
  export default function ChatPage() {
8
  const [me, setMe] = useState('');
 
18
  }, []);
19
 
20
  return (
21
+ <div style={{ width: '100%', padding: '20px', backgroundColor: 'rgb(28, 28, 47)', height: '100dvh' }}>
22
+ <div className='pulse-and-fade' style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', width: '100%' }}>
23
  <div style={{ textAlign: 'center' }}>
24
+ <h1 style={{ color: '#4a4a4a', fontSize: '24px', marginBottom: '20px' }}>Nexus MS</h1>
25
+ <RecentChats/>
 
 
 
 
 
26
  </div>
27
  </div>
28
  </div>
frontend/app/search/page.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import UserSearch from '@components/SearchUsers';
5
+
6
+ export default function SearchUsersPage() {
7
+ const [me, setMe] = useState('');
8
+
9
+ useEffect(() => {
10
+ // Retrieve 'me' from localStorage because who doesn't like remembering things?
11
+ const storedMe = localStorage.getItem('me');
12
+ if (storedMe) {
13
+ setMe(storedMe);
14
+ } else {
15
+ toast.warn("Hey, there's no 'me' in localStorage. Are you sure you're logged in?");
16
+ }
17
+ }, []);
18
+
19
+ return (
20
+ <div className='search-page' style={{ width: '100%', padding: '20px', backgroundColor: 'rgb(28, 28, 47)', height: '100dvh' }}>
21
+ <div className='pulse-and-fade' style={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
22
+ <div style={{ textAlign: 'center' }}>
23
+ <h1 style={{ color: '#4a4a4a', fontSize: '24px', marginBottom: '20px' }}>Nexus MS</h1>
24
+ <UserSearch />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
frontend/app/u/[recipientusername]/page.js CHANGED
@@ -2,61 +2,40 @@
2
 
3
  import { useState, useEffect } from "react";
4
  import { useParams, useRouter } from "next/navigation";
5
- import { useWebSocket } from "../../WebSocketContext";
6
  import Image from "next/image";
 
 
7
 
8
  export default function ChatPage() {
9
  const { recipientusername } = useParams();
10
  const [recipient, setRecipient] = useState(null);
11
  const [message, setMessage] = useState("");
12
  const [chatLog, setChatLog] = useState([]);
13
- const { ws } = useWebSocket();
14
 
15
  const [me, setMe] = useState("");
16
  const [recipientExists, setRecipientExists] = useState(true);
17
- const router = useRouter(); // Hook to navigate
18
-
19
- const servicesUrl = process.env.NEXT_PUBLIC_SERVICES_URL;
20
- if (!servicesUrl) {
21
- console.error('Environment variable NEXT_PUBLIC_SERVICES_URL is not defined.');
22
- return;
23
- }
24
-
25
- useEffect(() => {
26
- // Retrieve 'me' from localStorage
27
- const storedMe = localStorage.getItem("me");
28
- if (storedMe) {
29
- setMe(storedMe);
30
- }
31
- }, []);
32
 
33
  useEffect(() => {
34
- if (recipientusername) {
 
 
 
 
 
 
 
 
35
  setRecipient(recipientusername);
36
- }
37
- }, [recipientusername]);
 
 
 
 
38
 
39
- useEffect(() => {
40
- if (recipient) {
41
- // Check if recipient exists by making an API request to the server
42
- const checkUserExistence = async () => {
43
- try {
44
- const response = await fetch(`${servicesUrl}/api/user/${recipient}`);
45
- if (response.ok) {
46
- setRecipientExists(true);
47
- const storedMessages = JSON.parse(localStorage.getItem(recipient)) || [];
48
- setChatLog(formatMessages(storedMessages));
49
- } else {
50
- setRecipientExists(false);
51
- }
52
- } catch (error) {
53
- console.error("Error checking user existence:", error);
54
- setRecipientExists(false);
55
- }
56
- };
57
- checkUserExistence();
58
- }
59
- }, [recipient, servicesUrl]);
60
 
61
  useEffect(() => {
62
  const interval = setInterval(() => {
@@ -70,15 +49,6 @@ export default function ChatPage() {
70
  }, [recipient]);
71
 
72
  const handleSendMessage = () => {
73
- if (!ws) {
74
- alert("WebSocket is not connected.");
75
- return;
76
- }
77
-
78
- if (!message.trim()) {
79
- alert("Message cannot be empty.");
80
- return;
81
- }
82
 
83
  if (recipient && ws) {
84
  const msgData = JSON.stringify({ recipient, message });
@@ -115,6 +85,7 @@ export default function ChatPage() {
115
 
116
  return (
117
  <div
 
118
  style={{
119
  backgroundColor: "#1a1a2e",
120
  minHeight: "100vh",
@@ -197,11 +168,11 @@ export default function ChatPage() {
197
  src={
198
  entry.from === "You"
199
  ? `https://ui-avatars.com/api/?name=${encodeURIComponent(
200
- me
201
- )}&background=random`
202
  : `https://ui-avatars.com/api/?name=${encodeURIComponent(
203
- recipient
204
- )}&background=random`
205
  }
206
  alt={`${entry.from}'s avatar`}
207
  width={30}
@@ -230,7 +201,7 @@ export default function ChatPage() {
230
  </div>
231
  ))
232
  ) : (
233
- <p>No messages yet</p>
234
  )}
235
  </div>
236
 
@@ -277,10 +248,10 @@ export default function ChatPage() {
277
  </div>
278
  </>
279
  ) : (
280
- <p>User does not exist.</p>
281
  )
282
  ) : (
283
- <p>Loading recipient...</p>
284
  )}
285
  </div>
286
  );
 
2
 
3
  import { useState, useEffect } from "react";
4
  import { useParams, useRouter } from "next/navigation";
 
5
  import Image from "next/image";
6
+ import NexusAuthApi from "@/lib/Nexus_Auth_API";
7
+ import { toast } from "react-toastify";
8
 
9
  export default function ChatPage() {
10
  const { recipientusername } = useParams();
11
  const [recipient, setRecipient] = useState(null);
12
  const [message, setMessage] = useState("");
13
  const [chatLog, setChatLog] = useState([]);
 
14
 
15
  const [me, setMe] = useState("");
16
  const [recipientExists, setRecipientExists] = useState(true);
17
+ const router = useRouter();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  useEffect(() => {
20
+ const fetchRecipientData = async () => {
21
+ // Retrieve 'me' from localStorage
22
+ const storedMe = localStorage.getItem("me");
23
+ if (storedMe) {
24
+ setMe(storedMe);
25
+ }
26
+ // Check if the recipient exists
27
+ const response = await NexusAuthApi.isUsernameAvailable(recipientusername);
28
+ // Set the recipient from the URL
29
  setRecipient(recipientusername);
30
+ if (response.username === recipientusername && response.is_available === false) {
31
+ setRecipientExists(true);
32
+ } else if (response.username === recipientusername && response.is_available === true) {
33
+ setRecipientExists(false);
34
+ }
35
+ };
36
 
37
+ fetchRecipientData();
38
+ }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  useEffect(() => {
41
  const interval = setInterval(() => {
 
49
  }, [recipient]);
50
 
51
  const handleSendMessage = () => {
 
 
 
 
 
 
 
 
 
52
 
53
  if (recipient && ws) {
54
  const msgData = JSON.stringify({ recipient, message });
 
85
 
86
  return (
87
  <div
88
+ className='pulse-and-fade'
89
  style={{
90
  backgroundColor: "#1a1a2e",
91
  minHeight: "100vh",
 
168
  src={
169
  entry.from === "You"
170
  ? `https://ui-avatars.com/api/?name=${encodeURIComponent(
171
+ me
172
+ )}&background=random`
173
  : `https://ui-avatars.com/api/?name=${encodeURIComponent(
174
+ recipient
175
+ )}&background=random`
176
  }
177
  alt={`${entry.from}'s avatar`}
178
  width={30}
 
201
  </div>
202
  ))
203
  ) : (
204
+ <p className='page-content'>No messages yet</p>
205
  )}
206
  </div>
207
 
 
248
  </div>
249
  </>
250
  ) : (
251
+ <p className="className='page-content' flex justify-center">User does not exist.</p>
252
  )
253
  ) : (
254
+ <p className="className='page-content' flex justify-center">Loading recipient...</p>
255
  )}
256
  </div>
257
  );
frontend/assets/logo.jpg ADDED
frontend/assets/logo2.jpg ADDED
frontend/assets/logo6.png ADDED
frontend/components/NexusAuth.css ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Container Styling */
2
+ .nexus-auth-form {
3
+ max-width: 400px;
4
+ margin: 10px auto;
5
+ padding: 20px;
6
+ border: none;
7
+ border-radius: 12px;
8
+ background: linear-gradient(145deg, var(--accent), var(--secondary));
9
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2), 0 4px 6px rgba(0, 0, 0, 0.1);
10
+ color: var(--foreground);
11
+ animation: fadeIn .5s ease-in-out, pulse .3s ease-in-out;
12
+ }
13
+
14
+ /* Header Styling */
15
+ .nexus-auth-form h2 {
16
+ text-align: center;
17
+ margin-bottom: 10px;
18
+ color: var(--foreground);
19
+ font-size: 1.4rem;
20
+ font-weight: 600;
21
+ letter-spacing: 1px;
22
+ text-transform: uppercase;
23
+ border-bottom: 2px solid var(--primary);
24
+ padding-bottom: 5px;
25
+ }
26
+
27
+ /* Form Group Styling */
28
+ .form-group {
29
+ margin-bottom: 5px;
30
+ }
31
+
32
+ .form-group label {
33
+ display: block;
34
+ margin-bottom: 5px;
35
+ color: var(--foreground);
36
+ font-size: 1rem;
37
+ font-weight: 500;
38
+ letter-spacing: 0.5px;
39
+ }
40
+
41
+ .form-group input {
42
+ width: 100%;
43
+ padding: 10px;
44
+ background-color: var(--secondary);
45
+ color: var(--foreground);
46
+ border: 2px solid var(--primary);
47
+ border-radius: 8px;
48
+ font-size: 1rem;
49
+ transition: all 0.3s ease;
50
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
51
+ }
52
+
53
+ .form-group input:focus {
54
+ border-color: var(--accent);
55
+ box-shadow: 0 0 10px var(--accent);
56
+ outline: none;
57
+ }
58
+
59
+ /* Error Message Styling */
60
+ .error-message {
61
+ color: #ff6b6b; /* Vibrant red for errors */
62
+ font-size: 0.9rem;
63
+ margin-top: 5px;
64
+ font-weight: bold;
65
+ }
66
+
67
+ /* Submit Button Styling */
68
+ .submit-button {
69
+ width: 100%;
70
+ padding: 14px;
71
+ font-size: 1rem;
72
+ font-weight: bold;
73
+ color: var(--foreground);
74
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
75
+ border: none;
76
+ border-radius: 8px;
77
+ cursor: pointer;
78
+ transition: all 0.3s ease;
79
+ text-transform: uppercase;
80
+ }
81
+
82
+ .submit-button:hover {
83
+ background: linear-gradient(135deg, var(--secondary), var(--accent));
84
+ transform: translateY(-2px) scale(1.02);
85
+ box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2);
86
+ }
87
+
88
+ .submit-button:disabled {
89
+ background-color: var(--primary);
90
+ opacity: 0.5;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ /* Signup/Login Section Styling */
95
+ .nexus-auth-signup-login {
96
+ text-align: center;
97
+ margin-top: 40px;
98
+ animation: fadeIn .5s ease-in-out, pulse .3s ease-in-out;
99
+ }
100
+
101
+ .nexus-auth-signup-login h1 {
102
+ margin-bottom: 20px;
103
+ color: var(--foreground);
104
+ font-size: 1.5rem;
105
+ font-weight: bold;
106
+ letter-spacing: 0.8px;
107
+ }
108
+
109
+ .nexus-auth-signup-login button {
110
+ padding: 12px 30px;
111
+ font-size: 1rem;
112
+ font-weight: bold;
113
+ color: var(--foreground);
114
+ background: linear-gradient(145deg, var(--primary), var(--secondary));
115
+ border: none;
116
+ border-radius: 8px;
117
+ cursor: pointer;
118
+ transition: all 0.3s ease;
119
+ }
120
+
121
+ .nexus-auth-signup-login button:hover {
122
+ background: linear-gradient(145deg, var(--secondary), var(--accent));
123
+ transform: translateY(-2px);
124
+ box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2);
125
+ }
126
+
127
+ /* Responsive Design */
128
+ @media (max-width: 768px) {
129
+ .nexus-auth-form {
130
+ padding: 20px;
131
+ max-width: 90%;
132
+ }
133
+
134
+ .nexus-auth-form h2 {
135
+ font-size: 1.5rem;
136
+ }
137
+
138
+ .form-group label {
139
+ font-size: 0.9rem;
140
+ }
141
+
142
+ .form-group input {
143
+ padding: 10px;
144
+ font-size: 0.95rem;
145
+ }
146
+
147
+ .submit-button {
148
+ padding: 12px;
149
+ font-size: 0.9rem;
150
+ }
151
+
152
+ .nexus-auth-signup-login h1 {
153
+ font-size: 1.3rem;
154
+ }
155
+
156
+ .nexus-auth-signup-login button {
157
+ font-size: 0.9rem;
158
+ }
159
+ }
160
+
161
+ @media (max-width: 480px) {
162
+ .nexus-auth-form {
163
+ padding: 10px;
164
+ max-width: 90%;
165
+ }
166
+
167
+ .nexus-auth-form h2 {
168
+ font-size: 1.3rem;
169
+ }
170
+
171
+ .form-group label {
172
+ font-size: 0.8rem;
173
+ }
174
+
175
+ .form-group input {
176
+ padding: 8px;
177
+ font-size: 0.85rem;
178
+ }
179
+
180
+ .submit-button {
181
+ padding: 10px;
182
+ font-size: 0.85rem;
183
+ }
184
+
185
+ .nexus-auth-signup-login h1 {
186
+ font-size: 1.2rem;
187
+ }
188
+
189
+ .nexus-auth-signup-login button {
190
+ padding: 10px 20px;
191
+ font-size: 0.85rem;
192
+ }
193
+ }
frontend/components/NexusAuth.js ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import './NexusAuth.css';
3
+ import { useState, useEffect } from 'react';
4
+ import NexusAuthApi from '@lib/Nexus_Auth_API';
5
+ import SplashScreen from '@components/SplashScreen';
6
+ import { useToast } from '@lib/ToastContext';
7
+ import { CheckCircleIcon } from '@heroicons/react/20/solid';
8
+
9
+ const SignupForm = ({ onSignup }) => {
10
+ const [username, setUsername] = useState('');
11
+ const [password, setPassword] = useState('');
12
+ const [confirmPassword, setConfirmPassword] = useState('');
13
+ const [email, setEmail] = useState('');
14
+ const [usernameAvailable, setUsernameAvailable] = useState(null); // null for initial state
15
+ const [doesUsernameContainInvalidChars, setDoesUsernameContainInvalidChars] = useState(false);
16
+ const [doesUsernameExceedMinLength, setDoesUsernameExceedMinLength] = useState(false);
17
+ const [passwordValid, setPasswordValid] = useState(false); // Initially invalid
18
+ const [formValid, setFormValid] = useState(false);
19
+ const [debounceTimeout, setDebounceTimeout] = useState(null); // Store timeout ID
20
+
21
+ const minUsernameLength = 3;
22
+
23
+ const validatePassword = (password) => {
24
+ return password.length >= 8;
25
+ };
26
+
27
+ const handleUsernameChange = (e) => {
28
+ const newUsername = e.target.value;
29
+ setUsername(newUsername);
30
+
31
+ // Reset username availability while typing
32
+ setUsernameAvailable(null);
33
+
34
+ // Clear any existing debounce timeout
35
+ if (debounceTimeout) {
36
+ clearTimeout(debounceTimeout);
37
+ }
38
+
39
+ // Check for invalid characters
40
+ const invalidChars = /[^a-zA-Z0-9_]/g;
41
+ if (invalidChars.test(newUsername)) {
42
+ setDoesUsernameContainInvalidChars(true);
43
+ setTimeout(() => {
44
+ setDoesUsernameContainInvalidChars(false);
45
+ }, 2000); // Show error for 2 seconds
46
+ }
47
+
48
+ // Basic sanitization to prevent SQL injection
49
+ const sanitizedUsername = newUsername.replace(invalidChars, '');
50
+
51
+ if (sanitizedUsername.length < minUsernameLength) {
52
+ setDoesUsernameExceedMinLength(false);
53
+ return;
54
+ } else {
55
+ setDoesUsernameExceedMinLength(true);}
56
+
57
+ if (sanitizedUsername.trim().length > 0) {
58
+ // Set a new timeout to check availability
59
+ const newTimeout = setTimeout(async () => {
60
+ try {
61
+ const response = await NexusAuthApi.isUsernameAvailable(sanitizedUsername);
62
+ setUsernameAvailable(response?.is_available === true);
63
+ } catch (error) {
64
+ console.error('Error checking username availability:', error);
65
+ setUsernameAvailable(null); // Fallback state
66
+ }
67
+ }, 1000); // 1-second debounce delay
68
+
69
+ setDebounceTimeout(newTimeout);
70
+ } else {
71
+ setUsernameAvailable(null); // Reset availability check when input is empty
72
+ }
73
+
74
+ // Set sanitized username
75
+ setUsername(sanitizedUsername);
76
+ };
77
+
78
+ const handlePasswordChange = (e) => {
79
+ const newPassword = e.target.value;
80
+ setPassword(newPassword);
81
+ setPasswordValid(validatePassword(newPassword));
82
+ };
83
+
84
+ const handleConfirmPasswordChange = (e) => {
85
+ setConfirmPassword(e.target.value);
86
+ };
87
+
88
+ const handleSubmit = (e) => {
89
+ e.preventDefault();
90
+
91
+ // Set email to null if it's empty
92
+ const emailValue = email.trim() === '' ? null : email;
93
+
94
+ if (password === confirmPassword && passwordValid) {
95
+ onSignup({ username, password, email: emailValue });
96
+ }
97
+ };
98
+
99
+ useEffect(() => {
100
+ setFormValid(
101
+ usernameAvailable === true &&
102
+ password === confirmPassword &&
103
+ passwordValid &&
104
+ username.length >= minUsernameLength &&
105
+ !doesUsernameContainInvalidChars
106
+ );
107
+ }, [username, password, confirmPassword, usernameAvailable, passwordValid]);
108
+
109
+ return (
110
+ <form onSubmit={handleSubmit} className="nexus-auth-form">
111
+ <h2>Signup</h2>
112
+ <div className="form-group">
113
+ <label>Username:</label>
114
+ <input
115
+ type="text"
116
+ value={username}
117
+ onChange={handleUsernameChange}
118
+ required
119
+ className={usernameAvailable === false ? 'error' : ''} />
120
+ {usernameAvailable === true && username.length > 0 && (
121
+ <CheckCircleIcon className="h-5 w-5 text-green-500" />
122
+ )}
123
+ {doesUsernameExceedMinLength === false && (
124
+ <p className="error-message text-red-500">Username must have more than {minUsernameLength} characters.</p>
125
+ )}
126
+ {doesUsernameContainInvalidChars === true && (
127
+ <p className="error-message text-red-500">Username cannot contain invalid characters.</p>
128
+ )}
129
+ {usernameAvailable === false && (
130
+ <p className="error-message text-red-500">Username is already taken</p>
131
+ )}
132
+ {usernameAvailable === null && username.length > 0 && (
133
+ <p className="typing-message text-green-500">Checking username availability...</p>
134
+ )}
135
+ </div>
136
+ <div className="form-group">
137
+ <label>Password:</label>
138
+ <input
139
+ type="password"
140
+ value={password}
141
+ onChange={handlePasswordChange}
142
+ required
143
+ className={passwordValid ? '' : 'error'} />
144
+ {passwordValid && (
145
+ <CheckCircleIcon className="h-5 w-5 text-green-500" />
146
+ )}
147
+ {!passwordValid && (
148
+ <p className="error-message text-yellow-500">Password must be at least 8 characters long</p>
149
+ )}
150
+ </div>
151
+ <div className="form-group">
152
+ <label>Confirm Password:</label>
153
+ <input
154
+ type="password"
155
+ value={confirmPassword}
156
+ onChange={handleConfirmPasswordChange}
157
+ required
158
+ className={password === confirmPassword ? '' : 'error'} />
159
+ {password === confirmPassword && confirmPassword.length > 0 && (
160
+ <CheckCircleIcon className="h-5 w-5 text-green-500" />
161
+ )}
162
+ {password !== confirmPassword && confirmPassword.length > 0 && (
163
+ <p className="error-message text-red-500">Passwords do not match</p>
164
+ )}
165
+ </div>
166
+ <div className="form-group">
167
+ <label>Email (optional):</label>
168
+ <input
169
+ type="email"
170
+ value={email}
171
+ onChange={(e) => setEmail(e.target.value)} />
172
+ </div>
173
+ <button type="submit" className="submit-button" disabled={!formValid}>
174
+ Signup
175
+ </button>
176
+ </form>
177
+ );
178
+ };
179
+
180
+ const LoginForm = ({ onLogin }) => {
181
+ const [username, setUsername] = useState('');
182
+ const [password, setPassword] = useState('');
183
+
184
+ const handleSubmit = (e) => {
185
+ e.preventDefault();
186
+ onLogin({ username, password });
187
+ };
188
+
189
+ return (
190
+ <form onSubmit={handleSubmit} className="nexus-auth-form">
191
+ <h2>Login</h2>
192
+ <div className="form-group">
193
+ <label>Username:</label>
194
+ <input
195
+ type="text"
196
+ value={username}
197
+ onChange={(e) => setUsername(e.target.value)}
198
+ required />
199
+ </div>
200
+ <div className="form-group">
201
+ <label>Password:</label>
202
+ <input
203
+ type="password"
204
+ value={password}
205
+ onChange={(e) => setPassword(e.target.value)}
206
+ required />
207
+ </div>
208
+ <button type="submit" className="submit-button">
209
+ Login
210
+ </button>
211
+ </form>
212
+ );
213
+ };
214
+
215
+ export const NexusAuthWrapper = ({ children }) => {
216
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
217
+ const [isSignup, setIsSignup] = useState(false);
218
+ const [isLoading, setIsLoading] = useState(true);
219
+ const toast = useToast();
220
+
221
+ useEffect(() => {
222
+ const validateUserSession = async () => {
223
+ const storedUsername = localStorage.getItem("me");
224
+ const storedToken = localStorage.getItem("s_tkn");
225
+ const storedUserID = localStorage.getItem("u_id");
226
+
227
+ if (storedUsername && storedToken && storedUserID) {
228
+ try {
229
+ // Validate the token with the NexusAuthApi
230
+ const response = await NexusAuthApi.validateToken(storedUserID, storedToken);
231
+
232
+ if (response.data && response.data.user_id) {
233
+ // Token is valid; response contains user details
234
+ console.log("User is already logged in.");
235
+ toast.info("Welcome back, " + response.data.username + "!");
236
+ setIsLoggedIn(true);
237
+
238
+ // Optionally, update localStorage with new details if needed
239
+ localStorage.setItem("me", response.data.username);
240
+ localStorage.setItem("s_tkn", response.data.access_token);
241
+ localStorage.setItem("u_id", response.data.user_id);
242
+ localStorage.setItem("a_l", response.data.access_level);
243
+ } else if (response.status === 401) {
244
+ // Token is invalid; clear local storage
245
+ console.error("Token validation failed with status 401:");
246
+ clearLocalStorage();
247
+ } else {
248
+ // Handle other errors (e.g., network issues)
249
+ console.error("Token validation failed due to an unexpected error:", response.data);
250
+ toast.error("Unable to validate token. Please check your connection.");
251
+ }
252
+ } catch (error) {
253
+ // Handle other errors (e.g., network issues)
254
+ console.error("Token validation failed due to an unexpected error:", error);
255
+ toast.error("Unable to validate token. Please check your connection.");
256
+
257
+ }
258
+ }
259
+ setIsLoading(false);
260
+
261
+ };
262
+
263
+ const clearLocalStorage = () => {
264
+ localStorage.removeItem("me");
265
+ localStorage.removeItem("s_tkn");
266
+ localStorage.removeItem("u_id");
267
+ localStorage.removeItem("a_l");
268
+ setIsLoggedIn(false);
269
+ toast.error("Session expired. Please login again.");
270
+ };
271
+
272
+ validateUserSession();
273
+ }, []);
274
+
275
+ const handleSignup = async (data) => {
276
+ setIsLoading(true);
277
+ try {
278
+ const response = await NexusAuthApi.signup(data.username, data.password, data.email);
279
+ console.log("Signup successful:", response);
280
+ setIsLoading(false);
281
+ toast.success('Signup successful. Please login to continue');
282
+ setIsSignup(false);
283
+ } catch (error) {
284
+ setIsLoading(false);
285
+ console.error("Signup failed:", error);
286
+ toast.error("Signup failed");
287
+ }
288
+ };
289
+
290
+ const handleLogin = async (data) => {
291
+ setIsLoading(true);
292
+ try {
293
+ const response = await NexusAuthApi.login(data.username, data.password);
294
+ console.log("Login successful:", response);
295
+ toast.success('Login successful.');
296
+ // Save username and token to localStorage
297
+ localStorage.setItem("me", response.username);
298
+ localStorage.setItem("s_tkn", response.access_token);
299
+ localStorage.setItem("u_id", response.user_id);
300
+ localStorage.setItem("a_l", response.access_level);
301
+
302
+ setIsLoggedIn(true);
303
+ setIsLoading(false);
304
+ } catch (error) {
305
+ setIsLoading(false);
306
+ console.error("Login failed:", error);
307
+ toast.error("Login failed");
308
+ }
309
+ };
310
+
311
+ if (isLoading) {
312
+ return <SplashScreen />;
313
+ }
314
+
315
+ return (
316
+ <div>
317
+ {isLoggedIn ? (
318
+ children
319
+ ) : (
320
+ <div className="nexus-auth-signup-login">
321
+ <h1>Nexus Accounts</h1>
322
+ <button onClick={() => setIsSignup(!isSignup)}>
323
+ {isSignup ? "Already have an Account? Login" : "Don't have an Account? Signup"}
324
+ </button>
325
+ {isSignup ? (
326
+ <SignupForm onSignup={handleSignup} />
327
+ ) : (
328
+ <LoginForm onLogin={handleLogin} />
329
+ )}
330
+ </div>
331
+ )}
332
+ </div>
333
+ );
334
+ };
frontend/components/RecentChats.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useState, useEffect } from 'react';
6
+
7
+ export default function RecentChats() {
8
+ const router = useRouter();
9
+ const [me, setMe] = useState('');
10
+ const [allChats, setAllChats] = useState([]);
11
+ useEffect(() => {
12
+ // Retrieve 'me' from localStorage
13
+ const storedMe = localStorage.getItem('me');
14
+ if (storedMe) {
15
+ setMe(storedMe);
16
+ }
17
+
18
+ // Retrieve all chats stored in localStorage
19
+ const allKeys = Object.keys(localStorage);
20
+ const chats = allKeys.filter((key) => key !== 'me' && key !== 's_tkn' && key !== 'a_l' && key !== 'u_id' && key !== 'ally-supports-cache').map((key) => {
21
+ return {
22
+ username: key,
23
+ chatData: JSON.parse(localStorage.getItem(key)),
24
+ };
25
+ });
26
+
27
+ setAllChats(chats);
28
+ }, []);
29
+
30
+ const handleUserSelect = (username) => {
31
+ router.push(`/u/${username}`);
32
+ };
33
+
34
+ return (
35
+ <div>
36
+ <h1 style={{ color: '#fff', marginBottom: '10px' }}>Chats</h1>
37
+ {allChats.length === 0 ? (
38
+ <div className='pulse-and-fade' style={{ color: '#7d7d7d', fontSize: '16px', marginBottom: '20px' }}>
39
+ <p>{me ? 'This looks empty. Open sidebar to go to the find page to look for someone to chat.' : 'Log in to start chatting!'}</p>
40
+ </div>
41
+ ) : (
42
+ <ul style={{ listStyleType: 'none', padding: 0 }}>
43
+ {allChats.map((chat, index) => (
44
+ <li
45
+ key={index}
46
+ style={{
47
+ color: '#fff',
48
+ cursor: 'pointer',
49
+ marginBottom: '15px',
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ }}
53
+ onClick={() => handleUserSelect(chat.username)}
54
+ >
55
+ <div
56
+ style={{
57
+ width: '40px',
58
+ height: '40px',
59
+ position: 'relative',
60
+ marginRight: '10px',
61
+ }}
62
+ >
63
+ <Image
64
+ src={`https://ui-avatars.com/api/?background=random&name=${encodeURIComponent(
65
+ chat.username
66
+ )}`}
67
+ alt={`${chat.username}'s avatar`}
68
+ layout="fill"
69
+ objectFit="cover"
70
+ style={{ borderRadius: '50%' }}
71
+ />
72
+ </div>
73
+ <span style={{ textDecoration: 'none', color: '#fff', fontSize: '16px' }}>
74
+ {chat.username} {chat.username === me ? '(Me)' : ''}
75
+ </span>
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ )}
80
+ </div>
81
+ );
82
+ }
frontend/components/RelayAPI.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // RelayAPI.js - Next.js Wrapper for the WebSocket Server
2
+
3
+ import { useEffect, useState, useCallback, useRef } from "react";
4
+
5
+ const useWebSocket = (userId) => {
6
+ const [socket, setSocket] = useState(null);
7
+ const [messages, setMessages] = useState([]);
8
+ const [connectedChannels, setConnectedChannels] = useState(new Set());
9
+ const reconnectInterval = useRef(null);
10
+
11
+ const connect = useCallback(() => {
12
+ const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/ws?user_id=${userId}`);
13
+
14
+ ws.onopen = () => {
15
+ console.debug("WebSocket connected.");
16
+ clearInterval(reconnectInterval.current);
17
+ };
18
+
19
+ ws.onmessage = (event) => {
20
+ try {
21
+ const data = JSON.parse(event.data);
22
+ console.debug("Received message:", data);
23
+ setMessages((prev) => [...prev, data]);
24
+ } catch (error) {
25
+ console.error("Failed to parse WebSocket message:", event.data);
26
+ }
27
+ };
28
+
29
+ ws.onclose = () => {
30
+ console.warn("WebSocket closed. Reconnecting...");
31
+ reconnectInterval.current = setInterval(() => connect(), 5000);
32
+ };
33
+
34
+ ws.onerror = (error) => {
35
+ console.error("WebSocket error:", error);
36
+ ws.close();
37
+ };
38
+
39
+ setSocket(ws);
40
+ }, [userId]);
41
+
42
+ useEffect(() => {
43
+ if (!userId) return;
44
+
45
+ connect();
46
+
47
+ return () => {
48
+ clearInterval(reconnectInterval.current);
49
+ if (socket) socket.close();
50
+ };
51
+ }, [connect, userId, socket]);
52
+
53
+ const sendMessage = useCallback(
54
+ (action, channel, message, targetUserId = null) => {
55
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
56
+ console.warn("WebSocket is not connected.");
57
+ return;
58
+ }
59
+
60
+ const payload = { action, channel, message, sender: userId };
61
+ if (targetUserId) payload.target_user_id = targetUserId;
62
+
63
+ try {
64
+ socket.send(JSON.stringify(payload));
65
+ console.debug("Sent message:", payload);
66
+ } catch (error) {
67
+ console.error("Failed to send WebSocket message:", error);
68
+ }
69
+ },
70
+ [socket, userId]
71
+ );
72
+
73
+ const joinChannel = useCallback(
74
+ (channel) => {
75
+ if (connectedChannels.has(channel)) return;
76
+
77
+ sendMessage("join", channel);
78
+ setConnectedChannels((prev) => new Set(prev).add(channel));
79
+ },
80
+ [sendMessage, connectedChannels]
81
+ );
82
+
83
+ const leaveChannel = useCallback(
84
+ (channel) => {
85
+ if (!connectedChannels.has(channel)) return;
86
+
87
+ sendMessage("leave", channel);
88
+ setConnectedChannels((prev) => {
89
+ const updated = new Set(prev);
90
+ updated.delete(channel);
91
+ return updated;
92
+ });
93
+ },
94
+ [sendMessage, connectedChannels]
95
+ );
96
+
97
+ const broadcast = useCallback(
98
+ (channel, message) => {
99
+ sendMessage("broadcast", channel, message);
100
+ },
101
+ [sendMessage]
102
+ );
103
+
104
+ const sendToUser = useCallback(
105
+ (targetUserId, message) => {
106
+ sendMessage("send_to_user", null, message, targetUserId);
107
+ },
108
+ [sendMessage]
109
+ );
110
+
111
+ return { messages, joinChannel, leaveChannel, broadcast, sendToUser };
112
+ };
113
+
114
+ export const RelayAPIProvider = ({ userId, children }) => {
115
+ const contextValue = useWebSocket(userId);
116
+
117
+ return (
118
+ <RelayAPIContext.Provider value={contextValue}>{children}</RelayAPIContext.Provider>
119
+ );
120
+ };
121
+
122
+ import React, { useContext } from "react";
123
+
124
+ const RelayAPIContext = React.createContext(null);
125
+
126
+ export const useRelayAPI = () => {
127
+ const context = useContext(RelayAPIContext);
128
+ if (!context) {
129
+ throw new Error("useRelayAPI must be used within a RelayAPIProvider");
130
+ }
131
+ return context;
132
+ };
frontend/components/SearchUsers.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useRef } from 'react';
4
+ import Image from 'next/image';
5
+ import { useRouter } from 'next/navigation';
6
+ import NexusAuthApi from '@lib/Nexus_Auth_API';
7
+
8
+ export default function UserSearch({ me }) {
9
+ const [searchQuery, setSearchQuery] = useState('');
10
+ const [filteredUsers, setFilteredUsers] = useState([]);
11
+ const router = useRouter();
12
+ const debounceTimeout = useRef(null);
13
+
14
+ const handleSearchChange = (e) => {
15
+ const query = e.target.value;
16
+ setSearchQuery(query);
17
+
18
+ if (debounceTimeout.current) {
19
+ clearTimeout(debounceTimeout.current);
20
+ }
21
+
22
+ debounceTimeout.current = setTimeout(async () => {
23
+ if (!query) {
24
+ setFilteredUsers([]);
25
+ return;
26
+ }
27
+
28
+ if (query.length > 2) {
29
+ try {
30
+ const data = await NexusAuthApi.searchUsers(query);
31
+ setFilteredUsers(Array.isArray(data) ? data : []);
32
+ } catch (error) {
33
+ console.error('Error fetching search results:', error);
34
+ setFilteredUsers([]);
35
+ }
36
+ } else {
37
+ setFilteredUsers([]);
38
+ }
39
+ }, 500); // Debounce delay of 500ms
40
+ };
41
+
42
+ const handleUserSelect = (recipient) => {
43
+ router.push(`/u/${recipient}`);
44
+ };
45
+
46
+ return (
47
+ <div style={styles.container}>
48
+ <input
49
+ type="text"
50
+ placeholder="Find People..."
51
+ value={searchQuery}
52
+ onChange={handleSearchChange}
53
+ style={styles.searchInput}
54
+ />
55
+
56
+ {searchQuery && (
57
+ <div
58
+ style={{
59
+ ...styles.searchResults,
60
+ height: filteredUsers.length ? '70dvh' : '200px',
61
+ }}
62
+ >
63
+ {filteredUsers.length === 0 ? <h4 style={styles.searchResultsHeading}>Found Nothing</h4> : <h4 style={styles.searchResultsHeading}>Found</h4>}
64
+ <ul style={styles.userList}>
65
+ {Array.isArray(filteredUsers) &&
66
+ filteredUsers.map((recipient, index) => (
67
+ <li
68
+ className="pulse-and-fade"
69
+ key={index}
70
+ style={styles.userItem}
71
+ onClick={() => handleUserSelect(recipient)}
72
+ >
73
+ <div style={styles.avatarContainer}>
74
+ <Image
75
+ src={`https://ui-avatars.com/api/?background=random&name=${encodeURIComponent(
76
+ recipient
77
+ )}`}
78
+ alt={`${recipient}'s avatar`}
79
+ layout="fill"
80
+ objectFit="cover"
81
+ style={styles.avatar}
82
+ />
83
+ </div>
84
+ <span style={styles.username}>
85
+ {recipient} {recipient === me ? '(Me)' : ''}
86
+ </span>
87
+ </li>
88
+ ))}
89
+ </ul>
90
+
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ const styles = {
98
+ container: {
99
+ backgroundColor: 'var(--background)',
100
+ color: 'var(--foreground)',
101
+ padding: '20px',
102
+ borderRadius: '12px',
103
+ boxShadow: '0px 6px 12px rgba(0, 0, 0, 0.2)',
104
+ maxWidth: '600px',
105
+ margin: 'auto',
106
+ transition: 'all 0.5s ease',
107
+ },
108
+ searchInput: {
109
+ width: '100%',
110
+ padding: '12px',
111
+ fontSize: '16px',
112
+ borderRadius: '8px',
113
+ border: '1px solid var(--hover-accent)',
114
+ backgroundColor: 'var(--secondary)',
115
+ color: 'var(--foreground)',
116
+ marginBottom: '15px',
117
+ boxShadow: 'inset 0 2px 4px rgba(0, 0, 0, 0.1)',
118
+ },
119
+ searchResults: {
120
+ overflow: 'hidden',
121
+ transition: 'height 0.3s ease',
122
+ backgroundColor: 'var(--secondary)',
123
+ borderRadius: '8px',
124
+ padding: '10px',
125
+ boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)',
126
+ },
127
+ searchResultsHeading: {
128
+ color: 'var(--foreground)',
129
+ marginBottom: '10px',
130
+ fontSize: '18px',
131
+ fontWeight: 'bold',
132
+ },
133
+ userList: {
134
+ listStyleType: 'none',
135
+ padding: 0,
136
+ maxHeight: '60dvh',
137
+ overflowX: 'hidden',
138
+ overflowY: 'auto',
139
+ },
140
+ userItem: {
141
+ cursor: 'pointer',
142
+ marginBottom: '15px',
143
+ display: 'flex',
144
+ alignItems: 'center',
145
+ padding: '10px',
146
+ borderRadius: '8px',
147
+ backgroundColor: 'var(--hover-accent)',
148
+ color: 'var(--foreground)',
149
+ transition: 'background-color 0.3s',
150
+ },
151
+ avatarContainer: {
152
+ width: '50px',
153
+ height: '50px',
154
+ position: 'relative',
155
+ marginRight: '15px',
156
+ borderRadius: '50%',
157
+ overflow: 'hidden',
158
+ },
159
+ avatar: {
160
+ borderRadius: '50%',
161
+ },
162
+ username: {
163
+ fontSize: '16px',
164
+ fontWeight: '500',
165
+ },
166
+ noResults: {
167
+ color: 'var(--foreground-muted)',
168
+ fontSize: '14px',
169
+ textAlign: 'center',
170
+ marginTop: '10px',
171
+ },
172
+ recentChatsContainer: {
173
+ textAlign: 'center',
174
+ color: 'var(--foreground-muted)',
175
+ fontSize: '16px',
176
+ padding: '20px',
177
+ },
178
+ };
frontend/components/Sidebar.js ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { usePathname } from "next/navigation";
3
+ import { Bars3Icon, Cog6ToothIcon, ArrowRightStartOnRectangleIcon, MagnifyingGlassIcon, ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/20/solid";
4
+ import Link from "next/link";
5
+ import { useEffect, useState, useRef } from "react";
6
+ import { useRouter } from "next/navigation";
7
+ import Image from "next/image";
8
+ import logo from "@assets/logo.jpg";
9
+
10
+ export default function ChatSidebar() {
11
+ const pathname = usePathname();
12
+ const [isCollapsed, setIsCollapsed] = useState(true);
13
+ const sidebarRef = useRef(null);
14
+ const router = useRouter();
15
+
16
+ const toggleSidebar = () => {
17
+ setIsCollapsed(!isCollapsed);
18
+ };
19
+
20
+ useEffect(() => {
21
+ const handleClickOutside = (event) => {
22
+ if (sidebarRef.current && !sidebarRef.current.contains(event.target)) {
23
+ setIsCollapsed(true);
24
+ }
25
+ };
26
+
27
+ document.addEventListener("mousedown", handleClickOutside);
28
+ return () => document.removeEventListener("mousedown", handleClickOutside);
29
+ }, []);
30
+
31
+ const logoStyle = {
32
+ width: isCollapsed ? "60px" : "150px",
33
+ borderRadius: "50px",
34
+ transition: "width 0.3s ease, height 0.3s ease",
35
+ marginBottom: 0,
36
+ marginTop: "35px",
37
+ };
38
+
39
+ const sidebarStyle = {
40
+ width: "180px",
41
+ left: isCollapsed ? "-180px" : "0",
42
+ backgroundColor: "var(--sidebar-background)",
43
+ color: "var(--foreground)",
44
+ padding: isCollapsed ? "15px" : "20px",
45
+ position: "fixed",
46
+ height: "100vh",
47
+ display: "flex",
48
+ flexDirection: "column",
49
+ borderRight: "1px solid rgba(255, 255, 255, 0.1)",
50
+ transition: "left 0.3s ease, width 0.3s ease, padding 0.3s ease",
51
+ zIndex: 10,
52
+ };
53
+
54
+ const navLinkStyle = {
55
+ display: "flex",
56
+ alignItems: "center",
57
+ gap: "5px",
58
+ padding: "10px 5px",
59
+ transition: "all 0.3s ease",
60
+ color: "var(--foreground)",
61
+ fontSize: "1em",
62
+ };
63
+
64
+ const activeNavLinkStyle = {
65
+ backgroundColor: "var(--hover-accent)",
66
+ borderRadius: "5px",
67
+ animation: "fadeIn .5s ease-in-out, pulse .5s ease-in-out",
68
+ };
69
+
70
+ const textStyle = {
71
+ opacity: isCollapsed ? 0 : 1,
72
+ transition: "opacity 0.3s ease",
73
+ pointerEvents: isCollapsed ? "none" : "auto",
74
+ };
75
+
76
+ const iconStyle = {
77
+ width: "30px",
78
+ height: "30px",
79
+ flexShrink: 0,
80
+ };
81
+
82
+ const toggleButtonStyle = {
83
+ position: "absolute",
84
+ top: "20px",
85
+ right: isCollapsed ? "-50px" : "10px",
86
+ width: "35px",
87
+ height: "35px",
88
+ backgroundColor: "var(--button-background)",
89
+ color: "var(--foreground)",
90
+ border: "none",
91
+ borderRadius: "10px",
92
+ cursor: "pointer",
93
+ display: "flex",
94
+ alignItems: "center",
95
+ justifyContent: "center",
96
+ boxShadow: "0 2px 5px rgba(0, 0, 0, 0.2)",
97
+ transition: "background-color 0.3s ease, right 0.3s ease",
98
+ };
99
+
100
+ const handleLogout = () => {
101
+ const userID = localStorage.getItem('u_id');
102
+ const token = localStorage.getItem('s_tkn');
103
+ NexusAuthApi.logout(userID, token)
104
+ .then(() => {
105
+ clearLocalStorage();
106
+ router.push('/');
107
+ window.location.reload();
108
+ })
109
+ .catch((error) => {
110
+ console.error("Logout failed", error);
111
+ });
112
+ };
113
+
114
+ return (
115
+ <div ref={sidebarRef} className="sidebar-container" style={sidebarStyle}>
116
+ <div className="logo-container" style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
117
+ <Image style={logoStyle} src={logo} alt="Chat App Logo" />
118
+ <button
119
+ style={toggleButtonStyle}
120
+ onClick={() => toggleSidebar(!isCollapsed)}
121
+ >
122
+ <Bars3Icon style={{ transform: `rotate(${isCollapsed ? 0 : 90}deg)`, transition: "transform .3s", width: "20px" }} />
123
+ </button>
124
+ </div>
125
+ <ul className="nav-links" style={{ listStyleType: "none", marginTop: "20px", paddingLeft: "0" }}>
126
+ <li>
127
+ <Link href="/" legacyBehavior>
128
+ <a style={{ ...navLinkStyle, ...(pathname === "/" && activeNavLinkStyle) }}>
129
+ <ChatBubbleOvalLeftEllipsisIcon style={iconStyle} />
130
+ <span style={textStyle}>Chats</span>
131
+ </a>
132
+ </Link>
133
+ </li>
134
+ <li>
135
+ <Link href="/search" legacyBehavior>
136
+ <a style={{ ...navLinkStyle, ...(pathname === "/search" && activeNavLinkStyle) }}>
137
+ <MagnifyingGlassIcon style={iconStyle} />
138
+ <span style={textStyle}>Find</span>
139
+ </a>
140
+ </Link>
141
+ </li>
142
+ <li>
143
+ <Link href="/settings" legacyBehavior>
144
+ <a style={{ ...navLinkStyle, ...(pathname === "/settings" && activeNavLinkStyle) }}>
145
+ <Cog6ToothIcon style={iconStyle} />
146
+ <span style={textStyle}>Settings</span>
147
+ </a>
148
+ </Link>
149
+ </li>
150
+ <li>
151
+ <Link href="#logout" legacyBehavior>
152
+ <a style={{ ...navLinkStyle, ...(pathname === "#logout" && activeNavLinkStyle) }} onClick={handleLogout}>
153
+ <ArrowRightStartOnRectangleIcon style={iconStyle} />
154
+ <span style={textStyle}>Logout</span>
155
+ </a>
156
+ </Link>
157
+ </li>
158
+ </ul>
159
+ </div>
160
+ );
161
+ }
frontend/components/SplashScreen.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import logo from "@assets/logo.jpg";
3
+ import Image from "next/image";
4
+ import { useEffect, useState } from "react";
5
+
6
+ const SplashScreen = () => {
7
+ const [loadingMessage, setLoadingMessage] = useState("Initializing...");
8
+
9
+ useEffect(() => {
10
+ // Optionally update the loading message over time
11
+ const messages = ["Loading...", "Please wait...", "Almost there..."];
12
+ let index = 0;
13
+ const interval = setInterval(() => {
14
+ setLoadingMessage(messages[index]);
15
+ index = (index + 1) % messages.length;
16
+ }, 1500);
17
+
18
+ return () => clearInterval(interval);
19
+ }, []);
20
+
21
+ const splashScreenStyle = {
22
+ display: "flex",
23
+ flexDirection: "column",
24
+ justifyContent: "center",
25
+ alignItems: "center",
26
+ height: "100vh",
27
+ width: "100vw",
28
+ backgroundColor: "var(--background)",
29
+ color: "white",
30
+ fontFamily: "'Poppins', sans-serif",
31
+ animation: "fadeIn 1.5s ease-in-out",
32
+ };
33
+
34
+ const logoStyle = {
35
+ width: "300px",
36
+ height: "300px",
37
+ marginBottom: "1rem",
38
+ borderRadius: "50%",
39
+ animation: "pulse 2s infinite",
40
+ };
41
+
42
+ const messageStyle = {
43
+ fontSize: "1.2rem",
44
+ fontWeight: "400",
45
+ textAlign: "center",
46
+ opacity: 0.8,
47
+ };
48
+
49
+ return (
50
+ <div style={splashScreenStyle}>
51
+ <Image style={logoStyle} src={logo} alt="App Logo" />
52
+ <p style={messageStyle}>{loadingMessage}</p>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export default SplashScreen;
frontend/jsconfig.json CHANGED
@@ -1,7 +1,10 @@
1
  {
2
  "compilerOptions": {
3
  "paths": {
4
- "@/*": ["./*"]
 
 
 
5
  }
6
  }
7
  }
 
1
  {
2
  "compilerOptions": {
3
  "paths": {
4
+ "@/*": ["./*"],
5
+ "@components/*": ["./components/*"],
6
+ "@lib/*": ["./lib/*"],
7
+ "@assets/*": ["./assets/*"],
8
  }
9
  }
10
  }
frontend/lib/Nexus_Auth_API.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+ import { API_BASE_URL, DEBUG } from "./config";
3
+
4
+ function debugLog(message, data = null) {
5
+ if (DEBUG) {
6
+ console.log(`[DEBUG] ${message}`);
7
+ if (data) {
8
+ console.log(data);
9
+ }
10
+ }
11
+ }
12
+
13
+ const NexusAuthApi = {
14
+ async signup(username, password, email = null) {
15
+ debugLog("Attempting signup", { username, email });
16
+
17
+ try {
18
+ // Construct the payload dynamically to exclude `email` if it's null
19
+ const payload = { username, password };
20
+ if (email) {
21
+ payload.email = email;
22
+ }
23
+
24
+ const response = await axios.post(`${API_BASE_URL}/auth/signup`, payload);
25
+ debugLog("Signup successful", response.data);
26
+ return response.data;
27
+ } catch (error) {
28
+ debugLog("Signup failed", error.response?.data || error.message);
29
+ throw error.response?.data || error.message;
30
+ }
31
+ },
32
+
33
+ async login(username, password) {
34
+ debugLog("Attempting login", { username });
35
+ try {
36
+ const response = await axios.post(
37
+ `${API_BASE_URL}/auth/login`,
38
+ { username, password }
39
+ );
40
+ debugLog("Login successful", response.data);
41
+ return response.data;
42
+ } catch (error) {
43
+ debugLog("Login failed", error.response?.data || error.message);
44
+ throw error.response?.data || error.message;
45
+ }
46
+ },
47
+
48
+ async logout(userId, token) {
49
+ debugLog("Attempting logout", { userId });
50
+ try {
51
+ const response = await axios.post(
52
+ `${API_BASE_URL}/auth/logout?user_id=${encodeURIComponent(userId)}&token=${encodeURIComponent(token)}`
53
+ );
54
+ debugLog("Logout successful", response.data);
55
+ return response.data;
56
+ } catch (error) {
57
+ debugLog("Logout failed", error.response?.data || error.message);
58
+ throw error.response?.data || error.message;
59
+ }
60
+ },
61
+
62
+
63
+ async validateToken(userId, token) {
64
+ debugLog("Validating token", { userId, token });
65
+ try {
66
+ const response = await axios.get(
67
+ `${API_BASE_URL}/auth/validate?user_id=${encodeURIComponent(userId)}&token=${encodeURIComponent(token)}`
68
+ );
69
+ debugLog("Token validation successful", response);
70
+ return response;
71
+ } catch (error) {
72
+ debugLog("Token validation failed", error.response?.data || error);
73
+ return error;
74
+ }
75
+ },
76
+
77
+
78
+ async searchUsers(query) {
79
+ debugLog("Searching users", { query });
80
+ try {
81
+ const response = await axios.get(`${API_BASE_URL}/auth/search-users`, {
82
+ params: { query },
83
+ });
84
+ debugLog("User search successful", response.data);
85
+ return response.data;
86
+ } catch (error) {
87
+ debugLog("User search failed", error.response?.data || error.message);
88
+ throw error.response?.data || error.message;
89
+ }
90
+ },
91
+
92
+ async isUsernameAvailable(query) {
93
+ debugLog("Searching users", { query });
94
+ try {
95
+ const response = await axios.get(`${API_BASE_URL}/auth/is-username-available`, {
96
+ params: { query },
97
+ });
98
+ debugLog("User search successful", response.data);
99
+ return response.data;
100
+ } catch (error) {
101
+ debugLog("User search failed", error.response?.data || error.message);
102
+ throw error.response?.data || error.message;
103
+ }
104
+ },
105
+
106
+ async getUserId(username) {
107
+ debugLog("Fetching user ID", { username });
108
+ try {
109
+ const response = await axios.get(`${API_BASE_URL}/auth/get-user-id`, {
110
+ params: { username },
111
+ });
112
+ debugLog("User ID fetch successful", response.data);
113
+ return response.data;
114
+ } catch (error) {
115
+ debugLog("User ID fetch failed", error.response?.data || error.message);
116
+ throw error.response?.data || error.message;
117
+ }
118
+ },
119
+
120
+ async updateOwnData(userId, updateData, token) {
121
+ debugLog("Updating own data", { userId, updateData });
122
+ try {
123
+ const response = await axios.put(`${API_BASE_URL}/auth/user/update`, updateData, {
124
+ params: { user_id: userId, token },
125
+ });
126
+ debugLog("Update own data successful", response.data);
127
+ return response.data;
128
+ } catch (error) {
129
+ debugLog("Update own data failed", error.response?.data || error.message);
130
+ throw error.response?.data || error.message;
131
+ }
132
+ },
133
+
134
+ // Admin API
135
+ async getAllUsers(adminId, token) {
136
+ debugLog("Fetching all users", { adminId });
137
+ try {
138
+ const response = await axios.get(`${API_BASE_URL}/admin/users`, {
139
+ params: { user_id: adminId, token }
140
+ });
141
+ debugLog("Fetch all users successful", response.data);
142
+ return response;
143
+ } catch (error) {
144
+ debugLog("Fetch all users failed", error.response?.data || error.message);
145
+ throw error.response?.data || error.message;
146
+ }
147
+ },
148
+
149
+ async getUser(adminId, token, userId) {
150
+ debugLog("Fetching user details", { adminId, userId });
151
+ try {
152
+ const response = await axios.get(`${API_BASE_URL}/admin/user/${userId}`, {
153
+ params: { admin_id: adminId, token },
154
+ });
155
+ debugLog("Fetch user details successful", response.data);
156
+ return response.data;
157
+ } catch (error) {
158
+ debugLog("Fetch user details failed", error.response?.data || error.message);
159
+ throw error.response?.data || error.message;
160
+ }
161
+ },
162
+
163
+ async updateUser(adminId, token, userId, updateData) {
164
+ debugLog("Updating user", { adminId, userId, updateData });
165
+ try {
166
+ const response = await axios.put(`${API_BASE_URL}/admin/user/${userId}`, updateData, {
167
+ params: { admin_id: adminId, token }
168
+ });
169
+ debugLog("Update user successful", response.data);
170
+ return response.data;
171
+ } catch (error) {
172
+ debugLog("Update user failed", error.response?.data || error.message);
173
+ throw error.response?.data || error.message;
174
+ }
175
+ },
176
+
177
+ async updateAccessLevel(adminId, token, userId, accessLevel) {
178
+ debugLog("Updating access level", { adminId, userId, accessLevel });
179
+ try {
180
+ const response = await axios.put(
181
+ `${API_BASE_URL}/admin/user/${userId}/access-level`,
182
+ { access_level: accessLevel },
183
+ {
184
+ params: { admin_id: adminId, token }
185
+ }
186
+ );
187
+ debugLog("Update access level successful", response.data);
188
+ return response.data;
189
+ } catch (error) {
190
+ debugLog("Update access level failed", error.response?.data || error.message);
191
+ throw error.response?.data || error.message;
192
+ }
193
+ },
194
+ };
195
+
196
+ export default NexusAuthApi;
frontend/lib/ToastContext.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from "react";
4
+ import { toast } from "react-toastify";
5
+
6
+ const ToastContext = createContext();
7
+
8
+ export function ToastProvider({ children }) {
9
+ return (
10
+ <ToastContext.Provider value={toast}>
11
+ {children}
12
+ </ToastContext.Provider>
13
+ );
14
+ }
15
+
16
+ export function useToast() {
17
+ const context = useContext(ToastContext);
18
+ if (!context) {
19
+ throw new Error("useToast must be used within a ToastProvider");
20
+ }
21
+ return context;
22
+ }
frontend/lib/config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export const API_BASE_URL = "https://hans-r-d-auth-nexus.hf.space/v1/api";
2
+ export const DEBUG = true;
3
+ export const VERSION = "v0.0.1 Debug";
4
+ export const AUTH_VERSION = "v0.0.3 Debug";
5
+ export const APP_NAME = "Nexus MS";
6
+ export const APP_DESCRIPTION = "A privacy focused chat application";
frontend/package-lock.json CHANGED
@@ -8,10 +8,31 @@
8
  "name": "nex-com",
9
  "version": "0.1.0",
10
  "dependencies": {
 
 
11
  "env-cmd": "^10.1.0",
12
  "next": "15.1.4",
13
  "react": "^19.0.0",
14
- "react-dom": "^19.0.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
  },
17
  "node_modules/@emnapi/runtime": {
@@ -24,6 +45,15 @@
24
  "tslib": "^2.4.0"
25
  }
26
  },
 
 
 
 
 
 
 
 
 
27
  "node_modules/@img/sharp-darwin-arm64": {
28
  "version": "0.33.5",
29
  "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@@ -385,6 +415,77 @@
385
  "url": "https://opencollective.com/libvips"
386
  }
387
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  "node_modules/@next/env": {
389
  "version": "15.1.4",
390
  "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
@@ -519,6 +620,55 @@
519
  "node": ">= 10"
520
  }
521
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  "node_modules/@swc/counter": {
523
  "version": "0.1.3",
524
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -534,6 +684,191 @@
534
  "tslib": "^2.8.0"
535
  }
536
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  "node_modules/busboy": {
538
  "version": "1.6.0",
539
  "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -545,6 +880,16 @@
545
  "node": ">=10.16.0"
546
  }
547
  },
 
 
 
 
 
 
 
 
 
 
548
  "node_modules/caniuse-lite": {
549
  "version": "1.0.30001692",
550
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
@@ -565,12 +910,59 @@
565
  ],
566
  "license": "CC-BY-4.0"
567
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  "node_modules/client-only": {
569
  "version": "0.0.1",
570
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
571
  "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
572
  "license": "MIT"
573
  },
 
 
 
 
 
 
 
 
 
574
  "node_modules/color": {
575
  "version": "4.2.3",
576
  "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -589,8 +981,8 @@
589
  "version": "2.0.1",
590
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
591
  "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
 
592
  "license": "MIT",
593
- "optional": true,
594
  "dependencies": {
595
  "color-name": "~1.1.4"
596
  },
@@ -602,8 +994,8 @@
602
  "version": "1.1.4",
603
  "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
604
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
605
- "license": "MIT",
606
- "optional": true
607
  },
608
  "node_modules/color-string": {
609
  "version": "1.9.1",
@@ -616,6 +1008,18 @@
616
  "simple-swizzle": "^0.2.2"
617
  }
618
  },
 
 
 
 
 
 
 
 
 
 
 
 
619
  "node_modules/commander": {
620
  "version": "4.1.1",
621
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -639,6 +1043,28 @@
639
  "node": ">= 8"
640
  }
641
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  "node_modules/detect-libc": {
643
  "version": "2.0.3",
644
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -649,6 +1075,41 @@
649
  "node": ">=8"
650
  }
651
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  "node_modules/env-cmd": {
653
  "version": "10.1.0",
654
  "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz",
@@ -665,50 +1126,458 @@
665
  "node": ">=8.0.0"
666
  }
667
  },
668
- "node_modules/is-arrayish": {
669
- "version": "0.3.2",
670
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
671
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
 
672
  "license": "MIT",
673
- "optional": true
674
- },
675
- "node_modules/isexe": {
676
- "version": "2.0.0",
677
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
678
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
679
- "license": "ISC"
680
  },
681
- "node_modules/nanoid": {
682
- "version": "3.3.8",
683
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
684
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
685
- "funding": [
686
- {
687
- "type": "github",
688
- "url": "https://github.com/sponsors/ai"
689
- }
690
- ],
691
  "license": "MIT",
692
- "bin": {
693
- "nanoid": "bin/nanoid.cjs"
 
 
 
 
694
  },
695
  "engines": {
696
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
697
  }
698
  },
699
- "node_modules/next": {
700
- "version": "15.1.4",
701
- "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
702
- "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
703
- "license": "MIT",
 
704
  "dependencies": {
705
- "@next/env": "15.1.4",
706
- "@swc/counter": "0.1.3",
707
- "@swc/helpers": "0.5.15",
708
- "busboy": "1.6.0",
709
- "caniuse-lite": "^1.0.30001579",
710
- "postcss": "8.4.31",
711
- "styled-jsx": "5.1.6"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  },
713
  "bin": {
714
  "next": "dist/bin/next"
@@ -750,6 +1619,88 @@
750
  }
751
  }
752
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  "node_modules/path-key": {
754
  "version": "3.1.1",
755
  "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -759,25 +1710,182 @@
759
  "node": ">=8"
760
  }
761
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  "node_modules/picocolors": {
763
  "version": "1.1.1",
764
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
765
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
766
  "license": "ISC"
767
  },
768
- "node_modules/postcss": {
769
- "version": "8.4.31",
770
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
771
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  "funding": [
773
  {
774
  "type": "opencollective",
775
  "url": "https://opencollective.com/postcss/"
776
  },
777
- {
778
- "type": "tidelift",
779
- "url": "https://tidelift.com/funding/github/npm/postcss"
780
- },
781
  {
782
  "type": "github",
783
  "url": "https://github.com/sponsors/ai"
@@ -785,14 +1893,63 @@
785
  ],
786
  "license": "MIT",
787
  "dependencies": {
788
- "nanoid": "^3.3.6",
789
- "picocolors": "^1.0.0",
790
- "source-map-js": "^1.0.2"
791
  },
792
  "engines": {
793
- "node": "^10 || ^12 || >=14"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  }
795
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  "node_modules/react": {
797
  "version": "19.0.0",
798
  "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@@ -814,6 +1971,98 @@
814
  "react": "^19.0.0"
815
  }
816
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
817
  "node_modules/scheduler": {
818
  "version": "0.25.0",
819
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -894,6 +2143,19 @@
894
  "node": ">=8"
895
  }
896
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  "node_modules/simple-swizzle": {
898
  "version": "0.2.2",
899
  "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -921,6 +2183,110 @@
921
  "node": ">=10.0.0"
922
  }
923
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
924
  "node_modules/styled-jsx": {
925
  "version": "5.1.6",
926
  "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -944,12 +2310,167 @@
944
  }
945
  }
946
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  "node_modules/tslib": {
948
  "version": "2.8.1",
949
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
950
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
951
  "license": "0BSD"
952
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953
  "node_modules/which": {
954
  "version": "2.0.2",
955
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -964,6 +2485,117 @@
964
  "engines": {
965
  "node": ">= 8"
966
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  }
968
  }
969
  }
 
8
  "name": "nex-com",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@heroicons/react": "^2.2.0",
12
+ "axios": "^1.7.9",
13
  "env-cmd": "^10.1.0",
14
  "next": "15.1.4",
15
  "react": "^19.0.0",
16
+ "react-dom": "^19.0.0",
17
+ "react-toastify": "^11.0.3"
18
+ },
19
+ "devDependencies": {
20
+ "autoprefixer": "^10.4.20",
21
+ "postcss": "^8.5.1",
22
+ "tailwindcss": "^3.4.17"
23
+ }
24
+ },
25
+ "node_modules/@alloc/quick-lru": {
26
+ "version": "5.2.0",
27
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
28
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=10"
33
+ },
34
+ "funding": {
35
+ "url": "https://github.com/sponsors/sindresorhus"
36
  }
37
  },
38
  "node_modules/@emnapi/runtime": {
 
45
  "tslib": "^2.4.0"
46
  }
47
  },
48
+ "node_modules/@heroicons/react": {
49
+ "version": "2.2.0",
50
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
51
+ "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
52
+ "license": "MIT",
53
+ "peerDependencies": {
54
+ "react": ">= 16 || ^19.0.0-rc"
55
+ }
56
+ },
57
  "node_modules/@img/sharp-darwin-arm64": {
58
  "version": "0.33.5",
59
  "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
 
415
  "url": "https://opencollective.com/libvips"
416
  }
417
  },
418
+ "node_modules/@isaacs/cliui": {
419
+ "version": "8.0.2",
420
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
421
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
422
+ "dev": true,
423
+ "license": "ISC",
424
+ "dependencies": {
425
+ "string-width": "^5.1.2",
426
+ "string-width-cjs": "npm:string-width@^4.2.0",
427
+ "strip-ansi": "^7.0.1",
428
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
429
+ "wrap-ansi": "^8.1.0",
430
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
431
+ },
432
+ "engines": {
433
+ "node": ">=12"
434
+ }
435
+ },
436
+ "node_modules/@jridgewell/gen-mapping": {
437
+ "version": "0.3.8",
438
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
439
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
440
+ "dev": true,
441
+ "license": "MIT",
442
+ "dependencies": {
443
+ "@jridgewell/set-array": "^1.2.1",
444
+ "@jridgewell/sourcemap-codec": "^1.4.10",
445
+ "@jridgewell/trace-mapping": "^0.3.24"
446
+ },
447
+ "engines": {
448
+ "node": ">=6.0.0"
449
+ }
450
+ },
451
+ "node_modules/@jridgewell/resolve-uri": {
452
+ "version": "3.1.2",
453
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
454
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
455
+ "dev": true,
456
+ "license": "MIT",
457
+ "engines": {
458
+ "node": ">=6.0.0"
459
+ }
460
+ },
461
+ "node_modules/@jridgewell/set-array": {
462
+ "version": "1.2.1",
463
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
464
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
465
+ "dev": true,
466
+ "license": "MIT",
467
+ "engines": {
468
+ "node": ">=6.0.0"
469
+ }
470
+ },
471
+ "node_modules/@jridgewell/sourcemap-codec": {
472
+ "version": "1.5.0",
473
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
474
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
475
+ "dev": true,
476
+ "license": "MIT"
477
+ },
478
+ "node_modules/@jridgewell/trace-mapping": {
479
+ "version": "0.3.25",
480
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
481
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
482
+ "dev": true,
483
+ "license": "MIT",
484
+ "dependencies": {
485
+ "@jridgewell/resolve-uri": "^3.1.0",
486
+ "@jridgewell/sourcemap-codec": "^1.4.14"
487
+ }
488
+ },
489
  "node_modules/@next/env": {
490
  "version": "15.1.4",
491
  "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
 
620
  "node": ">= 10"
621
  }
622
  },
623
+ "node_modules/@nodelib/fs.scandir": {
624
+ "version": "2.1.5",
625
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
626
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
627
+ "dev": true,
628
+ "license": "MIT",
629
+ "dependencies": {
630
+ "@nodelib/fs.stat": "2.0.5",
631
+ "run-parallel": "^1.1.9"
632
+ },
633
+ "engines": {
634
+ "node": ">= 8"
635
+ }
636
+ },
637
+ "node_modules/@nodelib/fs.stat": {
638
+ "version": "2.0.5",
639
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
640
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
641
+ "dev": true,
642
+ "license": "MIT",
643
+ "engines": {
644
+ "node": ">= 8"
645
+ }
646
+ },
647
+ "node_modules/@nodelib/fs.walk": {
648
+ "version": "1.2.8",
649
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
650
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
651
+ "dev": true,
652
+ "license": "MIT",
653
+ "dependencies": {
654
+ "@nodelib/fs.scandir": "2.1.5",
655
+ "fastq": "^1.6.0"
656
+ },
657
+ "engines": {
658
+ "node": ">= 8"
659
+ }
660
+ },
661
+ "node_modules/@pkgjs/parseargs": {
662
+ "version": "0.11.0",
663
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
664
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
665
+ "dev": true,
666
+ "license": "MIT",
667
+ "optional": true,
668
+ "engines": {
669
+ "node": ">=14"
670
+ }
671
+ },
672
  "node_modules/@swc/counter": {
673
  "version": "0.1.3",
674
  "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
 
684
  "tslib": "^2.8.0"
685
  }
686
  },
687
+ "node_modules/ansi-regex": {
688
+ "version": "6.1.0",
689
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
690
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
691
+ "dev": true,
692
+ "license": "MIT",
693
+ "engines": {
694
+ "node": ">=12"
695
+ },
696
+ "funding": {
697
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
698
+ }
699
+ },
700
+ "node_modules/ansi-styles": {
701
+ "version": "6.2.1",
702
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
703
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
704
+ "dev": true,
705
+ "license": "MIT",
706
+ "engines": {
707
+ "node": ">=12"
708
+ },
709
+ "funding": {
710
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
711
+ }
712
+ },
713
+ "node_modules/any-promise": {
714
+ "version": "1.3.0",
715
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
716
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
717
+ "dev": true,
718
+ "license": "MIT"
719
+ },
720
+ "node_modules/anymatch": {
721
+ "version": "3.1.3",
722
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
723
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
724
+ "dev": true,
725
+ "license": "ISC",
726
+ "dependencies": {
727
+ "normalize-path": "^3.0.0",
728
+ "picomatch": "^2.0.4"
729
+ },
730
+ "engines": {
731
+ "node": ">= 8"
732
+ }
733
+ },
734
+ "node_modules/arg": {
735
+ "version": "5.0.2",
736
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
737
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
738
+ "dev": true,
739
+ "license": "MIT"
740
+ },
741
+ "node_modules/asynckit": {
742
+ "version": "0.4.0",
743
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
744
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
745
+ "license": "MIT"
746
+ },
747
+ "node_modules/autoprefixer": {
748
+ "version": "10.4.20",
749
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
750
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
751
+ "dev": true,
752
+ "funding": [
753
+ {
754
+ "type": "opencollective",
755
+ "url": "https://opencollective.com/postcss/"
756
+ },
757
+ {
758
+ "type": "tidelift",
759
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
760
+ },
761
+ {
762
+ "type": "github",
763
+ "url": "https://github.com/sponsors/ai"
764
+ }
765
+ ],
766
+ "license": "MIT",
767
+ "dependencies": {
768
+ "browserslist": "^4.23.3",
769
+ "caniuse-lite": "^1.0.30001646",
770
+ "fraction.js": "^4.3.7",
771
+ "normalize-range": "^0.1.2",
772
+ "picocolors": "^1.0.1",
773
+ "postcss-value-parser": "^4.2.0"
774
+ },
775
+ "bin": {
776
+ "autoprefixer": "bin/autoprefixer"
777
+ },
778
+ "engines": {
779
+ "node": "^10 || ^12 || >=14"
780
+ },
781
+ "peerDependencies": {
782
+ "postcss": "^8.1.0"
783
+ }
784
+ },
785
+ "node_modules/axios": {
786
+ "version": "1.7.9",
787
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
788
+ "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
789
+ "license": "MIT",
790
+ "dependencies": {
791
+ "follow-redirects": "^1.15.6",
792
+ "form-data": "^4.0.0",
793
+ "proxy-from-env": "^1.1.0"
794
+ }
795
+ },
796
+ "node_modules/balanced-match": {
797
+ "version": "1.0.2",
798
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
799
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
800
+ "dev": true,
801
+ "license": "MIT"
802
+ },
803
+ "node_modules/binary-extensions": {
804
+ "version": "2.3.0",
805
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
806
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
807
+ "dev": true,
808
+ "license": "MIT",
809
+ "engines": {
810
+ "node": ">=8"
811
+ },
812
+ "funding": {
813
+ "url": "https://github.com/sponsors/sindresorhus"
814
+ }
815
+ },
816
+ "node_modules/brace-expansion": {
817
+ "version": "2.0.1",
818
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
819
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
820
+ "dev": true,
821
+ "license": "MIT",
822
+ "dependencies": {
823
+ "balanced-match": "^1.0.0"
824
+ }
825
+ },
826
+ "node_modules/braces": {
827
+ "version": "3.0.3",
828
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
829
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
830
+ "dev": true,
831
+ "license": "MIT",
832
+ "dependencies": {
833
+ "fill-range": "^7.1.1"
834
+ },
835
+ "engines": {
836
+ "node": ">=8"
837
+ }
838
+ },
839
+ "node_modules/browserslist": {
840
+ "version": "4.24.4",
841
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
842
+ "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
843
+ "dev": true,
844
+ "funding": [
845
+ {
846
+ "type": "opencollective",
847
+ "url": "https://opencollective.com/browserslist"
848
+ },
849
+ {
850
+ "type": "tidelift",
851
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
852
+ },
853
+ {
854
+ "type": "github",
855
+ "url": "https://github.com/sponsors/ai"
856
+ }
857
+ ],
858
+ "license": "MIT",
859
+ "dependencies": {
860
+ "caniuse-lite": "^1.0.30001688",
861
+ "electron-to-chromium": "^1.5.73",
862
+ "node-releases": "^2.0.19",
863
+ "update-browserslist-db": "^1.1.1"
864
+ },
865
+ "bin": {
866
+ "browserslist": "cli.js"
867
+ },
868
+ "engines": {
869
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
870
+ }
871
+ },
872
  "node_modules/busboy": {
873
  "version": "1.6.0",
874
  "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
 
880
  "node": ">=10.16.0"
881
  }
882
  },
883
+ "node_modules/camelcase-css": {
884
+ "version": "2.0.1",
885
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
886
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
887
+ "dev": true,
888
+ "license": "MIT",
889
+ "engines": {
890
+ "node": ">= 6"
891
+ }
892
+ },
893
  "node_modules/caniuse-lite": {
894
  "version": "1.0.30001692",
895
  "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz",
 
910
  ],
911
  "license": "CC-BY-4.0"
912
  },
913
+ "node_modules/chokidar": {
914
+ "version": "3.6.0",
915
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
916
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
917
+ "dev": true,
918
+ "license": "MIT",
919
+ "dependencies": {
920
+ "anymatch": "~3.1.2",
921
+ "braces": "~3.0.2",
922
+ "glob-parent": "~5.1.2",
923
+ "is-binary-path": "~2.1.0",
924
+ "is-glob": "~4.0.1",
925
+ "normalize-path": "~3.0.0",
926
+ "readdirp": "~3.6.0"
927
+ },
928
+ "engines": {
929
+ "node": ">= 8.10.0"
930
+ },
931
+ "funding": {
932
+ "url": "https://paulmillr.com/funding/"
933
+ },
934
+ "optionalDependencies": {
935
+ "fsevents": "~2.3.2"
936
+ }
937
+ },
938
+ "node_modules/chokidar/node_modules/glob-parent": {
939
+ "version": "5.1.2",
940
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
941
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
942
+ "dev": true,
943
+ "license": "ISC",
944
+ "dependencies": {
945
+ "is-glob": "^4.0.1"
946
+ },
947
+ "engines": {
948
+ "node": ">= 6"
949
+ }
950
+ },
951
  "node_modules/client-only": {
952
  "version": "0.0.1",
953
  "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
954
  "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
955
  "license": "MIT"
956
  },
957
+ "node_modules/clsx": {
958
+ "version": "2.1.1",
959
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
960
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
961
+ "license": "MIT",
962
+ "engines": {
963
+ "node": ">=6"
964
+ }
965
+ },
966
  "node_modules/color": {
967
  "version": "4.2.3",
968
  "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
 
981
  "version": "2.0.1",
982
  "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
983
  "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
984
+ "devOptional": true,
985
  "license": "MIT",
 
986
  "dependencies": {
987
  "color-name": "~1.1.4"
988
  },
 
994
  "version": "1.1.4",
995
  "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
996
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
997
+ "devOptional": true,
998
+ "license": "MIT"
999
  },
1000
  "node_modules/color-string": {
1001
  "version": "1.9.1",
 
1008
  "simple-swizzle": "^0.2.2"
1009
  }
1010
  },
1011
+ "node_modules/combined-stream": {
1012
+ "version": "1.0.8",
1013
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1014
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1015
+ "license": "MIT",
1016
+ "dependencies": {
1017
+ "delayed-stream": "~1.0.0"
1018
+ },
1019
+ "engines": {
1020
+ "node": ">= 0.8"
1021
+ }
1022
+ },
1023
  "node_modules/commander": {
1024
  "version": "4.1.1",
1025
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
 
1043
  "node": ">= 8"
1044
  }
1045
  },
1046
+ "node_modules/cssesc": {
1047
+ "version": "3.0.0",
1048
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
1049
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
1050
+ "dev": true,
1051
+ "license": "MIT",
1052
+ "bin": {
1053
+ "cssesc": "bin/cssesc"
1054
+ },
1055
+ "engines": {
1056
+ "node": ">=4"
1057
+ }
1058
+ },
1059
+ "node_modules/delayed-stream": {
1060
+ "version": "1.0.0",
1061
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1062
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1063
+ "license": "MIT",
1064
+ "engines": {
1065
+ "node": ">=0.4.0"
1066
+ }
1067
+ },
1068
  "node_modules/detect-libc": {
1069
  "version": "2.0.3",
1070
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
 
1075
  "node": ">=8"
1076
  }
1077
  },
1078
+ "node_modules/didyoumean": {
1079
+ "version": "1.2.2",
1080
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
1081
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
1082
+ "dev": true,
1083
+ "license": "Apache-2.0"
1084
+ },
1085
+ "node_modules/dlv": {
1086
+ "version": "1.1.3",
1087
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
1088
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
1089
+ "dev": true,
1090
+ "license": "MIT"
1091
+ },
1092
+ "node_modules/eastasianwidth": {
1093
+ "version": "0.2.0",
1094
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
1095
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
1096
+ "dev": true,
1097
+ "license": "MIT"
1098
+ },
1099
+ "node_modules/electron-to-chromium": {
1100
+ "version": "1.5.87",
1101
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz",
1102
+ "integrity": "sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg==",
1103
+ "dev": true,
1104
+ "license": "ISC"
1105
+ },
1106
+ "node_modules/emoji-regex": {
1107
+ "version": "9.2.2",
1108
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
1109
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
1110
+ "dev": true,
1111
+ "license": "MIT"
1112
+ },
1113
  "node_modules/env-cmd": {
1114
  "version": "10.1.0",
1115
  "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz",
 
1126
  "node": ">=8.0.0"
1127
  }
1128
  },
1129
+ "node_modules/escalade": {
1130
+ "version": "3.2.0",
1131
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1132
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1133
+ "dev": true,
1134
  "license": "MIT",
1135
+ "engines": {
1136
+ "node": ">=6"
1137
+ }
 
 
 
 
1138
  },
1139
+ "node_modules/fast-glob": {
1140
+ "version": "3.3.3",
1141
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1142
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1143
+ "dev": true,
 
 
 
 
 
1144
  "license": "MIT",
1145
+ "dependencies": {
1146
+ "@nodelib/fs.stat": "^2.0.2",
1147
+ "@nodelib/fs.walk": "^1.2.3",
1148
+ "glob-parent": "^5.1.2",
1149
+ "merge2": "^1.3.0",
1150
+ "micromatch": "^4.0.8"
1151
  },
1152
  "engines": {
1153
+ "node": ">=8.6.0"
1154
  }
1155
  },
1156
+ "node_modules/fast-glob/node_modules/glob-parent": {
1157
+ "version": "5.1.2",
1158
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
1159
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
1160
+ "dev": true,
1161
+ "license": "ISC",
1162
  "dependencies": {
1163
+ "is-glob": "^4.0.1"
1164
+ },
1165
+ "engines": {
1166
+ "node": ">= 6"
1167
+ }
1168
+ },
1169
+ "node_modules/fastq": {
1170
+ "version": "1.18.0",
1171
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
1172
+ "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
1173
+ "dev": true,
1174
+ "license": "ISC",
1175
+ "dependencies": {
1176
+ "reusify": "^1.0.4"
1177
+ }
1178
+ },
1179
+ "node_modules/fill-range": {
1180
+ "version": "7.1.1",
1181
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
1182
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
1183
+ "dev": true,
1184
+ "license": "MIT",
1185
+ "dependencies": {
1186
+ "to-regex-range": "^5.0.1"
1187
+ },
1188
+ "engines": {
1189
+ "node": ">=8"
1190
+ }
1191
+ },
1192
+ "node_modules/follow-redirects": {
1193
+ "version": "1.15.9",
1194
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
1195
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
1196
+ "funding": [
1197
+ {
1198
+ "type": "individual",
1199
+ "url": "https://github.com/sponsors/RubenVerborgh"
1200
+ }
1201
+ ],
1202
+ "license": "MIT",
1203
+ "engines": {
1204
+ "node": ">=4.0"
1205
+ },
1206
+ "peerDependenciesMeta": {
1207
+ "debug": {
1208
+ "optional": true
1209
+ }
1210
+ }
1211
+ },
1212
+ "node_modules/foreground-child": {
1213
+ "version": "3.3.0",
1214
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
1215
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
1216
+ "dev": true,
1217
+ "license": "ISC",
1218
+ "dependencies": {
1219
+ "cross-spawn": "^7.0.0",
1220
+ "signal-exit": "^4.0.1"
1221
+ },
1222
+ "engines": {
1223
+ "node": ">=14"
1224
+ },
1225
+ "funding": {
1226
+ "url": "https://github.com/sponsors/isaacs"
1227
+ }
1228
+ },
1229
+ "node_modules/form-data": {
1230
+ "version": "4.0.1",
1231
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
1232
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
1233
+ "license": "MIT",
1234
+ "dependencies": {
1235
+ "asynckit": "^0.4.0",
1236
+ "combined-stream": "^1.0.8",
1237
+ "mime-types": "^2.1.12"
1238
+ },
1239
+ "engines": {
1240
+ "node": ">= 6"
1241
+ }
1242
+ },
1243
+ "node_modules/fraction.js": {
1244
+ "version": "4.3.7",
1245
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
1246
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
1247
+ "dev": true,
1248
+ "license": "MIT",
1249
+ "engines": {
1250
+ "node": "*"
1251
+ },
1252
+ "funding": {
1253
+ "type": "patreon",
1254
+ "url": "https://github.com/sponsors/rawify"
1255
+ }
1256
+ },
1257
+ "node_modules/fsevents": {
1258
+ "version": "2.3.3",
1259
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1260
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1261
+ "dev": true,
1262
+ "hasInstallScript": true,
1263
+ "license": "MIT",
1264
+ "optional": true,
1265
+ "os": [
1266
+ "darwin"
1267
+ ],
1268
+ "engines": {
1269
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1270
+ }
1271
+ },
1272
+ "node_modules/function-bind": {
1273
+ "version": "1.1.2",
1274
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1275
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1276
+ "dev": true,
1277
+ "license": "MIT",
1278
+ "funding": {
1279
+ "url": "https://github.com/sponsors/ljharb"
1280
+ }
1281
+ },
1282
+ "node_modules/glob": {
1283
+ "version": "10.4.5",
1284
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
1285
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
1286
+ "dev": true,
1287
+ "license": "ISC",
1288
+ "dependencies": {
1289
+ "foreground-child": "^3.1.0",
1290
+ "jackspeak": "^3.1.2",
1291
+ "minimatch": "^9.0.4",
1292
+ "minipass": "^7.1.2",
1293
+ "package-json-from-dist": "^1.0.0",
1294
+ "path-scurry": "^1.11.1"
1295
+ },
1296
+ "bin": {
1297
+ "glob": "dist/esm/bin.mjs"
1298
+ },
1299
+ "funding": {
1300
+ "url": "https://github.com/sponsors/isaacs"
1301
+ }
1302
+ },
1303
+ "node_modules/glob-parent": {
1304
+ "version": "6.0.2",
1305
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1306
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1307
+ "dev": true,
1308
+ "license": "ISC",
1309
+ "dependencies": {
1310
+ "is-glob": "^4.0.3"
1311
+ },
1312
+ "engines": {
1313
+ "node": ">=10.13.0"
1314
+ }
1315
+ },
1316
+ "node_modules/hasown": {
1317
+ "version": "2.0.2",
1318
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1319
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1320
+ "dev": true,
1321
+ "license": "MIT",
1322
+ "dependencies": {
1323
+ "function-bind": "^1.1.2"
1324
+ },
1325
+ "engines": {
1326
+ "node": ">= 0.4"
1327
+ }
1328
+ },
1329
+ "node_modules/is-arrayish": {
1330
+ "version": "0.3.2",
1331
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
1332
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
1333
+ "license": "MIT",
1334
+ "optional": true
1335
+ },
1336
+ "node_modules/is-binary-path": {
1337
+ "version": "2.1.0",
1338
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1339
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1340
+ "dev": true,
1341
+ "license": "MIT",
1342
+ "dependencies": {
1343
+ "binary-extensions": "^2.0.0"
1344
+ },
1345
+ "engines": {
1346
+ "node": ">=8"
1347
+ }
1348
+ },
1349
+ "node_modules/is-core-module": {
1350
+ "version": "2.16.1",
1351
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
1352
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
1353
+ "dev": true,
1354
+ "license": "MIT",
1355
+ "dependencies": {
1356
+ "hasown": "^2.0.2"
1357
+ },
1358
+ "engines": {
1359
+ "node": ">= 0.4"
1360
+ },
1361
+ "funding": {
1362
+ "url": "https://github.com/sponsors/ljharb"
1363
+ }
1364
+ },
1365
+ "node_modules/is-extglob": {
1366
+ "version": "2.1.1",
1367
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1368
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1369
+ "dev": true,
1370
+ "license": "MIT",
1371
+ "engines": {
1372
+ "node": ">=0.10.0"
1373
+ }
1374
+ },
1375
+ "node_modules/is-fullwidth-code-point": {
1376
+ "version": "3.0.0",
1377
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1378
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1379
+ "dev": true,
1380
+ "license": "MIT",
1381
+ "engines": {
1382
+ "node": ">=8"
1383
+ }
1384
+ },
1385
+ "node_modules/is-glob": {
1386
+ "version": "4.0.3",
1387
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1388
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1389
+ "dev": true,
1390
+ "license": "MIT",
1391
+ "dependencies": {
1392
+ "is-extglob": "^2.1.1"
1393
+ },
1394
+ "engines": {
1395
+ "node": ">=0.10.0"
1396
+ }
1397
+ },
1398
+ "node_modules/is-number": {
1399
+ "version": "7.0.0",
1400
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1401
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1402
+ "dev": true,
1403
+ "license": "MIT",
1404
+ "engines": {
1405
+ "node": ">=0.12.0"
1406
+ }
1407
+ },
1408
+ "node_modules/isexe": {
1409
+ "version": "2.0.0",
1410
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1411
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1412
+ "license": "ISC"
1413
+ },
1414
+ "node_modules/jackspeak": {
1415
+ "version": "3.4.3",
1416
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
1417
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
1418
+ "dev": true,
1419
+ "license": "BlueOak-1.0.0",
1420
+ "dependencies": {
1421
+ "@isaacs/cliui": "^8.0.2"
1422
+ },
1423
+ "funding": {
1424
+ "url": "https://github.com/sponsors/isaacs"
1425
+ },
1426
+ "optionalDependencies": {
1427
+ "@pkgjs/parseargs": "^0.11.0"
1428
+ }
1429
+ },
1430
+ "node_modules/jiti": {
1431
+ "version": "1.21.7",
1432
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1433
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1434
+ "dev": true,
1435
+ "license": "MIT",
1436
+ "bin": {
1437
+ "jiti": "bin/jiti.js"
1438
+ }
1439
+ },
1440
+ "node_modules/lilconfig": {
1441
+ "version": "3.1.3",
1442
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
1443
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
1444
+ "dev": true,
1445
+ "license": "MIT",
1446
+ "engines": {
1447
+ "node": ">=14"
1448
+ },
1449
+ "funding": {
1450
+ "url": "https://github.com/sponsors/antonk52"
1451
+ }
1452
+ },
1453
+ "node_modules/lines-and-columns": {
1454
+ "version": "1.2.4",
1455
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
1456
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
1457
+ "dev": true,
1458
+ "license": "MIT"
1459
+ },
1460
+ "node_modules/lru-cache": {
1461
+ "version": "10.4.3",
1462
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
1463
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
1464
+ "dev": true,
1465
+ "license": "ISC"
1466
+ },
1467
+ "node_modules/merge2": {
1468
+ "version": "1.4.1",
1469
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
1470
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
1471
+ "dev": true,
1472
+ "license": "MIT",
1473
+ "engines": {
1474
+ "node": ">= 8"
1475
+ }
1476
+ },
1477
+ "node_modules/micromatch": {
1478
+ "version": "4.0.8",
1479
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
1480
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1481
+ "dev": true,
1482
+ "license": "MIT",
1483
+ "dependencies": {
1484
+ "braces": "^3.0.3",
1485
+ "picomatch": "^2.3.1"
1486
+ },
1487
+ "engines": {
1488
+ "node": ">=8.6"
1489
+ }
1490
+ },
1491
+ "node_modules/mime-db": {
1492
+ "version": "1.52.0",
1493
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1494
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1495
+ "license": "MIT",
1496
+ "engines": {
1497
+ "node": ">= 0.6"
1498
+ }
1499
+ },
1500
+ "node_modules/mime-types": {
1501
+ "version": "2.1.35",
1502
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1503
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1504
+ "license": "MIT",
1505
+ "dependencies": {
1506
+ "mime-db": "1.52.0"
1507
+ },
1508
+ "engines": {
1509
+ "node": ">= 0.6"
1510
+ }
1511
+ },
1512
+ "node_modules/minimatch": {
1513
+ "version": "9.0.5",
1514
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
1515
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1516
+ "dev": true,
1517
+ "license": "ISC",
1518
+ "dependencies": {
1519
+ "brace-expansion": "^2.0.1"
1520
+ },
1521
+ "engines": {
1522
+ "node": ">=16 || 14 >=14.17"
1523
+ },
1524
+ "funding": {
1525
+ "url": "https://github.com/sponsors/isaacs"
1526
+ }
1527
+ },
1528
+ "node_modules/minipass": {
1529
+ "version": "7.1.2",
1530
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
1531
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1532
+ "dev": true,
1533
+ "license": "ISC",
1534
+ "engines": {
1535
+ "node": ">=16 || 14 >=14.17"
1536
+ }
1537
+ },
1538
+ "node_modules/mz": {
1539
+ "version": "2.7.0",
1540
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
1541
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
1542
+ "dev": true,
1543
+ "license": "MIT",
1544
+ "dependencies": {
1545
+ "any-promise": "^1.0.0",
1546
+ "object-assign": "^4.0.1",
1547
+ "thenify-all": "^1.0.0"
1548
+ }
1549
+ },
1550
+ "node_modules/nanoid": {
1551
+ "version": "3.3.8",
1552
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
1553
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
1554
+ "funding": [
1555
+ {
1556
+ "type": "github",
1557
+ "url": "https://github.com/sponsors/ai"
1558
+ }
1559
+ ],
1560
+ "license": "MIT",
1561
+ "bin": {
1562
+ "nanoid": "bin/nanoid.cjs"
1563
+ },
1564
+ "engines": {
1565
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1566
+ }
1567
+ },
1568
+ "node_modules/next": {
1569
+ "version": "15.1.4",
1570
+ "resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
1571
+ "integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
1572
+ "license": "MIT",
1573
+ "dependencies": {
1574
+ "@next/env": "15.1.4",
1575
+ "@swc/counter": "0.1.3",
1576
+ "@swc/helpers": "0.5.15",
1577
+ "busboy": "1.6.0",
1578
+ "caniuse-lite": "^1.0.30001579",
1579
+ "postcss": "8.4.31",
1580
+ "styled-jsx": "5.1.6"
1581
  },
1582
  "bin": {
1583
  "next": "dist/bin/next"
 
1619
  }
1620
  }
1621
  },
1622
+ "node_modules/next/node_modules/postcss": {
1623
+ "version": "8.4.31",
1624
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1625
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1626
+ "funding": [
1627
+ {
1628
+ "type": "opencollective",
1629
+ "url": "https://opencollective.com/postcss/"
1630
+ },
1631
+ {
1632
+ "type": "tidelift",
1633
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1634
+ },
1635
+ {
1636
+ "type": "github",
1637
+ "url": "https://github.com/sponsors/ai"
1638
+ }
1639
+ ],
1640
+ "license": "MIT",
1641
+ "dependencies": {
1642
+ "nanoid": "^3.3.6",
1643
+ "picocolors": "^1.0.0",
1644
+ "source-map-js": "^1.0.2"
1645
+ },
1646
+ "engines": {
1647
+ "node": "^10 || ^12 || >=14"
1648
+ }
1649
+ },
1650
+ "node_modules/node-releases": {
1651
+ "version": "2.0.19",
1652
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
1653
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
1654
+ "dev": true,
1655
+ "license": "MIT"
1656
+ },
1657
+ "node_modules/normalize-path": {
1658
+ "version": "3.0.0",
1659
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1660
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1661
+ "dev": true,
1662
+ "license": "MIT",
1663
+ "engines": {
1664
+ "node": ">=0.10.0"
1665
+ }
1666
+ },
1667
+ "node_modules/normalize-range": {
1668
+ "version": "0.1.2",
1669
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
1670
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
1671
+ "dev": true,
1672
+ "license": "MIT",
1673
+ "engines": {
1674
+ "node": ">=0.10.0"
1675
+ }
1676
+ },
1677
+ "node_modules/object-assign": {
1678
+ "version": "4.1.1",
1679
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1680
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1681
+ "dev": true,
1682
+ "license": "MIT",
1683
+ "engines": {
1684
+ "node": ">=0.10.0"
1685
+ }
1686
+ },
1687
+ "node_modules/object-hash": {
1688
+ "version": "3.0.0",
1689
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1690
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1691
+ "dev": true,
1692
+ "license": "MIT",
1693
+ "engines": {
1694
+ "node": ">= 6"
1695
+ }
1696
+ },
1697
+ "node_modules/package-json-from-dist": {
1698
+ "version": "1.0.1",
1699
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
1700
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
1701
+ "dev": true,
1702
+ "license": "BlueOak-1.0.0"
1703
+ },
1704
  "node_modules/path-key": {
1705
  "version": "3.1.1",
1706
  "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
 
1710
  "node": ">=8"
1711
  }
1712
  },
1713
+ "node_modules/path-parse": {
1714
+ "version": "1.0.7",
1715
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1716
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1717
+ "dev": true,
1718
+ "license": "MIT"
1719
+ },
1720
+ "node_modules/path-scurry": {
1721
+ "version": "1.11.1",
1722
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
1723
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
1724
+ "dev": true,
1725
+ "license": "BlueOak-1.0.0",
1726
+ "dependencies": {
1727
+ "lru-cache": "^10.2.0",
1728
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
1729
+ },
1730
+ "engines": {
1731
+ "node": ">=16 || 14 >=14.18"
1732
+ },
1733
+ "funding": {
1734
+ "url": "https://github.com/sponsors/isaacs"
1735
+ }
1736
+ },
1737
  "node_modules/picocolors": {
1738
  "version": "1.1.1",
1739
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1740
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1741
  "license": "ISC"
1742
  },
1743
+ "node_modules/picomatch": {
1744
+ "version": "2.3.1",
1745
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1746
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1747
+ "dev": true,
1748
+ "license": "MIT",
1749
+ "engines": {
1750
+ "node": ">=8.6"
1751
+ },
1752
+ "funding": {
1753
+ "url": "https://github.com/sponsors/jonschlinkert"
1754
+ }
1755
+ },
1756
+ "node_modules/pify": {
1757
+ "version": "2.3.0",
1758
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1759
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1760
+ "dev": true,
1761
+ "license": "MIT",
1762
+ "engines": {
1763
+ "node": ">=0.10.0"
1764
+ }
1765
+ },
1766
+ "node_modules/pirates": {
1767
+ "version": "4.0.6",
1768
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
1769
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
1770
+ "dev": true,
1771
+ "license": "MIT",
1772
+ "engines": {
1773
+ "node": ">= 6"
1774
+ }
1775
+ },
1776
+ "node_modules/postcss": {
1777
+ "version": "8.5.1",
1778
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
1779
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
1780
+ "dev": true,
1781
+ "funding": [
1782
+ {
1783
+ "type": "opencollective",
1784
+ "url": "https://opencollective.com/postcss/"
1785
+ },
1786
+ {
1787
+ "type": "tidelift",
1788
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1789
+ },
1790
+ {
1791
+ "type": "github",
1792
+ "url": "https://github.com/sponsors/ai"
1793
+ }
1794
+ ],
1795
+ "license": "MIT",
1796
+ "dependencies": {
1797
+ "nanoid": "^3.3.8",
1798
+ "picocolors": "^1.1.1",
1799
+ "source-map-js": "^1.2.1"
1800
+ },
1801
+ "engines": {
1802
+ "node": "^10 || ^12 || >=14"
1803
+ }
1804
+ },
1805
+ "node_modules/postcss-import": {
1806
+ "version": "15.1.0",
1807
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1808
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1809
+ "dev": true,
1810
+ "license": "MIT",
1811
+ "dependencies": {
1812
+ "postcss-value-parser": "^4.0.0",
1813
+ "read-cache": "^1.0.0",
1814
+ "resolve": "^1.1.7"
1815
+ },
1816
+ "engines": {
1817
+ "node": ">=14.0.0"
1818
+ },
1819
+ "peerDependencies": {
1820
+ "postcss": "^8.0.0"
1821
+ }
1822
+ },
1823
+ "node_modules/postcss-js": {
1824
+ "version": "4.0.1",
1825
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
1826
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
1827
+ "dev": true,
1828
+ "license": "MIT",
1829
+ "dependencies": {
1830
+ "camelcase-css": "^2.0.1"
1831
+ },
1832
+ "engines": {
1833
+ "node": "^12 || ^14 || >= 16"
1834
+ },
1835
+ "funding": {
1836
+ "type": "opencollective",
1837
+ "url": "https://opencollective.com/postcss/"
1838
+ },
1839
+ "peerDependencies": {
1840
+ "postcss": "^8.4.21"
1841
+ }
1842
+ },
1843
+ "node_modules/postcss-load-config": {
1844
+ "version": "4.0.2",
1845
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
1846
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
1847
+ "dev": true,
1848
+ "funding": [
1849
+ {
1850
+ "type": "opencollective",
1851
+ "url": "https://opencollective.com/postcss/"
1852
+ },
1853
+ {
1854
+ "type": "github",
1855
+ "url": "https://github.com/sponsors/ai"
1856
+ }
1857
+ ],
1858
+ "license": "MIT",
1859
+ "dependencies": {
1860
+ "lilconfig": "^3.0.0",
1861
+ "yaml": "^2.3.4"
1862
+ },
1863
+ "engines": {
1864
+ "node": ">= 14"
1865
+ },
1866
+ "peerDependencies": {
1867
+ "postcss": ">=8.0.9",
1868
+ "ts-node": ">=9.0.0"
1869
+ },
1870
+ "peerDependenciesMeta": {
1871
+ "postcss": {
1872
+ "optional": true
1873
+ },
1874
+ "ts-node": {
1875
+ "optional": true
1876
+ }
1877
+ }
1878
+ },
1879
+ "node_modules/postcss-nested": {
1880
+ "version": "6.2.0",
1881
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1882
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1883
+ "dev": true,
1884
  "funding": [
1885
  {
1886
  "type": "opencollective",
1887
  "url": "https://opencollective.com/postcss/"
1888
  },
 
 
 
 
1889
  {
1890
  "type": "github",
1891
  "url": "https://github.com/sponsors/ai"
 
1893
  ],
1894
  "license": "MIT",
1895
  "dependencies": {
1896
+ "postcss-selector-parser": "^6.1.1"
 
 
1897
  },
1898
  "engines": {
1899
+ "node": ">=12.0"
1900
+ },
1901
+ "peerDependencies": {
1902
+ "postcss": "^8.2.14"
1903
+ }
1904
+ },
1905
+ "node_modules/postcss-selector-parser": {
1906
+ "version": "6.1.2",
1907
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1908
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1909
+ "dev": true,
1910
+ "license": "MIT",
1911
+ "dependencies": {
1912
+ "cssesc": "^3.0.0",
1913
+ "util-deprecate": "^1.0.2"
1914
+ },
1915
+ "engines": {
1916
+ "node": ">=4"
1917
  }
1918
  },
1919
+ "node_modules/postcss-value-parser": {
1920
+ "version": "4.2.0",
1921
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1922
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1923
+ "dev": true,
1924
+ "license": "MIT"
1925
+ },
1926
+ "node_modules/proxy-from-env": {
1927
+ "version": "1.1.0",
1928
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1929
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1930
+ "license": "MIT"
1931
+ },
1932
+ "node_modules/queue-microtask": {
1933
+ "version": "1.2.3",
1934
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1935
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1936
+ "dev": true,
1937
+ "funding": [
1938
+ {
1939
+ "type": "github",
1940
+ "url": "https://github.com/sponsors/feross"
1941
+ },
1942
+ {
1943
+ "type": "patreon",
1944
+ "url": "https://www.patreon.com/feross"
1945
+ },
1946
+ {
1947
+ "type": "consulting",
1948
+ "url": "https://feross.org/support"
1949
+ }
1950
+ ],
1951
+ "license": "MIT"
1952
+ },
1953
  "node_modules/react": {
1954
  "version": "19.0.0",
1955
  "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
 
1971
  "react": "^19.0.0"
1972
  }
1973
  },
1974
+ "node_modules/react-toastify": {
1975
+ "version": "11.0.3",
1976
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
1977
+ "integrity": "sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==",
1978
+ "license": "MIT",
1979
+ "dependencies": {
1980
+ "clsx": "^2.1.1"
1981
+ },
1982
+ "peerDependencies": {
1983
+ "react": "^18 || ^19",
1984
+ "react-dom": "^18 || ^19"
1985
+ }
1986
+ },
1987
+ "node_modules/read-cache": {
1988
+ "version": "1.0.0",
1989
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1990
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1991
+ "dev": true,
1992
+ "license": "MIT",
1993
+ "dependencies": {
1994
+ "pify": "^2.3.0"
1995
+ }
1996
+ },
1997
+ "node_modules/readdirp": {
1998
+ "version": "3.6.0",
1999
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
2000
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
2001
+ "dev": true,
2002
+ "license": "MIT",
2003
+ "dependencies": {
2004
+ "picomatch": "^2.2.1"
2005
+ },
2006
+ "engines": {
2007
+ "node": ">=8.10.0"
2008
+ }
2009
+ },
2010
+ "node_modules/resolve": {
2011
+ "version": "1.22.10",
2012
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
2013
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
2014
+ "dev": true,
2015
+ "license": "MIT",
2016
+ "dependencies": {
2017
+ "is-core-module": "^2.16.0",
2018
+ "path-parse": "^1.0.7",
2019
+ "supports-preserve-symlinks-flag": "^1.0.0"
2020
+ },
2021
+ "bin": {
2022
+ "resolve": "bin/resolve"
2023
+ },
2024
+ "engines": {
2025
+ "node": ">= 0.4"
2026
+ },
2027
+ "funding": {
2028
+ "url": "https://github.com/sponsors/ljharb"
2029
+ }
2030
+ },
2031
+ "node_modules/reusify": {
2032
+ "version": "1.0.4",
2033
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
2034
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
2035
+ "dev": true,
2036
+ "license": "MIT",
2037
+ "engines": {
2038
+ "iojs": ">=1.0.0",
2039
+ "node": ">=0.10.0"
2040
+ }
2041
+ },
2042
+ "node_modules/run-parallel": {
2043
+ "version": "1.2.0",
2044
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
2045
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
2046
+ "dev": true,
2047
+ "funding": [
2048
+ {
2049
+ "type": "github",
2050
+ "url": "https://github.com/sponsors/feross"
2051
+ },
2052
+ {
2053
+ "type": "patreon",
2054
+ "url": "https://www.patreon.com/feross"
2055
+ },
2056
+ {
2057
+ "type": "consulting",
2058
+ "url": "https://feross.org/support"
2059
+ }
2060
+ ],
2061
+ "license": "MIT",
2062
+ "dependencies": {
2063
+ "queue-microtask": "^1.2.2"
2064
+ }
2065
+ },
2066
  "node_modules/scheduler": {
2067
  "version": "0.25.0",
2068
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
 
2143
  "node": ">=8"
2144
  }
2145
  },
2146
+ "node_modules/signal-exit": {
2147
+ "version": "4.1.0",
2148
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
2149
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
2150
+ "dev": true,
2151
+ "license": "ISC",
2152
+ "engines": {
2153
+ "node": ">=14"
2154
+ },
2155
+ "funding": {
2156
+ "url": "https://github.com/sponsors/isaacs"
2157
+ }
2158
+ },
2159
  "node_modules/simple-swizzle": {
2160
  "version": "0.2.2",
2161
  "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
 
2183
  "node": ">=10.0.0"
2184
  }
2185
  },
2186
+ "node_modules/string-width": {
2187
+ "version": "5.1.2",
2188
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
2189
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
2190
+ "dev": true,
2191
+ "license": "MIT",
2192
+ "dependencies": {
2193
+ "eastasianwidth": "^0.2.0",
2194
+ "emoji-regex": "^9.2.2",
2195
+ "strip-ansi": "^7.0.1"
2196
+ },
2197
+ "engines": {
2198
+ "node": ">=12"
2199
+ },
2200
+ "funding": {
2201
+ "url": "https://github.com/sponsors/sindresorhus"
2202
+ }
2203
+ },
2204
+ "node_modules/string-width-cjs": {
2205
+ "name": "string-width",
2206
+ "version": "4.2.3",
2207
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2208
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2209
+ "dev": true,
2210
+ "license": "MIT",
2211
+ "dependencies": {
2212
+ "emoji-regex": "^8.0.0",
2213
+ "is-fullwidth-code-point": "^3.0.0",
2214
+ "strip-ansi": "^6.0.1"
2215
+ },
2216
+ "engines": {
2217
+ "node": ">=8"
2218
+ }
2219
+ },
2220
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
2221
+ "version": "5.0.1",
2222
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2223
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2224
+ "dev": true,
2225
+ "license": "MIT",
2226
+ "engines": {
2227
+ "node": ">=8"
2228
+ }
2229
+ },
2230
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
2231
+ "version": "8.0.0",
2232
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2233
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2234
+ "dev": true,
2235
+ "license": "MIT"
2236
+ },
2237
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
2238
+ "version": "6.0.1",
2239
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2240
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2241
+ "dev": true,
2242
+ "license": "MIT",
2243
+ "dependencies": {
2244
+ "ansi-regex": "^5.0.1"
2245
+ },
2246
+ "engines": {
2247
+ "node": ">=8"
2248
+ }
2249
+ },
2250
+ "node_modules/strip-ansi": {
2251
+ "version": "7.1.0",
2252
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
2253
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
2254
+ "dev": true,
2255
+ "license": "MIT",
2256
+ "dependencies": {
2257
+ "ansi-regex": "^6.0.1"
2258
+ },
2259
+ "engines": {
2260
+ "node": ">=12"
2261
+ },
2262
+ "funding": {
2263
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
2264
+ }
2265
+ },
2266
+ "node_modules/strip-ansi-cjs": {
2267
+ "name": "strip-ansi",
2268
+ "version": "6.0.1",
2269
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2270
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2271
+ "dev": true,
2272
+ "license": "MIT",
2273
+ "dependencies": {
2274
+ "ansi-regex": "^5.0.1"
2275
+ },
2276
+ "engines": {
2277
+ "node": ">=8"
2278
+ }
2279
+ },
2280
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
2281
+ "version": "5.0.1",
2282
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2283
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2284
+ "dev": true,
2285
+ "license": "MIT",
2286
+ "engines": {
2287
+ "node": ">=8"
2288
+ }
2289
+ },
2290
  "node_modules/styled-jsx": {
2291
  "version": "5.1.6",
2292
  "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
 
2310
  }
2311
  }
2312
  },
2313
+ "node_modules/sucrase": {
2314
+ "version": "3.35.0",
2315
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
2316
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
2317
+ "dev": true,
2318
+ "license": "MIT",
2319
+ "dependencies": {
2320
+ "@jridgewell/gen-mapping": "^0.3.2",
2321
+ "commander": "^4.0.0",
2322
+ "glob": "^10.3.10",
2323
+ "lines-and-columns": "^1.1.6",
2324
+ "mz": "^2.7.0",
2325
+ "pirates": "^4.0.1",
2326
+ "ts-interface-checker": "^0.1.9"
2327
+ },
2328
+ "bin": {
2329
+ "sucrase": "bin/sucrase",
2330
+ "sucrase-node": "bin/sucrase-node"
2331
+ },
2332
+ "engines": {
2333
+ "node": ">=16 || 14 >=14.17"
2334
+ }
2335
+ },
2336
+ "node_modules/supports-preserve-symlinks-flag": {
2337
+ "version": "1.0.0",
2338
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
2339
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
2340
+ "dev": true,
2341
+ "license": "MIT",
2342
+ "engines": {
2343
+ "node": ">= 0.4"
2344
+ },
2345
+ "funding": {
2346
+ "url": "https://github.com/sponsors/ljharb"
2347
+ }
2348
+ },
2349
+ "node_modules/tailwindcss": {
2350
+ "version": "3.4.17",
2351
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
2352
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
2353
+ "dev": true,
2354
+ "license": "MIT",
2355
+ "dependencies": {
2356
+ "@alloc/quick-lru": "^5.2.0",
2357
+ "arg": "^5.0.2",
2358
+ "chokidar": "^3.6.0",
2359
+ "didyoumean": "^1.2.2",
2360
+ "dlv": "^1.1.3",
2361
+ "fast-glob": "^3.3.2",
2362
+ "glob-parent": "^6.0.2",
2363
+ "is-glob": "^4.0.3",
2364
+ "jiti": "^1.21.6",
2365
+ "lilconfig": "^3.1.3",
2366
+ "micromatch": "^4.0.8",
2367
+ "normalize-path": "^3.0.0",
2368
+ "object-hash": "^3.0.0",
2369
+ "picocolors": "^1.1.1",
2370
+ "postcss": "^8.4.47",
2371
+ "postcss-import": "^15.1.0",
2372
+ "postcss-js": "^4.0.1",
2373
+ "postcss-load-config": "^4.0.2",
2374
+ "postcss-nested": "^6.2.0",
2375
+ "postcss-selector-parser": "^6.1.2",
2376
+ "resolve": "^1.22.8",
2377
+ "sucrase": "^3.35.0"
2378
+ },
2379
+ "bin": {
2380
+ "tailwind": "lib/cli.js",
2381
+ "tailwindcss": "lib/cli.js"
2382
+ },
2383
+ "engines": {
2384
+ "node": ">=14.0.0"
2385
+ }
2386
+ },
2387
+ "node_modules/thenify": {
2388
+ "version": "3.3.1",
2389
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
2390
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
2391
+ "dev": true,
2392
+ "license": "MIT",
2393
+ "dependencies": {
2394
+ "any-promise": "^1.0.0"
2395
+ }
2396
+ },
2397
+ "node_modules/thenify-all": {
2398
+ "version": "1.6.0",
2399
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
2400
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
2401
+ "dev": true,
2402
+ "license": "MIT",
2403
+ "dependencies": {
2404
+ "thenify": ">= 3.1.0 < 4"
2405
+ },
2406
+ "engines": {
2407
+ "node": ">=0.8"
2408
+ }
2409
+ },
2410
+ "node_modules/to-regex-range": {
2411
+ "version": "5.0.1",
2412
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
2413
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2414
+ "dev": true,
2415
+ "license": "MIT",
2416
+ "dependencies": {
2417
+ "is-number": "^7.0.0"
2418
+ },
2419
+ "engines": {
2420
+ "node": ">=8.0"
2421
+ }
2422
+ },
2423
+ "node_modules/ts-interface-checker": {
2424
+ "version": "0.1.13",
2425
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
2426
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
2427
+ "dev": true,
2428
+ "license": "Apache-2.0"
2429
+ },
2430
  "node_modules/tslib": {
2431
  "version": "2.8.1",
2432
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2433
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2434
  "license": "0BSD"
2435
  },
2436
+ "node_modules/update-browserslist-db": {
2437
+ "version": "1.1.2",
2438
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
2439
+ "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
2440
+ "dev": true,
2441
+ "funding": [
2442
+ {
2443
+ "type": "opencollective",
2444
+ "url": "https://opencollective.com/browserslist"
2445
+ },
2446
+ {
2447
+ "type": "tidelift",
2448
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2449
+ },
2450
+ {
2451
+ "type": "github",
2452
+ "url": "https://github.com/sponsors/ai"
2453
+ }
2454
+ ],
2455
+ "license": "MIT",
2456
+ "dependencies": {
2457
+ "escalade": "^3.2.0",
2458
+ "picocolors": "^1.1.1"
2459
+ },
2460
+ "bin": {
2461
+ "update-browserslist-db": "cli.js"
2462
+ },
2463
+ "peerDependencies": {
2464
+ "browserslist": ">= 4.21.0"
2465
+ }
2466
+ },
2467
+ "node_modules/util-deprecate": {
2468
+ "version": "1.0.2",
2469
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2470
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2471
+ "dev": true,
2472
+ "license": "MIT"
2473
+ },
2474
  "node_modules/which": {
2475
  "version": "2.0.2",
2476
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
2485
  "engines": {
2486
  "node": ">= 8"
2487
  }
2488
+ },
2489
+ "node_modules/wrap-ansi": {
2490
+ "version": "8.1.0",
2491
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
2492
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
2493
+ "dev": true,
2494
+ "license": "MIT",
2495
+ "dependencies": {
2496
+ "ansi-styles": "^6.1.0",
2497
+ "string-width": "^5.0.1",
2498
+ "strip-ansi": "^7.0.1"
2499
+ },
2500
+ "engines": {
2501
+ "node": ">=12"
2502
+ },
2503
+ "funding": {
2504
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2505
+ }
2506
+ },
2507
+ "node_modules/wrap-ansi-cjs": {
2508
+ "name": "wrap-ansi",
2509
+ "version": "7.0.0",
2510
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
2511
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
2512
+ "dev": true,
2513
+ "license": "MIT",
2514
+ "dependencies": {
2515
+ "ansi-styles": "^4.0.0",
2516
+ "string-width": "^4.1.0",
2517
+ "strip-ansi": "^6.0.0"
2518
+ },
2519
+ "engines": {
2520
+ "node": ">=10"
2521
+ },
2522
+ "funding": {
2523
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2524
+ }
2525
+ },
2526
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
2527
+ "version": "5.0.1",
2528
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2529
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2530
+ "dev": true,
2531
+ "license": "MIT",
2532
+ "engines": {
2533
+ "node": ">=8"
2534
+ }
2535
+ },
2536
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
2537
+ "version": "4.3.0",
2538
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
2539
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
2540
+ "dev": true,
2541
+ "license": "MIT",
2542
+ "dependencies": {
2543
+ "color-convert": "^2.0.1"
2544
+ },
2545
+ "engines": {
2546
+ "node": ">=8"
2547
+ },
2548
+ "funding": {
2549
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2550
+ }
2551
+ },
2552
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
2553
+ "version": "8.0.0",
2554
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2555
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2556
+ "dev": true,
2557
+ "license": "MIT"
2558
+ },
2559
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
2560
+ "version": "4.2.3",
2561
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2562
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2563
+ "dev": true,
2564
+ "license": "MIT",
2565
+ "dependencies": {
2566
+ "emoji-regex": "^8.0.0",
2567
+ "is-fullwidth-code-point": "^3.0.0",
2568
+ "strip-ansi": "^6.0.1"
2569
+ },
2570
+ "engines": {
2571
+ "node": ">=8"
2572
+ }
2573
+ },
2574
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
2575
+ "version": "6.0.1",
2576
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2577
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2578
+ "dev": true,
2579
+ "license": "MIT",
2580
+ "dependencies": {
2581
+ "ansi-regex": "^5.0.1"
2582
+ },
2583
+ "engines": {
2584
+ "node": ">=8"
2585
+ }
2586
+ },
2587
+ "node_modules/yaml": {
2588
+ "version": "2.7.0",
2589
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
2590
+ "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
2591
+ "dev": true,
2592
+ "license": "ISC",
2593
+ "bin": {
2594
+ "yaml": "bin.mjs"
2595
+ },
2596
+ "engines": {
2597
+ "node": ">= 14"
2598
+ }
2599
  }
2600
  }
2601
  }
frontend/package.json CHANGED
@@ -9,9 +9,17 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
 
 
12
  "env-cmd": "^10.1.0",
13
  "next": "15.1.4",
14
  "react": "^19.0.0",
15
- "react-dom": "^19.0.0"
 
 
 
 
 
 
16
  }
17
  }
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "@heroicons/react": "^2.2.0",
13
+ "axios": "^1.7.9",
14
  "env-cmd": "^10.1.0",
15
  "next": "15.1.4",
16
  "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "react-toastify": "^11.0.3"
19
+ },
20
+ "devDependencies": {
21
+ "autoprefixer": "^10.4.20",
22
+ "postcss": "^8.5.1",
23
+ "tailwindcss": "^3.4.17"
24
  }
25
  }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
5
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
7
+
8
+ ],
9
+ theme: {
10
+ extend: {},
11
+ },
12
+ plugins: [],
13
+ }