create-nara 0.1.0

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.
Files changed (50) hide show
  1. package/README.md +17 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +50 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +3 -0
  6. package/dist/template.d.ts +8 -0
  7. package/dist/template.js +68 -0
  8. package/package.json +28 -0
  9. package/templates/base/.env.example +3 -0
  10. package/templates/base/tsconfig.json +14 -0
  11. package/templates/minimal/routes/web.ts +11 -0
  12. package/templates/minimal/server.ts +10 -0
  13. package/templates/svelte/resources/js/app.ts +12 -0
  14. package/templates/svelte/resources/js/components/DarkModeToggle.svelte +67 -0
  15. package/templates/svelte/resources/js/components/Header.svelte +240 -0
  16. package/templates/svelte/resources/js/components/NaraIcon.svelte +3 -0
  17. package/templates/svelte/resources/js/components/Pagination.svelte +55 -0
  18. package/templates/svelte/resources/js/components/UserModal.svelte +234 -0
  19. package/templates/svelte/resources/js/components/helper.ts +300 -0
  20. package/templates/svelte/resources/js/pages/auth/forgot-password.svelte +97 -0
  21. package/templates/svelte/resources/js/pages/auth/login.svelte +138 -0
  22. package/templates/svelte/resources/js/pages/auth/register.svelte +176 -0
  23. package/templates/svelte/resources/js/pages/auth/reset-password.svelte +106 -0
  24. package/templates/svelte/resources/js/pages/dashboard.svelte +224 -0
  25. package/templates/svelte/resources/js/pages/landing.svelte +446 -0
  26. package/templates/svelte/resources/js/pages/profile.svelte +368 -0
  27. package/templates/svelte/resources/js/pages/users.svelte +260 -0
  28. package/templates/svelte/resources/views/inertia.html +12 -0
  29. package/templates/svelte/routes/web.ts +17 -0
  30. package/templates/svelte/server.ts +12 -0
  31. package/templates/svelte/vite.config.ts +19 -0
  32. package/templates/vue/resources/js/app.ts +14 -0
  33. package/templates/vue/resources/js/components/DarkModeToggle.vue +81 -0
  34. package/templates/vue/resources/js/components/Header.vue +251 -0
  35. package/templates/vue/resources/js/components/NaraIcon.vue +5 -0
  36. package/templates/vue/resources/js/components/Pagination.vue +71 -0
  37. package/templates/vue/resources/js/components/UserModal.vue +276 -0
  38. package/templates/vue/resources/js/components/index.ts +5 -0
  39. package/templates/vue/resources/js/pages/auth/forgot-password.vue +105 -0
  40. package/templates/vue/resources/js/pages/auth/login.vue +142 -0
  41. package/templates/vue/resources/js/pages/auth/register.vue +183 -0
  42. package/templates/vue/resources/js/pages/auth/reset-password.vue +115 -0
  43. package/templates/vue/resources/js/pages/dashboard.vue +233 -0
  44. package/templates/vue/resources/js/pages/landing.vue +358 -0
  45. package/templates/vue/resources/js/pages/profile.vue +370 -0
  46. package/templates/vue/resources/js/pages/users.vue +264 -0
  47. package/templates/vue/resources/views/inertia.html +12 -0
  48. package/templates/vue/routes/web.ts +17 -0
  49. package/templates/vue/server.ts +12 -0
  50. package/templates/vue/vite.config.ts +19 -0
