create-nara 1.0.44 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of create-nara might be problematic. Click here for more details.
- package/package.json +1 -1
- package/templates/svelte/postcss.config.js +6 -0
- package/templates/svelte/resources/js/components/Header.svelte +6 -16
- package/templates/svelte/resources/js/components/helper.ts +144 -0
- package/templates/svelte/resources/js/index.css +66 -205
- package/templates/svelte/resources/js/pages/auth/login.svelte +9 -57
- package/templates/svelte/tailwind.config.js +122 -0
- package/templates/svelte/vite.config.ts +1 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { fly, fade } from 'svelte/transition';
|
|
3
3
|
import { page, router, inertia } from '@inertiajs/svelte';
|
|
4
|
-
import { clickOutside,
|
|
4
|
+
import { clickOutside, apiFetch } from '../components/helper';
|
|
5
5
|
import DarkModeToggle from '../components/DarkModeToggle.svelte';
|
|
6
6
|
|
|
7
7
|
interface User {
|
|
@@ -35,22 +35,12 @@
|
|
|
35
35
|
$: visibleMenuLinks = menuLinks.filter((item) => item.show);
|
|
36
36
|
|
|
37
37
|
async function handleLogout(): Promise<void> {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
headers: { 'Content-Type': 'application/json' }
|
|
42
|
-
});
|
|
43
|
-
const result = await response.json();
|
|
38
|
+
const result = await apiFetch('/api/auth/logout', {
|
|
39
|
+
method: 'POST'
|
|
40
|
+
});
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
router.visit(result.data?.redirect || '/login');
|
|
48
|
-
} else {
|
|
49
|
-
Toast(result.message || 'Logout failed', 'error');
|
|
50
|
-
}
|
|
51
|
-
} catch (err: unknown) {
|
|
52
|
-
const errorMessage = err instanceof Error ? err.message : 'Network error';
|
|
53
|
-
Toast(errorMessage, 'error');
|
|
42
|
+
if (result.success || result.redirected) {
|
|
43
|
+
router.visit(result.redirectUrl || '/login');
|
|
54
44
|
}
|
|
55
45
|
}
|
|
56
46
|
</script>
|
|
@@ -68,6 +68,18 @@ interface ApiOptions {
|
|
|
68
68
|
showErrorToast?: boolean;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Fetch API response type
|
|
73
|
+
*/
|
|
74
|
+
export interface FetchApiResponse<T = unknown> {
|
|
75
|
+
success: boolean;
|
|
76
|
+
data?: T;
|
|
77
|
+
message: string;
|
|
78
|
+
redirected?: boolean;
|
|
79
|
+
redirectUrl?: string;
|
|
80
|
+
errors?: Record<string, string[]>;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
/**
|
|
72
84
|
* Creates a click outside event listener for a DOM node
|
|
73
85
|
*/
|
|
@@ -252,6 +264,138 @@ export async function api<T = unknown>(
|
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Reusable fetch API helper that handles JSON responses, redirects, and errors
|
|
269
|
+
*
|
|
270
|
+
* @param url - The URL to fetch
|
|
271
|
+
* @param options - Fetch options (method, body, headers, etc.)
|
|
272
|
+
* @param apiOptions - API options for toast notifications
|
|
273
|
+
* @returns FetchApiResponse with success, data, message, and redirect info
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* // POST request with JSON body
|
|
277
|
+
* const result = await apiFetch('/api/auth/logout', { method: 'POST' });
|
|
278
|
+
* if (result.success || result.redirected) {
|
|
279
|
+
* router.visit(result.redirectUrl || '/login');
|
|
280
|
+
* }
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* // POST request with body
|
|
284
|
+
* const result = await apiFetch('/api/auth/login', {
|
|
285
|
+
* method: 'POST',
|
|
286
|
+
* body: JSON.stringify({ email, password })
|
|
287
|
+
* });
|
|
288
|
+
*/
|
|
289
|
+
export async function apiFetch<T = unknown>(
|
|
290
|
+
url: string,
|
|
291
|
+
options: RequestInit = {},
|
|
292
|
+
apiOptions: ApiOptions = {}
|
|
293
|
+
): Promise<FetchApiResponse<T>> {
|
|
294
|
+
const { showSuccessToast = true, showErrorToast = true } = apiOptions;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const response = await fetch(url, {
|
|
298
|
+
...options,
|
|
299
|
+
headers: {
|
|
300
|
+
'Content-Type': 'application/json',
|
|
301
|
+
...options.headers,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Check if the response was a redirect (fetch follows redirects automatically)
|
|
306
|
+
// We detect this by checking if the final URL is different from the requested URL
|
|
307
|
+
// or if the URL matches common redirect targets
|
|
308
|
+
const isRedirected = response.redirected;
|
|
309
|
+
const finalUrl = response.url;
|
|
310
|
+
|
|
311
|
+
// Handle non-JSON responses (redirects, HTML error pages)
|
|
312
|
+
const contentType = response.headers.get('content-type');
|
|
313
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
314
|
+
// If redirected to a success page, treat as success
|
|
315
|
+
if (isRedirected || finalUrl.endsWith('/dashboard') || finalUrl.endsWith('/login')) {
|
|
316
|
+
const successMsg = finalUrl.includes('login') ? 'Logged out successfully' : 'Success';
|
|
317
|
+
if (showSuccessToast) {
|
|
318
|
+
Toast(successMsg, 'success');
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
message: successMsg,
|
|
323
|
+
redirected: true,
|
|
324
|
+
redirectUrl: finalUrl,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Handle error status codes
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
const errorMsg = getHttpErrorMessage(response.status, response.statusText);
|
|
331
|
+
if (showErrorToast) {
|
|
332
|
+
Toast(errorMsg, 'error');
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
success: false,
|
|
336
|
+
message: errorMsg,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Unknown non-JSON response
|
|
341
|
+
return {
|
|
342
|
+
success: true,
|
|
343
|
+
message: 'Success',
|
|
344
|
+
redirected: isRedirected,
|
|
345
|
+
redirectUrl: finalUrl,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Parse JSON response
|
|
350
|
+
const result = await response.json() as ApiResponse<T>;
|
|
351
|
+
|
|
352
|
+
if (result.success) {
|
|
353
|
+
if (showSuccessToast && result.message) {
|
|
354
|
+
Toast(result.message, 'success');
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
message: result.message,
|
|
359
|
+
data: result.data,
|
|
360
|
+
redirected: isRedirected,
|
|
361
|
+
redirectUrl: finalUrl,
|
|
362
|
+
};
|
|
363
|
+
} else {
|
|
364
|
+
if (showErrorToast) {
|
|
365
|
+
const errorMsg = result.errors
|
|
366
|
+
? formatValidationErrors(result.errors) || result.message
|
|
367
|
+
: result.message;
|
|
368
|
+
if (errorMsg) Toast(errorMsg, 'error');
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
message: result.message,
|
|
373
|
+
errors: result.errors,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
} catch (err: unknown) {
|
|
377
|
+
// Network or parsing error
|
|
378
|
+
let errorMessage: string;
|
|
379
|
+
|
|
380
|
+
if (err instanceof SyntaxError) {
|
|
381
|
+
errorMessage = 'Server returned an invalid response. Please try again.';
|
|
382
|
+
} else if (err instanceof TypeError && (err.message.includes('fetch') || err.message.includes('network'))) {
|
|
383
|
+
errorMessage = 'Unable to connect to server. Please check your connection.';
|
|
384
|
+
} else {
|
|
385
|
+
errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (showErrorToast) {
|
|
389
|
+
Toast(errorMessage, 'error');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
message: errorMessage,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
255
399
|
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
256
400
|
|
|
257
401
|
/**
|
|
@@ -1,233 +1,94 @@
|
|
|
1
1
|
@import url('https://rsms.me/inter/inter.css');
|
|
2
2
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap');
|
|
3
|
-
@
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@theme {
|
|
7
|
-
--color-primary-50: #ecfdf5;
|
|
8
|
-
--color-primary-100: #d1fae5;
|
|
9
|
-
--color-primary-200: #a7f3d0;
|
|
10
|
-
--color-primary-300: #6ee7b7;
|
|
11
|
-
--color-primary-400: #34d399;
|
|
12
|
-
--color-primary-500: #10b981;
|
|
13
|
-
--color-primary-600: #059669;
|
|
14
|
-
--color-primary-700: #047857;
|
|
15
|
-
--color-primary-800: #065f46;
|
|
16
|
-
--color-primary-900: #064e3b;
|
|
17
|
-
--color-primary-950: #022c22;
|
|
18
|
-
|
|
19
|
-
--color-secondary-50: #fffbeb;
|
|
20
|
-
--color-secondary-100: #fef3c7;
|
|
21
|
-
--color-secondary-200: #fde68a;
|
|
22
|
-
--color-secondary-300: #fcd34d;
|
|
23
|
-
--color-secondary-400: #fbbf24;
|
|
24
|
-
--color-secondary-500: #f59e0b;
|
|
25
|
-
--color-secondary-600: #d97706;
|
|
26
|
-
--color-secondary-700: #b45309;
|
|
27
|
-
--color-secondary-800: #92400e;
|
|
28
|
-
--color-secondary-900: #78350f;
|
|
29
|
-
--color-secondary-950: #451a03;
|
|
30
|
-
|
|
31
|
-
--color-accent-50: #faf5ff;
|
|
32
|
-
--color-accent-100: #f3e8ff;
|
|
33
|
-
--color-accent-200: #e9d5ff;
|
|
34
|
-
--color-accent-300: #d8b4fe;
|
|
35
|
-
--color-accent-400: #c084fc;
|
|
36
|
-
--color-accent-500: #a855f7;
|
|
37
|
-
--color-accent-600: #9333ea;
|
|
38
|
-
--color-accent-700: #7c3aed;
|
|
39
|
-
--color-accent-800: #6b21a8;
|
|
40
|
-
--color-accent-900: #581c87;
|
|
41
|
-
--color-accent-950: #3b0764;
|
|
42
|
-
|
|
43
|
-
--color-info-50: #ecfeff;
|
|
44
|
-
--color-info-100: #cffafe;
|
|
45
|
-
--color-info-200: #a5f3fc;
|
|
46
|
-
--color-info-300: #67e8f9;
|
|
47
|
-
--color-info-400: #22d3ee;
|
|
48
|
-
--color-info-500: #06b6d4;
|
|
49
|
-
--color-info-600: #0891b2;
|
|
50
|
-
--color-info-700: #0e7490;
|
|
51
|
-
--color-info-800: #155e75;
|
|
52
|
-
--color-info-900: #164e63;
|
|
53
|
-
--color-info-950: #083344;
|
|
54
|
-
|
|
55
|
-
--color-warning-50: #fff7ed;
|
|
56
|
-
--color-warning-100: #ffedd5;
|
|
57
|
-
--color-warning-200: #fed7aa;
|
|
58
|
-
--color-warning-300: #fdba74;
|
|
59
|
-
--color-warning-400: #fb923c;
|
|
60
|
-
--color-warning-500: #f97316;
|
|
61
|
-
--color-warning-600: #ea580c;
|
|
62
|
-
--color-warning-700: #c2410c;
|
|
63
|
-
--color-warning-800: #9a3412;
|
|
64
|
-
--color-warning-900: #7c2d12;
|
|
65
|
-
--color-warning-950: #431407;
|
|
66
|
-
|
|
67
|
-
--color-danger-50: #fef2f2;
|
|
68
|
-
--color-danger-100: #fee2e2;
|
|
69
|
-
--color-danger-200: #fecaca;
|
|
70
|
-
--color-danger-300: #fca5a5;
|
|
71
|
-
--color-danger-400: #f87171;
|
|
72
|
-
--color-danger-500: #ef4444;
|
|
73
|
-
--color-danger-600: #dc2626;
|
|
74
|
-
--color-danger-700: #b91c1c;
|
|
75
|
-
--color-danger-800: #991b1b;
|
|
76
|
-
--color-danger-900: #7f1d1d;
|
|
77
|
-
--color-danger-950: #450a0a;
|
|
78
|
-
|
|
79
|
-
--color-success-50: #ecfdf5;
|
|
80
|
-
--color-success-100: #d1fae5;
|
|
81
|
-
--color-success-200: #a7f3d0;
|
|
82
|
-
--color-success-300: #6ee7b7;
|
|
83
|
-
--color-success-400: #34d399;
|
|
84
|
-
--color-success-500: #10b981;
|
|
85
|
-
--color-success-600: #059669;
|
|
86
|
-
--color-success-700: #047857;
|
|
87
|
-
--color-success-800: #065f46;
|
|
88
|
-
--color-success-900: #064e3b;
|
|
89
|
-
--color-success-950: #022c22;
|
|
90
|
-
|
|
91
|
-
--color-surface-light: #f8f8f8;
|
|
92
|
-
--color-surface-dark: #0a0a0a;
|
|
93
|
-
--color-surface-card-light: #f1f5f9;
|
|
94
|
-
--color-surface-card-dark: #0f0f0f;
|
|
95
|
-
|
|
96
|
-
--font-family-sans: 'Inter var', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
97
|
-
--font-family-serif: 'Playfair Display', Georgia, Cambria, 'Times New Roman', Times, serif;
|
|
98
|
-
|
|
99
|
-
--shadow-soft: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/* Base styles */
|
|
103
|
-
html {
|
|
104
|
-
font-family: var(--font-family-sans);
|
|
105
|
-
}
|
|
3
|
+
@tailwind base;
|
|
4
|
+
@tailwind components;
|
|
5
|
+
@tailwind utilities;
|
|
106
6
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
body::-webkit-scrollbar-track {
|
|
112
|
-
background: transparent;
|
|
113
|
-
}
|
|
7
|
+
@layer base {
|
|
8
|
+
html {
|
|
9
|
+
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
10
|
+
}
|
|
114
11
|
|
|
115
|
-
body::-webkit-scrollbar
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
12
|
+
body::-webkit-scrollbar {
|
|
13
|
+
width: 4px;
|
|
14
|
+
}
|
|
119
15
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
transition-property: all;
|
|
123
|
-
transition-duration: 300ms;
|
|
124
|
-
&:hover {
|
|
125
|
-
box-shadow: var(--shadow-soft);
|
|
126
|
-
transform: translateY(-0.25rem);
|
|
16
|
+
body::-webkit-scrollbar-track {
|
|
17
|
+
background: transparent;
|
|
127
18
|
}
|
|
128
|
-
}
|
|
129
19
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
border-radius: 0.5rem;
|
|
134
|
-
transition-property: color, background-color;
|
|
135
|
-
transition-duration: 300ms;
|
|
136
|
-
&:hover {
|
|
137
|
-
color: rgb(17 24 39);
|
|
138
|
-
background-color: rgb(243 244 246);
|
|
20
|
+
body::-webkit-scrollbar-thumb {
|
|
21
|
+
background-color: theme('colors.primary.500');
|
|
22
|
+
border-radius: 3px;
|
|
139
23
|
}
|
|
140
24
|
}
|
|
141
25
|
|
|
142
|
-
@
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
background-image: linear-gradient(to right, var(--color-primary-600), var(--color-primary-400));
|
|
147
|
-
}
|
|
26
|
+
@layer components {
|
|
27
|
+
.card-hover {
|
|
28
|
+
@apply transition-all duration-300 hover:shadow-soft hover:-translate-y-1;
|
|
29
|
+
}
|
|
148
30
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
color: white;
|
|
152
|
-
padding: 0.5rem 1rem;
|
|
153
|
-
border-radius: 0.5rem;
|
|
154
|
-
transition-property: all;
|
|
155
|
-
transition-duration: 300ms;
|
|
156
|
-
&:hover {
|
|
157
|
-
background-color: var(--color-primary-700);
|
|
31
|
+
.nav-link {
|
|
32
|
+
@apply px-4 py-2 text-gray-600 dark:text-gray-200 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-300;
|
|
158
33
|
}
|
|
159
|
-
|
|
160
|
-
|
|
34
|
+
|
|
35
|
+
.nav-link.active {
|
|
36
|
+
@apply bg-primary-50 dark:bg-gray-800 text-primary-600;
|
|
161
37
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
38
|
+
|
|
39
|
+
.gradient-text {
|
|
40
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary-600 to-primary-400;
|
|
165
41
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
42
|
+
|
|
43
|
+
.mobile-nav-link {
|
|
44
|
+
@apply block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50;
|
|
169
45
|
}
|
|
170
|
-
}
|
|
171
46
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
color: rgb(17 24 39);
|
|
175
|
-
padding: 0.5rem 1rem;
|
|
176
|
-
border-radius: 0.5rem;
|
|
177
|
-
border: 1px solid rgb(209 213 219);
|
|
178
|
-
transition-property: all;
|
|
179
|
-
transition-duration: 300ms;
|
|
180
|
-
&:hover {
|
|
181
|
-
background-color: rgb(249 250 251);
|
|
47
|
+
.mobile-nav-link.active {
|
|
48
|
+
@apply text-primary-600 font-semibold;
|
|
182
49
|
}
|
|
183
|
-
|
|
184
|
-
|
|
50
|
+
|
|
51
|
+
.btn-primary {
|
|
52
|
+
@apply bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700
|
|
53
|
+
transition-all duration-300 active:bg-primary-800
|
|
54
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
55
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
|
|
185
56
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
57
|
+
|
|
58
|
+
.btn-secondary {
|
|
59
|
+
@apply bg-white text-gray-900 px-4 py-2 rounded-lg border border-gray-300
|
|
60
|
+
hover:bg-gray-50 transition-all duration-300 active:bg-gray-100
|
|
61
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
62
|
+
focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2;
|
|
189
63
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
64
|
+
|
|
65
|
+
.btn-danger {
|
|
66
|
+
@apply bg-danger-600 text-white px-4 py-2 rounded-lg hover:bg-danger-700
|
|
67
|
+
transition-all duration-300 active:bg-danger-800
|
|
68
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
69
|
+
focus:outline-none focus:ring-2 focus:ring-danger-500 focus:ring-offset-2;
|
|
193
70
|
}
|
|
194
|
-
}
|
|
195
71
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
padding: 0.5rem 1rem;
|
|
200
|
-
border-radius: 0.5rem;
|
|
201
|
-
transition-property: all;
|
|
202
|
-
transition-duration: 300ms;
|
|
203
|
-
&:hover {
|
|
204
|
-
background-color: var(--color-danger-700);
|
|
72
|
+
.card {
|
|
73
|
+
@apply bg-white rounded-xl shadow-soft p-6 transition-all duration-300
|
|
74
|
+
shadow-lg hover:shadow-xl hover:-translate-y-0.5;
|
|
205
75
|
}
|
|
206
|
-
|
|
207
|
-
|
|
76
|
+
|
|
77
|
+
/* Surface backgrounds */
|
|
78
|
+
.bg-surface {
|
|
79
|
+
@apply bg-surface-light dark:bg-surface-dark;
|
|
208
80
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
81
|
+
|
|
82
|
+
.bg-surface-card {
|
|
83
|
+
@apply bg-surface-card-light dark:bg-surface-card-dark;
|
|
212
84
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
85
|
+
|
|
86
|
+
/* Gradient text utilities */
|
|
87
|
+
.gradient-text-accent {
|
|
88
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-accent-600 to-accent-400;
|
|
216
89
|
}
|
|
217
|
-
}
|
|
218
90
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
border-radius: 0.75rem;
|
|
222
|
-
box-shadow: var(--shadow-soft);
|
|
223
|
-
padding: 1.5rem;
|
|
224
|
-
transition-property: all;
|
|
225
|
-
transition-duration: 300ms;
|
|
226
|
-
&:hover {
|
|
227
|
-
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
228
|
-
transform: translateY(-0.125rem);
|
|
91
|
+
.gradient-text-info {
|
|
92
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-info-600 to-info-400;
|
|
229
93
|
}
|
|
230
94
|
}
|
|
231
|
-
|
|
232
|
-
/* Surface utilities - use standard Tailwind classes instead */
|
|
233
|
-
/* bg-surface-light, bg-surface-dark, bg-surface-card-light, bg-surface-card-dark are available */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import { inertia, router } from '@inertiajs/svelte'
|
|
4
|
-
import { Toast } from '../../components/helper';
|
|
4
|
+
import { apiFetch, Toast } from '../../components/helper';
|
|
5
5
|
import NaraIcon from '../../components/NaraIcon.svelte';
|
|
6
6
|
import { fade, fly } from 'svelte/transition';
|
|
7
7
|
|
|
@@ -28,63 +28,15 @@
|
|
|
28
28
|
|
|
29
29
|
async function submitForm(): Promise<void> {
|
|
30
30
|
loading = true;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Check if login was successful via redirect (backend returns 302 on success)
|
|
39
|
-
// fetch follows redirects automatically, so we check if we ended up at dashboard
|
|
40
|
-
if (response.redirected || response.url.endsWith('/dashboard')) {
|
|
41
|
-
Toast('Login successful', 'success');
|
|
42
|
-
router.visit('/dashboard');
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Handle non-JSON responses (server errors returning HTML)
|
|
47
|
-
const contentType = response.headers.get('content-type');
|
|
48
|
-
if (!contentType || !contentType.includes('application/json')) {
|
|
49
|
-
const statusMessages: Record<number, string> = {
|
|
50
|
-
401: 'Invalid email or password.',
|
|
51
|
-
403: 'Access denied.',
|
|
52
|
-
404: 'Login service not available.',
|
|
53
|
-
500: 'Server error. Please try again later.',
|
|
54
|
-
502: 'Server is temporarily unavailable.',
|
|
55
|
-
503: 'Service unavailable. Please try again later.',
|
|
56
|
-
};
|
|
57
|
-
Toast(statusMessages[response.status] || `Server error (${response.status})`, 'error');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const result = await response.json();
|
|
62
|
-
|
|
63
|
-
if (result.success) {
|
|
64
|
-
Toast(result.message || 'Login successful', 'success');
|
|
65
|
-
router.visit(result.data?.redirect || '/dashboard');
|
|
66
|
-
} else {
|
|
67
|
-
// Handle validation errors
|
|
68
|
-
if (result.errors) {
|
|
69
|
-
const errorMessages = Object.values(result.errors).flat() as string[];
|
|
70
|
-
Toast(errorMessages[0] || result.message || 'Invalid email or password.', 'error');
|
|
71
|
-
} else {
|
|
72
|
-
Toast(result.message || 'Invalid email or password.', 'error');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} catch (err: unknown) {
|
|
76
|
-
// Network or parsing error
|
|
77
|
-
if (err instanceof SyntaxError) {
|
|
78
|
-
Toast('Server returned an invalid response. Please try again.', 'error');
|
|
79
|
-
} else if (err instanceof TypeError && (err.message.includes('fetch') || err.message.includes('network'))) {
|
|
80
|
-
Toast('Unable to connect to server. Please check your connection.', 'error');
|
|
81
|
-
} else {
|
|
82
|
-
const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
|
|
83
|
-
Toast(errorMessage, 'error');
|
|
84
|
-
}
|
|
85
|
-
} finally {
|
|
86
|
-
loading = false;
|
|
31
|
+
const result = await apiFetch('/api/auth/login', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
body: JSON.stringify({ email: form.email, password: form.password })
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.success || result.redirected) {
|
|
37
|
+
router.visit(result.redirectUrl || '/dashboard');
|
|
87
38
|
}
|
|
39
|
+
loading = false;
|
|
88
40
|
}
|
|
89
41
|
</script>
|
|
90
42
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
"./resources/**/*.{svelte,html,js,ts}",
|
|
5
|
+
],
|
|
6
|
+
darkMode: 'class',
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
fontFamily: {
|
|
10
|
+
sans: ['Inter var', 'Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
|
|
11
|
+
serif: ['Playfair Display', 'Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'],
|
|
12
|
+
},
|
|
13
|
+
colors: {
|
|
14
|
+
primary: {
|
|
15
|
+
50: '#ecfdf5',
|
|
16
|
+
100: '#d1fae5',
|
|
17
|
+
200: '#a7f3d0',
|
|
18
|
+
300: '#6ee7b7',
|
|
19
|
+
400: '#34d399',
|
|
20
|
+
500: '#10b981',
|
|
21
|
+
600: '#059669',
|
|
22
|
+
700: '#047857',
|
|
23
|
+
800: '#065f46',
|
|
24
|
+
900: '#064e3b',
|
|
25
|
+
950: '#022c22',
|
|
26
|
+
},
|
|
27
|
+
secondary: {
|
|
28
|
+
50: '#fffbeb',
|
|
29
|
+
100: '#fef3c7',
|
|
30
|
+
200: '#fde68a',
|
|
31
|
+
300: '#fcd34d',
|
|
32
|
+
400: '#fbbf24',
|
|
33
|
+
500: '#f59e0b',
|
|
34
|
+
600: '#d97706',
|
|
35
|
+
700: '#b45309',
|
|
36
|
+
800: '#92400e',
|
|
37
|
+
900: '#78350f',
|
|
38
|
+
950: '#451a03',
|
|
39
|
+
},
|
|
40
|
+
accent: {
|
|
41
|
+
50: '#faf5ff',
|
|
42
|
+
100: '#f3e8ff',
|
|
43
|
+
200: '#e9d5ff',
|
|
44
|
+
300: '#d8b4fe',
|
|
45
|
+
400: '#c084fc',
|
|
46
|
+
500: '#a855f7',
|
|
47
|
+
600: '#9333ea',
|
|
48
|
+
700: '#7c3aed',
|
|
49
|
+
800: '#6b21a8',
|
|
50
|
+
900: '#581c87',
|
|
51
|
+
950: '#3b0764',
|
|
52
|
+
},
|
|
53
|
+
info: {
|
|
54
|
+
50: '#ecfeff',
|
|
55
|
+
100: '#cffafe',
|
|
56
|
+
200: '#a5f3fc',
|
|
57
|
+
300: '#67e8f9',
|
|
58
|
+
400: '#22d3ee',
|
|
59
|
+
500: '#06b6d4',
|
|
60
|
+
600: '#0891b2',
|
|
61
|
+
700: '#0e7490',
|
|
62
|
+
800: '#155e75',
|
|
63
|
+
900: '#164e63',
|
|
64
|
+
950: '#083344',
|
|
65
|
+
},
|
|
66
|
+
warning: {
|
|
67
|
+
50: '#fff7ed',
|
|
68
|
+
100: '#ffedd5',
|
|
69
|
+
200: '#fed7aa',
|
|
70
|
+
300: '#fdba74',
|
|
71
|
+
400: '#fb923c',
|
|
72
|
+
500: '#f97316',
|
|
73
|
+
600: '#ea580c',
|
|
74
|
+
700: '#c2410c',
|
|
75
|
+
800: '#9a3412',
|
|
76
|
+
900: '#7c2d12',
|
|
77
|
+
950: '#431407',
|
|
78
|
+
},
|
|
79
|
+
danger: {
|
|
80
|
+
50: '#fef2f2',
|
|
81
|
+
100: '#fee2e2',
|
|
82
|
+
200: '#fecaca',
|
|
83
|
+
300: '#fca5a5',
|
|
84
|
+
400: '#f87171',
|
|
85
|
+
500: '#ef4444',
|
|
86
|
+
600: '#dc2626',
|
|
87
|
+
700: '#b91c1c',
|
|
88
|
+
800: '#991b1b',
|
|
89
|
+
900: '#7f1d1d',
|
|
90
|
+
950: '#450a0a',
|
|
91
|
+
},
|
|
92
|
+
success: {
|
|
93
|
+
50: '#ecfdf5',
|
|
94
|
+
100: '#d1fae5',
|
|
95
|
+
200: '#a7f3d0',
|
|
96
|
+
300: '#6ee7b7',
|
|
97
|
+
400: '#34d399',
|
|
98
|
+
500: '#10b981',
|
|
99
|
+
600: '#059669',
|
|
100
|
+
700: '#047857',
|
|
101
|
+
800: '#065f46',
|
|
102
|
+
900: '#064e3b',
|
|
103
|
+
950: '#022c22',
|
|
104
|
+
},
|
|
105
|
+
surface: {
|
|
106
|
+
light: '#f8f8f8',
|
|
107
|
+
dark: '#0a0a0a',
|
|
108
|
+
card: {
|
|
109
|
+
light: '#f1f5f9',
|
|
110
|
+
dark: '#0f0f0f',
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
boxShadow: {
|
|
115
|
+
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
plugins: [
|
|
120
|
+
require('@tailwindcss/typography'),
|
|
121
|
+
],
|
|
122
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { defineConfig, loadEnv } from 'vite';
|
|
2
2
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
-
import tailwindcss from '@tailwindcss/vite';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
|
|
6
5
|
export default defineConfig(({ mode }) => {
|
|
@@ -8,7 +7,7 @@ export default defineConfig(({ mode }) => {
|
|
|
8
7
|
const vitePort = parseInt(env.VITE_PORT || '5173');
|
|
9
8
|
|
|
10
9
|
return {
|
|
11
|
-
plugins: [svelte()
|
|
10
|
+
plugins: [svelte()],
|
|
12
11
|
resolve: {
|
|
13
12
|
alias: {
|
|
14
13
|
'@': path.resolve(__dirname, './resources/js')
|