create-nara 1.0.44 → 1.0.45
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
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,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
|
|