@@ -0,0 +1,234 @@
1
+ <script lang="ts">
2
+ import { fly, fade, scale } from 'svelte/transition';
3
+ import { backOut } from 'svelte/easing';
4
+ import type { UserForm } from '../types';
5
+ import { createEventDispatcher } from 'svelte';
6
+
7
+ export let show: boolean = false;
8
+ export let mode: 'create' | 'edit' = 'create';
9
+ export let form: UserForm;
10
+ export let isSubmitting: boolean = false;
11
+
12
+ const dispatch = createEventDispatcher<{
13
+ close: void;
14
+ submit: UserForm;
15
+ }>();
16
+
17
+ function handleClose(): void {
18
+ dispatch('close');
19
+ }
20
+
21
+ function handleSubmit(): void {
22
+ dispatch('submit', form);
23
+ }
24
+
25
+ function handleBackdropClick(e: MouseEvent): void {
26
+ if (e.target === e.currentTarget) {
27
+ handleClose();
28
+ }
29
+ }
30
+ </script>
31
+
32
+ {#if show}
33
+ <!-- Backdrop -->
34
+ <div
35
+ class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6"
36
+ on:click={handleBackdropClick}
37
+ on:keydown={(e) => e.key === 'Escape' && handleClose()}
38
+ role="dialog"
39
+ aria-modal="true"
40
+ aria-labelledby="modal-title"
41
+ tabindex="-1"
42
+ transition:fade={{ duration: 300 }}
43
+ >
44
+ <!-- Background Blur & Overlay -->
45
+ <div class="absolute inset-0 bg-black/70 backdrop-blur-md"></div>
46
+
47
+ <!-- Decorative Elements -->
48
+ <div class="absolute top-1/4 left-1/4 w-96 h-96 bg-accent-500/10 rounded-full blur-3xl pointer-events-none"></div>
49
+ <div class="absolute bottom-1/4 right-1/4 w-64 h-64 bg-primary-500/10 rounded-full blur-3xl pointer-events-none"></div>
50
+
51
+ <!-- Modal Container -->
52
+ <div
53
+ class="relative w-full max-w-lg"
54
+ transition:fly={{ y: 50, duration: 500, easing: backOut }}
55
+ >
56
+ <!-- Glowing Border Effect -->
57
+ <div class="absolute -inset-px bg-gradient-to-br from-accent-500/50 via-transparent to-primary-500/50 rounded-3xl blur-sm opacity-60"></div>
58
+
59
+ <!-- Modal Content -->
60
+ <div class="relative bg-white dark:bg-surface-dark rounded-3xl border border-slate-200 dark:border-white/10 overflow-hidden shadow-2xl">
61
+
62
+ <!-- Header -->
63
+ <div class="relative px-8 pt-8 pb-6 border-b border-slate-100 dark:border-white/5">
64
+ <!-- Decorative Line -->
65
+ <div class="absolute top-0 left-8 right-8 h-px bg-gradient-to-r from-transparent via-accent-500/50 to-transparent"></div>
66
+
67
+ <div class="flex items-start justify-between">
68
+ <div>
69
+ <p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-600 dark:text-accent-400 mb-2">
70
+ {mode === 'create' ? 'New Entry' : 'Edit Entry'}
71
+ </p>
72
+ <h3 id="modal-title" class="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">
73
+ {mode === 'create' ? 'Create User' : 'Update User'}
74
+ </h3>
75
+ </div>
76
+
77
+ <!-- Close Button -->
78
+ <button
79
+ class="group w-10 h-10 flex items-center justify-center rounded-full border border-slate-200 dark:border-white/10 hover:border-red-500/50 hover:bg-red-500/5 transition-all duration-300"
80
+ on:click={handleClose}
81
+ disabled={isSubmitting}
82
+ aria-label="Close modal"
83
+ >
84
+ <svg class="w-4 h-4 text-slate-400 group-hover:text-red-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
85
+ <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
86
+ </svg>
87
+ </button>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Form -->
92
+ <form class="p-8 space-y-6" on:submit|preventDefault={handleSubmit}>
93
+
94
+ <!-- Name Field -->
95
+ <div class="group">
96
+ <label for="user-name" class="block text-[10px] font-bold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400 mb-3">
97
+ Full Name
98
+ </label>
99
+ <div class="relative">
100
+ <input
101
+ id="user-name"
102
+ class="w-full px-0 py-3 bg-transparent border-0 border-b-2 border-slate-200 dark:border-white/10 text-lg text-slate-900 dark:text-white placeholder-slate-300 dark:placeholder-slate-600 focus:border-accent-500 dark:focus:border-accent-400 focus:ring-0 outline-none transition-colors"
103
+ type="text"
104
+ bind:value={form.name}
105
+ placeholder="Enter full name"
106
+ />
107
+ <div class="absolute bottom-0 left-0 w-0 h-0.5 bg-accent-500 group-focus-within:w-full transition-all duration-500"></div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Email Field -->
112
+ <div class="group">
113
+ <label for="user-email" class="block text-[10px] font-bold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400 mb-3">
114
+ Email Address
115
+ </label>
116
+ <div class="relative">
117
+ <input
118
+ id="user-email"
119
+ class="w-full px-0 py-3 bg-transparent border-0 border-b-2 border-slate-200 dark:border-white/10 text-lg text-slate-900 dark:text-white placeholder-slate-300 dark:placeholder-slate-600 focus:border-accent-500 dark:focus:border-accent-400 focus:ring-0 outline-none transition-colors"
120
+ type="email"
121
+ bind:value={form.email}
122
+ placeholder="user@example.com"
123
+ />
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Phone Field -->
128
+ <div class="group">
129
+ <label for="user-phone" class="block text-[10px] font-bold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400 mb-3">
130
+ Phone Number
131
+ <span class="text-slate-400 dark:text-slate-600 font-normal normal-case tracking-normal ml-1">(optional)</span>
132
+ </label>
133
+ <div class="relative">
134
+ <input
135
+ id="user-phone"
136
+ class="w-full px-0 py-3 bg-transparent border-0 border-b-2 border-slate-200 dark:border-white/10 text-lg text-slate-900 dark:text-white placeholder-slate-300 dark:placeholder-slate-600 focus:border-purple-500 dark:focus:border-purple-400 focus:ring-0 outline-none transition-colors font-mono"
137
+ type="text"
138
+ bind:value={form.phone}
139
+ placeholder="+62 xxx xxxx xxxx"
140
+ />
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Role & Status Toggles -->
145
+ <div class="grid grid-cols-2 gap-4 pt-2">
146
+ <!-- Admin Toggle -->
147
+ <label class="group relative flex items-center gap-4 p-4 rounded-2xl border border-slate-200 dark:border-white/10 hover:border-accent-500/30 cursor-pointer transition-all duration-300 {form.is_admin ? 'bg-accent-500/5 border-accent-500/30' : ''}">
148
+ <div class="relative">
149
+ <input
150
+ type="checkbox"
151
+ class="sr-only peer"
152
+ bind:checked={form.is_admin}
153
+ />
154
+ <div class="w-12 h-7 bg-slate-200 dark:bg-white/10 rounded-full peer-checked:bg-accent-500 transition-colors"></div>
155
+ <div class="absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow-md transform peer-checked:translate-x-5 transition-transform"></div>
156
+ </div>
157
+ <div>
158
+ <p class="text-sm font-bold text-slate-900 dark:text-white">Admin</p>
159
+ <p class="text-[10px] text-slate-400 dark:text-slate-500">Full access</p>
160
+ </div>
161
+ </label>
162
+
163
+ <!-- Verified Toggle -->
164
+ <label class="group relative flex items-center gap-4 p-4 rounded-2xl border border-slate-200 dark:border-white/10 hover:border-primary-500/30 cursor-pointer transition-all duration-300 {form.is_verified ? 'bg-primary-500/5 border-primary-500/30' : ''}">
165
+ <div class="relative">
166
+ <input
167
+ type="checkbox"
168
+ class="sr-only peer"
169
+ bind:checked={form.is_verified}
170
+ />
171
+ <div class="w-12 h-7 bg-slate-200 dark:bg-white/10 rounded-full peer-checked:bg-primary-500 transition-colors"></div>
172
+ <div class="absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow-md transform peer-checked:translate-x-5 transition-transform"></div>
173
+ </div>
174
+ <div>
175
+ <p class="text-sm font-bold text-slate-900 dark:text-white">Verified</p>
176
+ <p class="text-[10px] text-slate-400 dark:text-slate-500">Email confirmed</p>
177
+ </div>
178
+ </label>
179
+ </div>
180
+
181
+ <!-- Password Field -->
182
+ <div class="group pt-2">
183
+ <label for="user-password" class="block text-[10px] font-bold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400 mb-3">
184
+ {mode === 'create' ? 'Password' : 'New Password'}
185
+ <span class="text-slate-400 dark:text-slate-600 font-normal normal-case tracking-normal ml-1">(optional)</span>
186
+ </label>
187
+ <div class="relative">
188
+ <input
189
+ id="user-password"
190
+ class="w-full px-0 py-3 bg-transparent border-0 border-b-2 border-slate-200 dark:border-white/10 text-lg text-slate-900 dark:text-white placeholder-slate-300 dark:placeholder-slate-600 focus:border-accent-500 dark:focus:border-accent-400 focus:ring-0 outline-none transition-colors"
191
+ type="password"
192
+ bind:value={form.password}
193
+ placeholder={mode === 'create' ? 'Leave empty to use email' : 'Leave empty to keep current'}
194
+ />
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Actions -->
199
+ <div class="flex items-center justify-end gap-4 pt-6 border-t border-slate-100 dark:border-white/5">
200
+ <button
201
+ type="button"
202
+ class="px-6 py-3 text-xs font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors disabled:opacity-50"
203
+ on:click={handleClose}
204
+ disabled={isSubmitting}
205
+ >
206
+ Cancel
207
+ </button>
208
+ <button
209
+ type="submit"
210
+ class="group relative px-8 py-3 bg-slate-900 dark:bg-white text-white dark:text-black text-xs font-bold uppercase tracking-wider rounded-full overflow-hidden hover:scale-105 active:scale-95 transition-transform disabled:opacity-50 disabled:hover:scale-100"
211
+ disabled={isSubmitting}
212
+ >
213
+ <span class="relative z-10 flex items-center gap-2">
214
+ {#if isSubmitting}
215
+ <svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
216
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
217
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
218
+ </svg>
219
+ Saving...
220
+ {:else}
221
+ {mode === 'create' ? 'Create User' : 'Save Changes'}
222
+ <svg class="w-4 h-4 transform group-hover:translate-x-1 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
223
+ <path d="M5 12h14M12 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round"/>
224
+ </svg>
225
+ {/if}
226
+ </span>
227
+ <div class="absolute inset-0 bg-gradient-to-r from-accent-500 to-primary-500 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
228
+ </button>
229
+ </div>
230
+ </form>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ {/if}
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Svelte action return type
3
+ */
4
+ interface ActionReturn {
5
+ destroy(): void;
6
+ }
7
+
8
+ /**
9
+ * Get CSRF token from cookie
10
+ * Used for protecting state-changing requests (POST, PUT, DELETE)
11
+ *
12
+ * @returns CSRF token string or undefined if not found
13
+ *
14
+ * @example
15
+ * const token = getCSRFToken();
16
+ * fetch('/api/data', {
17
+ * method: 'POST',
18
+ * headers: {
19
+ * 'X-CSRF-Token': token || '',
20
+ * 'Content-Type': 'application/json',
21
+ * },
22
+ * body: JSON.stringify(data),
23
+ * });
24
+ */
25
+ export function getCSRFToken(): string | undefined {
26
+ return document.cookie.match(/csrf_token=([^;]+)/)?.[1];
27
+ }
28
+
29
+ /**
30
+ * Configure axios instance with CSRF token
31
+ * Call this once at app initialization to automatically include CSRF token in all requests
32
+ *
33
+ * @param axiosInstance - Axios instance to configure
34
+ *
35
+ * @example
36
+ * import axios from 'axios';
37
+ * import { configureAxiosCSRF } from '$lib/helper';
38
+ *
39
+ * configureAxiosCSRF(axios);
40
+ */
41
+ export function configureAxiosCSRF(axiosInstance: { interceptors: { request: { use: (fn: (config: any) => any) => void } } }): void {
42
+ axiosInstance.interceptors.request.use((config: any) => {
43
+ const token = getCSRFToken();
44
+ if (token && ['post', 'put', 'patch', 'delete'].includes(config.method?.toLowerCase() || '')) {
45
+ config.headers = config.headers || {};
46
+ config.headers['X-CSRF-Token'] = token;
47
+ }
48
+ return config;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * API response type
54
+ */
55
+ export interface ApiResponse<T = unknown> {
56
+ success: boolean;
57
+ message: string;
58
+ data?: T;
59
+ code?: string;
60
+ errors?: Record<string, string[]>;
61
+ }
62
+
63
+ /**
64
+ * API options type
65
+ */
66
+ interface ApiOptions {
67
+ showSuccessToast?: boolean;
68
+ showErrorToast?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Creates a click outside event listener for a DOM node
73
+ */
74
+ export function clickOutside(node: HTMLElement): ActionReturn {
75
+ const handleClick = (event: MouseEvent): void => {
76
+ if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
77
+ node.dispatchEvent(new CustomEvent('click_outside'));
78
+ }
79
+ };
80
+
81
+ document.addEventListener('click', handleClick, true);
82
+
83
+ return {
84
+ destroy() {
85
+ document.removeEventListener('click', handleClick, true);
86
+ }
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Generates a random password with a mix of letters, numbers and special characters
92
+ */
93
+ export function password_generator(pLength: number): string {
94
+ const keyListAlpha = "abcdefghijklmnopqrstuvwxyz";
95
+ const keyListInt = "123456789";
96
+ const keyListSpec = "!@#_";
97
+ let password = '';
98
+ let len = Math.ceil(pLength / 2);
99
+ len = len - 1;
100
+ const lenSpec = pLength - 2 * len;
101
+
102
+ for (let i = 0; i < len; i++) {
103
+ password += keyListAlpha.charAt(Math.floor(Math.random() * keyListAlpha.length));
104
+ password += keyListInt.charAt(Math.floor(Math.random() * keyListInt.length));
105
+ }
106
+
107
+ for (let i = 0; i < lenSpec; i++) {
108
+ password += keyListSpec.charAt(Math.floor(Math.random() * keyListSpec.length));
109
+ }
110
+
111
+ password = password.split('').sort(() => 0.5 - Math.random()).join('');
112
+
113
+ return password;
114
+ }
115
+
116
+ /**
117
+ * Creates a debounced version of a function that delays its execution
118
+ */
119
+ export function debounce<T extends (...args: unknown[]) => unknown>(
120
+ func: T,
121
+ timeout: number = 300
122
+ ): (...args: Parameters<T>) => void {
123
+ let timer: ReturnType<typeof setTimeout>;
124
+ return (...args: Parameters<T>): void => {
125
+ clearTimeout(timer);
126
+ timer = setTimeout(() => {
127
+ func(...args);
128
+ }, timeout);
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Format validation errors object into a readable string
134
+ */
135
+ function formatValidationErrors(errors: Record<string, string[]> | undefined): string {
136
+ if (!errors || typeof errors !== 'object') return '';
137
+
138
+ const messages: string[] = [];
139
+ for (const [field, fieldErrors] of Object.entries(errors)) {
140
+ if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
141
+ messages.push(`${field}: ${fieldErrors[0]}`);
142
+ }
143
+ }
144
+ return messages.join('; ');
145
+ }
146
+
147
+ /**
148
+ * Standardized API call wrapper that handles JSON responses from backend
149
+ */
150
+ export async function api<T = unknown>(
151
+ axiosCall: () => Promise<{ data: ApiResponse<T> }>,
152
+ options: ApiOptions = {}
153
+ ): Promise<ApiResponse<T>> {
154
+ const { showSuccessToast = true, showErrorToast = true } = options;
155
+
156
+ try {
157
+ const response = await axiosCall();
158
+ const result = response.data;
159
+
160
+ if (result.success) {
161
+ if (showSuccessToast && result.message) {
162
+ Toast(result.message, 'success');
163
+ }
164
+ return { success: true, message: result.message, data: result.data };
165
+ } else {
166
+ if (showErrorToast) {
167
+ const errorMsg = result.errors
168
+ ? formatValidationErrors(result.errors) || result.message
169
+ : result.message;
170
+ if (errorMsg) Toast(errorMsg, 'error');
171
+ }
172
+ return { success: false, message: result.message, code: result.code, errors: result.errors };
173
+ }
174
+ } catch (error: unknown) {
175
+ const axiosError = error as { response?: { data?: ApiResponse } };
176
+ const message = axiosError?.response?.data?.message || 'Terjadi kesalahan, coba lagi';
177
+ const code = axiosError?.response?.data?.code;
178
+ const errors = axiosError?.response?.data?.errors;
179
+
180
+ if (showErrorToast) {
181
+ const errorMsg = errors
182
+ ? formatValidationErrors(errors) || message
183
+ : message;
184
+ Toast(errorMsg, 'error');
185
+ }
186
+
187
+ return { success: false, message, code, errors };
188
+ }
189
+ }
190
+
191
+ type ToastType = 'success' | 'error' | 'warning' | 'info';
192
+
193
+ /**
194
+ * Displays a toast notification message
195
+ */
196
+ export function Toast(text: string, type: ToastType = "success", duration: number = 3000): void {
197
+ // Create toast container if it doesn't exist
198
+ let container = document.getElementById('toast-container');
199
+ if (!container) {
200
+ container = document.createElement('div');
201
+ container.id = 'toast-container';
202
+ container.style.cssText = `
203
+ position: fixed;
204
+ bottom: 24px;
205
+ left: 50%;
206
+ transform: translateX(-50%);
207
+ z-index: 9999;
208
+ display: flex;
209
+ flex-direction: column;
210
+ align-items: center;
211
+ gap: 8px;
212
+ `;
213
+ document.body.appendChild(container);
214
+ }
215
+
216
+ // Create toast element
217
+ const toast = document.createElement('div');
218
+ toast.style.cssText = `
219
+ min-width: 300px;
220
+ max-width: 90vw;
221
+ margin: 0;
222
+ padding: 12px 16px;
223
+ border-radius: 8px;
224
+ color: white;
225
+ font-size: 14px;
226
+ font-weight: 500;
227
+ opacity: 0;
228
+ transform: translateY(20px) scale(0.95);
229
+ transition: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55);
230
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.1);
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 12px;
234
+ backdrop-filter: blur(8px);
235
+ `;
236
+
237
+ // Define icons for different types with refined styling
238
+ const icons: Record<ToastType, string> = {
239
+ success: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>`,
240
+ error: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>`,
241
+ warning: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`,
242
+ info: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`
243
+ };
244
+
245
+ // Set background color based on type with refined colors
246
+ let iconType: ToastType = type;
247
+ switch(type) {
248
+ case 'success':
249
+ toast.style.backgroundColor = 'rgba(34, 197, 94, 0.95)';
250
+ break;
251
+ case 'error':
252
+ toast.style.backgroundColor = 'rgba(239, 68, 68, 0.95)';
253
+ break;
254
+ case 'warning':
255
+ toast.style.backgroundColor = 'rgba(245, 158, 11, 0.95)';
256
+ break;
257
+ default:
258
+ toast.style.backgroundColor = 'rgba(59, 130, 246, 0.95)';
259
+ iconType = 'info';
260
+ }
261
+
262
+ // Add icon and text with improved styling
263
+ const iconWrapper = document.createElement('div');
264
+ iconWrapper.style.cssText = `
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: center;
268
+ flex-shrink: 0;
269
+ `;
270
+ iconWrapper.innerHTML = icons[iconType];
271
+
272
+ const textWrapper = document.createElement('div');
273
+ textWrapper.style.cssText = `
274
+ flex-grow: 1;
275
+ line-height: 1.4;
276
+ `;
277
+ textWrapper.textContent = text;
278
+
279
+ toast.appendChild(iconWrapper);
280
+ toast.appendChild(textWrapper);
281
+ container.appendChild(toast);
282
+
283
+ // Enhanced animation
284
+ requestAnimationFrame(() => {
285
+ toast.style.opacity = '1';
286
+ toast.style.transform = 'translateY(0) scale(1)';
287
+ });
288
+
289
+ // Remove toast after duration with smooth exit animation
290
+ setTimeout(() => {
291
+ toast.style.opacity = '0';
292
+ toast.style.transform = 'translateY(-20px) scale(0.95)';
293
+ setTimeout(() => {
294
+ container.removeChild(toast);
295
+ if (container.children.length === 0) {
296
+ document.body.removeChild(container);
297
+ }
298
+ }, 200);
299
+ }, duration);
300
+ }
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import { inertia, router } from "@inertiajs/svelte";
3
+ import NaraIcon from "../../Components/NaraIcon.svelte";
4
+ import DarkModeToggle from "../../Components/DarkModeToggle.svelte";
5
+ import axios from "axios";
6
+ import { api, Toast } from "../../Components/helper";
7
+
8
+ interface ForgotPasswordForm {
9
+ email: string;
10
+ phone: string;
11
+ }
12
+
13
+ let form: ForgotPasswordForm = {
14
+ email: "",
15
+ phone: "",
16
+ };
17
+
18
+ let success: boolean = $state(false);
19
+ let { error }: { error?: string } = $props();
20
+
21
+ $effect(() => {
22
+ if (error) Toast(error, 'error');
23
+ });
24
+
25
+ async function submitForm(): Promise<void> {
26
+ const result = await api(() => axios.post("/forgot-password", form));
27
+
28
+ if (result.success) {
29
+ success = true;
30
+ form.email = "";
31
+ form.phone = "";
32
+ }
33
+ }
34
+ </script>
35
+
36
+ <section class="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-slate-50">
37
+ <!-- Header with dark mode toggle -->
38
+ <header class="fixed inset-x-0 top-0 z-40 border-b border-slate-800/60 bg-slate-950/80 backdrop-blur-xl">
39
+ <div class="max-w-6xl mx-auto flex h-16 items-center justify-between px-4 sm:h-20 sm:px-6 lg:px-8">
40
+ <a href="/" use:inertia class="flex items-center gap-2">
41
+ <img src="/public/nara.png" alt="Nara logo" class="h-7 w-7 rounded-lg object-cover" />
42
+ <div class="flex flex-col leading-tight">
43
+ <span class="text-sm font-semibold tracking-tight text-slate-50">Nara</span>
44
+ <span class="text-[10px] uppercase tracking-[0.22em] text-slate-500">TypeScript framework</span>
45
+ </div>
46
+ </a>
47
+ <div class="flex items-center gap-3">
48
+ <DarkModeToggle />
49
+ </div>
50
+ </div>
51
+ </header>
52
+
53
+ <div class="flex flex-col items-center justify-center px-6 py-8 mx-auto min-h-screen lg:py-0">
54
+ <div class="flex items-center mb-6 text-2xl font-semibold text-slate-50">
55
+ <NaraIcon></NaraIcon>
56
+ </div>
57
+ <div class="w-full max-w-md rounded-3xl border border-slate-800/80 bg-slate-900/70 backdrop-blur-xl shadow-[0_24px_80px_rgba(15,23,42,0.8)] md:mt-0 sm:max-w-md xl:p-0">
58
+ <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
59
+ <h1 class="text-xl font-bold leading-tight tracking-tight text-slate-50 md:text-2xl">
60
+ Reset Password
61
+ </h1>
62
+
63
+ {#if success}
64
+ <div class="p-4 mb-4 text-sm text-green-400 rounded-lg bg-green-900/50" role="alert">
65
+ Link reset password telah dikirim ke email atau nomor telepon Anda.
66
+ </div>
67
+ {/if}
68
+
69
+ <form
70
+ class="space-y-4 md:space-y-6"
71
+ on:submit|preventDefault={submitForm}
72
+ >
73
+ <div>
74
+ <label for="email" class="block mb-2 text-sm font-medium text-slate-200">Email atau Nomor Telepon</label>
75
+ <input
76
+ bind:value={form.email}
77
+ type="text"
78
+ name="email"
79
+ id="email"
80
+ class="bg-slate-900/70 border border-slate-700 text-slate-50 sm:text-sm rounded-lg focus:ring-2 focus:ring-primary-400 focus:border-primary-400 focus:outline-none block w-full py-2.5 px-3 placeholder-slate-500"
81
+ placeholder="email@example.com atau 08xxxxxxxxxx"
82
+ required
83
+ />
84
+ </div>
85
+
86
+ <button type="submit" class="w-full text-sm font-medium rounded-full px-5 py-2.5 text-slate-950 bg-primary-400 hover:bg-primary-300 focus:ring-4 focus:outline-none focus:ring-primary-300">
87
+ Kirim Link Reset Password
88
+ </button>
89
+
90
+ <p class="text-sm font-light text-slate-400">
91
+ Ingat password Anda? <a href="/login" use:inertia class="font-medium text-primary-400 hover:underline">Login disini</a>
92
+ </p>
93
+ </form>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </section>