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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.44",
3
+ "version": "1.0.45",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, Toast } from '../components/helper';
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
- try {
39
- const response = await fetch('/api/auth/logout', {
40
- method: 'POST',
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
- if (result.success) {
46
- Toast(result.message || 'Logged out successfully', 'success');
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
- try {
32
- const response = await fetch('/api/auth/login', {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- body: JSON.stringify({ email: form.email, password: form.password })
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