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,370 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import axios from "axios";
4
+ import Header from "../components/Header.vue";
5
+ import { api, Toast } from "../components/helper";
6
+
7
+ interface User {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ phone?: string;
12
+ avatar?: string;
13
+ is_admin: boolean;
14
+ is_verified: boolean;
15
+ }
16
+
17
+ const props = defineProps<{
18
+ user: User;
19
+ }>();
20
+
21
+ const current_password = ref('');
22
+ const new_password = ref('');
23
+ const confirm_password = ref('');
24
+ const isLoading = ref(false);
25
+ const previewUrl = ref(props.user.avatar || null);
26
+
27
+ function handleAvatarChange(event: Event): void {
28
+ const target = event.target as HTMLInputElement;
29
+ const file = target.files?.[0];
30
+ if (file) {
31
+ const formData = new FormData();
32
+ formData.append("file", file);
33
+ isLoading.value = true;
34
+ axios
35
+ .post("/assets/avatar", formData)
36
+ .then((response) => {
37
+ setTimeout(() => {
38
+ isLoading.value = false;
39
+ previewUrl.value = response.data + "?v=" + Date.now();
40
+ }, 500);
41
+ props.user.avatar = response.data + "?v=" + Date.now();
42
+ Toast("Avatar berhasil diupload", "success");
43
+ })
44
+ .catch(() => {
45
+ isLoading.value = false;
46
+ Toast("Gagal mengupload avatar", "error");
47
+ });
48
+ }
49
+ }
50
+
51
+ async function changeProfile(): Promise<void> {
52
+ isLoading.value = true;
53
+ await api(() => axios.post("/change-profile", props.user));
54
+ isLoading.value = false;
55
+ }
56
+
57
+ async function changePassword(): Promise<void> {
58
+ if (new_password.value != confirm_password.value) {
59
+ Toast("Password tidak cocok", "error");
60
+ return;
61
+ }
62
+
63
+ if (!current_password.value || !new_password.value || !confirm_password.value) {
64
+ Toast("Mohon isi semua field", "error");
65
+ return;
66
+ }
67
+
68
+ isLoading.value = true;
69
+ const result = await api(() => axios.post("/change-password", {
70
+ current_password: current_password.value,
71
+ new_password: new_password.value,
72
+ }));
73
+
74
+ if (result.success) {
75
+ current_password.value = "";
76
+ new_password.value = "";
77
+ confirm_password.value = "";
78
+ }
79
+ isLoading.value = false;
80
+ }
81
+ </script>
82
+
83
+ <template>
84
+ <Header group="profile" />
85
+
86
+ <div class="min-h-screen bg-surface-light dark:bg-surface-dark text-slate-900 dark:text-slate-100 transition-colors duration-500 overflow-x-hidden selection:bg-primary-400 selection:text-black">
87
+
88
+ <!-- Background Effects -->
89
+ <div class="fixed inset-0 pointer-events-none z-0">
90
+ <div class="absolute top-0 right-0 w-[600px] h-[600px] bg-info-500/5 rounded-full blur-3xl -mr-64 -mt-64"></div>
91
+ <div class="absolute bottom-0 left-0 w-[800px] h-[800px] bg-primary-500/5 rounded-full blur-3xl -ml-96 -mb-96"></div>
92
+ </div>
93
+
94
+ <section class="relative px-6 sm:px-12 lg:px-24 pt-24 pb-20">
95
+ <div class="max-w-[90rem] mx-auto">
96
+
97
+ <!-- Hero Section -->
98
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 mb-20">
99
+
100
+ <!-- Left: Giant Profile Card -->
101
+ <div class="lg:col-span-5">
102
+ <p class="text-xs font-bold uppercase tracking-[0.3em] text-info-600 dark:text-info-400 mb-6">
103
+ Account
104
+ </p>
105
+ <h1 class="text-[8vw] sm:text-[5vw] lg:text-[3.5vw] leading-[0.9] font-bold tracking-tighter mb-8">
106
+ YOUR
107
+ <span class="block text-transparent bg-clip-text bg-gradient-to-r from-info-500 to-primary-400">
108
+ PROFILE
109
+ </span>
110
+ </h1>
111
+
112
+ <!-- Profile Card -->
113
+ <div class="relative">
114
+ <div class="absolute -inset-1 bg-gradient-to-r from-info-500/20 to-primary-500/20 rounded-3xl blur-xl"></div>
115
+ <div class="relative bg-surface-card-light dark:bg-surface-card-dark border border-slate-200 dark:border-white/5 rounded-3xl p-8 overflow-hidden">
116
+ <!-- Decorative -->
117
+ <div class="absolute top-0 right-0 p-32 bg-info-500/5 rounded-full blur-3xl -mr-16 -mt-16"></div>
118
+
119
+ <div class="relative z-10">
120
+ <!-- Avatar Section -->
121
+ <div class="flex items-center gap-6 mb-8">
122
+ <div class="relative group">
123
+ <div class="w-24 h-24 rounded-2xl bg-gradient-to-br from-info-500 to-primary-500 p-0.5">
124
+ <div class="w-full h-full rounded-2xl bg-surface-card-light dark:bg-surface-card-dark overflow-hidden flex items-center justify-center">
125
+ <img
126
+ v-if="previewUrl"
127
+ :src="previewUrl"
128
+ alt="Profile"
129
+ class="w-full h-full object-cover"
130
+ />
131
+ <span v-else class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-br from-info-500 to-primary-500">
132
+ {{ user.name.charAt(0).toUpperCase() }}
133
+ </span>
134
+ </div>
135
+ </div>
136
+ <label
137
+ class="absolute -bottom-2 -right-2 w-8 h-8 bg-slate-900 dark:bg-white text-white dark:text-black rounded-full flex items-center justify-center cursor-pointer hover:scale-110 transition-transform"
138
+ >
139
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
140
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" stroke-linecap="round" stroke-linejoin="round"/>
141
+ <circle cx="12" cy="13" r="4" stroke-linecap="round" stroke-linejoin="round"/>
142
+ </svg>
143
+ <input
144
+ type="file"
145
+ accept="image/*"
146
+ @change="handleAvatarChange"
147
+ class="hidden"
148
+ />
149
+ </label>
150
+ </div>
151
+
152
+ <div>
153
+ <h2 class="text-2xl font-bold tracking-tight mb-1">{{ user.name }}</h2>
154
+ <p class="text-sm text-slate-500 dark:text-slate-400">{{ user.email }}</p>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Status Badges -->
159
+ <div class="flex flex-wrap gap-2 mb-6">
160
+ <span class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-full" :class="user.is_admin
161
+ ? 'bg-accent-500/10 text-accent-600 dark:text-accent-400 border border-accent-500/20'
162
+ : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-300 dark:border-slate-700'">
163
+ {{ user.is_admin ? 'Administrator' : 'Standard User' }}
164
+ </span>
165
+ <span class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-full" :class="user.is_verified
166
+ ? 'bg-primary-500/10 text-primary-600 dark:text-primary-400 border border-primary-500/20'
167
+ : 'bg-warning-500/10 text-warning-600 dark:text-warning-400 border border-warning-500/20'">
168
+ {{ user.is_verified ? 'Verified' : 'Unverified' }}
169
+ </span>
170
+ </div>
171
+
172
+ <!-- Quick Info -->
173
+ <div class="space-y-3">
174
+ <div class="flex items-center gap-3 text-sm">
175
+ <svg class="w-4 h-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
176
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" stroke-linecap="round" stroke-linejoin="round"/>
177
+ </svg>
178
+ <span class="font-mono text-slate-600 dark:text-slate-300">{{ user.phone || '—' }}</span>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Security Note -->
186
+ <div class="mt-6 p-4 border border-slate-200 dark:border-white/5 rounded-2xl">
187
+ <div class="flex items-start gap-3">
188
+ <div class="w-8 h-8 rounded-full bg-primary-500/10 flex items-center justify-center flex-shrink-0">
189
+ <svg class="w-4 h-4 text-primary-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
190
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke-linecap="round" stroke-linejoin="round"/>
191
+ </svg>
192
+ </div>
193
+ <div>
194
+ <p class="text-xs font-bold uppercase tracking-wider text-slate-500 mb-1">Secure Storage</p>
195
+ <p class="text-xs text-slate-500 dark:text-slate-400">Your data is encrypted and stored securely on Nara's backend infrastructure.</p>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Right: Forms -->
202
+ <div class="lg:col-span-7 space-y-8">
203
+
204
+ <!-- Personal Info Form -->
205
+ <div class="relative bg-surface-card-light dark:bg-surface-card-dark border border-slate-200 dark:border-white/5 hover:border-info-500/30 rounded-3xl p-8 transition-colors duration-500">
206
+ <div class="flex items-center justify-between mb-8">
207
+ <div>
208
+ <span class="text-[10px] font-bold uppercase tracking-[0.2em] text-info-600 dark:text-info-400">Settings</span>
209
+ <h3 class="text-xl font-bold tracking-tight mt-1">Personal Information</h3>
210
+ </div>
211
+ <div class="w-10 h-10 rounded-full bg-info-500/10 flex items-center justify-center">
212
+ <svg class="w-5 h-5 text-info-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
213
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke-linecap="round" stroke-linejoin="round"/>
214
+ <circle cx="12" cy="7" r="4" stroke-linecap="round" stroke-linejoin="round"/>
215
+ </svg>
216
+ </div>
217
+ </div>
218
+
219
+ <form @submit.prevent="changeProfile" class="space-y-6">
220
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
221
+ <div class="space-y-2">
222
+ <label for="name" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
223
+ Full Name
224
+ </label>
225
+ <input
226
+ v-model="user.name"
227
+ type="text"
228
+ id="name"
229
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-info-500 focus:ring-2 focus:ring-info-500/20 outline-none transition-all"
230
+ placeholder="Your full name"
231
+ />
232
+ </div>
233
+
234
+ <div class="space-y-2">
235
+ <label for="phone" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
236
+ Phone Number
237
+ </label>
238
+ <input
239
+ v-model="user.phone"
240
+ type="text"
241
+ id="phone"
242
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-info-500 focus:ring-2 focus:ring-info-500/20 outline-none transition-all"
243
+ placeholder="+62 xxx xxxx xxxx"
244
+ />
245
+ </div>
246
+ </div>
247
+
248
+ <div class="space-y-2">
249
+ <label for="email" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
250
+ Email Address
251
+ </label>
252
+ <input
253
+ v-model="user.email"
254
+ type="email"
255
+ id="email"
256
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-info-500 focus:ring-2 focus:ring-info-500/20 outline-none transition-all"
257
+ placeholder="you@example.com"
258
+ />
259
+ </div>
260
+
261
+ <div class="flex justify-end pt-4">
262
+ <button
263
+ type="submit"
264
+ :disabled="isLoading"
265
+ class="group relative px-6 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 transition-transform disabled:opacity-50"
266
+ >
267
+ <span class="relative z-10 flex items-center gap-2">
268
+ <template v-if="isLoading">
269
+ <svg class="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
270
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
271
+ <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>
272
+ </svg>
273
+ Saving...
274
+ </template>
275
+ <template v-else>
276
+ Save Changes
277
+ </template>
278
+ </span>
279
+ <div class="absolute inset-0 bg-info-500 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
280
+ </button>
281
+ </div>
282
+ </form>
283
+ </div>
284
+
285
+ <!-- Password Form -->
286
+ <div class="relative bg-surface-card-light dark:bg-surface-card-dark border border-slate-200 dark:border-white/5 hover:border-warning-500/30 rounded-3xl p-8 transition-colors duration-500">
287
+ <div class="flex items-center justify-between mb-8">
288
+ <div>
289
+ <span class="text-[10px] font-bold uppercase tracking-[0.2em] text-warning-600 dark:text-warning-400">Security</span>
290
+ <h3 class="text-xl font-bold tracking-tight mt-1">Change Password</h3>
291
+ </div>
292
+ <div class="w-10 h-10 rounded-full bg-warning-500/10 flex items-center justify-center">
293
+ <svg class="w-5 h-5 text-warning-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
294
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke-linecap="round" stroke-linejoin="round"/>
295
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" stroke-linecap="round" stroke-linejoin="round"/>
296
+ </svg>
297
+ </div>
298
+ </div>
299
+
300
+ <form @submit.prevent="changePassword" class="space-y-6">
301
+ <div class="space-y-2">
302
+ <label for="current_password" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
303
+ Current Password
304
+ </label>
305
+ <input
306
+ v-model="current_password"
307
+ type="password"
308
+ id="current_password"
309
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-warning-500 focus:ring-2 focus:ring-warning-500/20 outline-none transition-all"
310
+ placeholder="••••••••"
311
+ />
312
+ </div>
313
+
314
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
315
+ <div class="space-y-2">
316
+ <label for="new_password" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
317
+ New Password
318
+ </label>
319
+ <input
320
+ v-model="new_password"
321
+ type="password"
322
+ id="new_password"
323
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-warning-500 focus:ring-2 focus:ring-warning-500/20 outline-none transition-all"
324
+ placeholder="••••••••"
325
+ />
326
+ </div>
327
+
328
+ <div class="space-y-2">
329
+ <label for="confirm_password" class="block text-[10px] font-bold uppercase tracking-[0.15em] text-slate-500">
330
+ Confirm Password
331
+ </label>
332
+ <input
333
+ v-model="confirm_password"
334
+ type="password"
335
+ id="confirm_password"
336
+ class="w-full px-4 py-3 rounded-xl bg-white dark:bg-black/50 border border-slate-200 dark:border-slate-700 text-sm focus:border-warning-500 focus:ring-2 focus:ring-warning-500/20 outline-none transition-all"
337
+ placeholder="••••••••"
338
+ />
339
+ </div>
340
+ </div>
341
+
342
+ <div class="flex justify-end pt-4">
343
+ <button
344
+ type="submit"
345
+ :disabled="isLoading"
346
+ class="px-6 py-3 text-xs font-bold uppercase tracking-wider border border-slate-200 dark:border-slate-700 rounded-full hover:border-warning-500 hover:text-warning-500 transition-colors disabled:opacity-50"
347
+ >
348
+ <span v-if="isLoading" class="flex items-center gap-2">
349
+ <svg class="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
350
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
351
+ <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>
352
+ </svg>
353
+ Updating...
354
+ </span>
355
+ <span v-else>
356
+ Update Password
357
+ </span>
358
+ </button>
359
+ </div>
360
+ </form>
361
+ </div>
362
+
363
+ </div>
364
+ </div>
365
+
366
+ </div>
367
+ </section>
368
+
369
+ </div>
370
+ </template>
@@ -0,0 +1,264 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import { usePage, router } from '@inertiajs/vue3';
4
+ import Header from '../components/Header.vue';
5
+ import UserModal from '../components/UserModal.vue';
6
+ import Pagination from '../components/Pagination.vue';
7
+ import axios from 'axios';
8
+ import { api, Toast } from '../components/helper';
9
+ import type { User, UserForm, PaginationMeta } from '../types';
10
+ import { createEmptyUserForm, userToForm } from '../types';
11
+
12
+ const props = defineProps<{
13
+ users: User[];
14
+ total: number;
15
+ page: number;
16
+ limit: number;
17
+ totalPages: number;
18
+ hasNext: boolean;
19
+ hasPrev: boolean;
20
+ search: string;
21
+ filter: string;
22
+ }>();
23
+
24
+ const pageData = usePage();
25
+ const currentUser = computed(() => pageData.props.user as User | undefined);
26
+
27
+ const paginationMeta = computed(() => ({
28
+ total: props.total,
29
+ page: props.page,
30
+ limit: props.limit,
31
+ totalPages: props.totalPages,
32
+ hasNext: props.hasNext,
33
+ hasPrev: props.hasPrev
34
+ }) as PaginationMeta);
35
+
36
+ const showUserModal = ref(false);
37
+ const isSubmitting = ref(false);
38
+ const mode = ref<'create' | 'edit'>('create');
39
+ const form = ref<UserForm>(createEmptyUserForm());
40
+
41
+ function openCreateUser(): void {
42
+ mode.value = 'create';
43
+ form.value = createEmptyUserForm();
44
+ showUserModal.value = true;
45
+ }
46
+
47
+ function openEditUser(userItem: User): void {
48
+ mode.value = 'edit';
49
+ form.value = userToForm(userItem);
50
+ showUserModal.value = true;
51
+ }
52
+
53
+ function closeUserModal(): void {
54
+ showUserModal.value = false;
55
+ form.value = createEmptyUserForm();
56
+ }
57
+
58
+ async function handleSubmit(formData: UserForm): Promise<void> {
59
+ if (!formData.name || !formData.email) {
60
+ Toast('Nama dan email wajib diisi', 'error');
61
+ return;
62
+ }
63
+
64
+ isSubmitting.value = true;
65
+
66
+ const payload = {
67
+ name: formData.name,
68
+ email: formData.email,
69
+ phone: formData.phone || null,
70
+ is_admin: formData.is_admin,
71
+ is_verified: formData.is_verified,
72
+ password: formData.password || undefined
73
+ };
74
+
75
+ const result = mode.value === 'create'
76
+ ? await api(() => axios.post('/users', payload))
77
+ : await api(() => axios.put(`/users/${formData.id}`, payload));
78
+
79
+ if (result.success) {
80
+ closeUserModal();
81
+ router.visit('/users', { preserveScroll: true, preserveState: true });
82
+ }
83
+
84
+ isSubmitting.value = false;
85
+ }
86
+
87
+ async function deleteUser(id: string): Promise<void> {
88
+ if (!confirm('Yakin ingin menghapus user ini?')) {
89
+ return;
90
+ }
91
+
92
+ isSubmitting.value = true;
93
+
94
+ const result = await api(() => axios.delete('/users', { data: { ids: [id] } }));
95
+
96
+ if (result.success) {
97
+ router.visit('/users', { preserveScroll: true, preserveState: true });
98
+ }
99
+
100
+ isSubmitting.value = false;
101
+ }
102
+ </script>
103
+
104
+ <template>
105
+ <Header group="users" />
106
+
107
+ <div class="min-h-screen bg-surface-light dark:bg-surface-dark text-slate-900 dark:text-slate-100 transition-colors duration-500 overflow-x-hidden selection:bg-primary-400 selection:text-black">
108
+
109
+ <!-- Background Effects -->
110
+ <div class="fixed inset-0 pointer-events-none z-0">
111
+ <div class="absolute top-0 left-1/2 w-[1000px] h-[1000px] bg-accent-500/5 rounded-full blur-3xl -translate-x-1/2 -mt-[500px]"></div>
112
+ </div>
113
+
114
+ <section class="relative px-6 sm:px-12 lg:px-24 pt-24 pb-20">
115
+ <div class="max-w-[90rem] mx-auto">
116
+
117
+ <!-- Header Section -->
118
+ <div class="flex flex-col lg:flex-row lg:items-end justify-between gap-8 mb-16">
119
+ <div>
120
+ <p class="text-xs font-bold uppercase tracking-[0.3em] text-accent-600 dark:text-accent-400 mb-6">
121
+ Management
122
+ </p>
123
+ <h1 class="text-[8vw] sm:text-[6vw] lg:text-[4vw] leading-[0.9] font-bold tracking-tighter mb-4">
124
+ USERS
125
+ </h1>
126
+ <p class="text-lg text-slate-500 dark:text-slate-400 max-w-xl font-serif italic">
127
+ "Control who enters. Manage who stays."
128
+ </p>
129
+ </div>
130
+
131
+ <div class="flex items-center gap-6">
132
+ <!-- Stats -->
133
+ <div class="text-right">
134
+ <p class="text-[10px] uppercase tracking-[0.2em] text-slate-400 mb-1">Total</p>
135
+ <p class="text-3xl font-bold tracking-tight">{{ total }}</p>
136
+ </div>
137
+
138
+ <div class="h-12 w-px bg-slate-200 dark:bg-slate-700"></div>
139
+
140
+ <button
141
+ v-if="currentUser?.is_admin"
142
+ class="group relative px-6 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 transition-transform disabled:opacity-50"
143
+ @click="openCreateUser"
144
+ :disabled="isSubmitting"
145
+ >
146
+ <span class="relative z-10 flex items-center gap-2">
147
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
148
+ <path d="M12 5v14M5 12h14" stroke-linecap="round" stroke-linejoin="round"/>
149
+ </svg>
150
+ Add User
151
+ </span>
152
+ <div class="absolute inset-0 bg-primary-500 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
153
+ </button>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Users Grid - Card Based -->
158
+ <div class="mb-12">
159
+ <div v-if="users && users.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
160
+ <div
161
+ v-for="(userItem, i) in users"
162
+ :key="userItem.id"
163
+ class="group relative bg-surface-card-light dark:bg-surface-card-dark border border-slate-200 dark:border-white/5 hover:border-accent-500/50 dark:hover:border-accent-500/30 p-6 rounded-2xl transition-all duration-500 overflow-hidden"
164
+ >
165
+ <!-- Hover Glow -->
166
+ <div class="absolute top-0 right-0 p-20 bg-accent-500/5 rounded-full blur-3xl -mr-10 -mt-10 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
167
+
168
+ <div class="relative z-10">
169
+ <!-- User Header -->
170
+ <div class="flex items-start justify-between mb-4">
171
+ <div class="flex items-center gap-3">
172
+ <!-- Avatar -->
173
+ <div class="w-12 h-12 rounded-full bg-gradient-to-br from-accent-500 to-accent-400 flex items-center justify-center text-white font-bold text-lg">
174
+ {{ userItem.name.charAt(0).toUpperCase() }}
175
+ </div>
176
+ <div>
177
+ <h3 class="font-bold text-lg tracking-tight">{{ userItem.name }}</h3>
178
+ <p class="text-xs text-slate-500 dark:text-slate-400">{{ userItem.email }}</p>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Status Badge -->
183
+ <span class="px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider rounded-full" :class="userItem.is_verified
184
+ ? 'bg-primary-500/10 text-primary-600 dark:text-primary-400 border border-primary-500/20'
185
+ : 'bg-warning-500/10 text-warning-600 dark:text-warning-400 border border-warning-500/20'">
186
+ {{ userItem.is_verified ? 'Verified' : 'Pending' }}
187
+ </span>
188
+ </div>
189
+
190
+ <!-- User Details -->
191
+ <div class="space-y-2 mb-4">
192
+ <div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
193
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
194
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" stroke-linecap="round" stroke-linejoin="round"/>
195
+ </svg>
196
+ <span class="font-mono">{{ userItem.phone || '—' }}</span>
197
+ </div>
198
+ <div class="flex items-center gap-2 text-xs">
199
+ <span class="inline-flex h-1.5 w-1.5 rounded-full" :class="userItem.is_admin ? 'bg-accent-500' : 'bg-slate-400'"></span>
200
+ <span class="text-slate-500 dark:text-slate-400">{{ userItem.is_admin ? 'Administrator' : 'Standard User' }}</span>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Actions -->
205
+ <div v-if="currentUser?.is_admin" class="flex items-center gap-2 pt-4 border-t border-slate-200 dark:border-white/5">
206
+ <button
207
+ class="flex-1 px-4 py-2 text-xs font-bold uppercase tracking-wider border border-slate-200 dark:border-slate-700 rounded-full hover:border-accent-500/50 hover:text-accent-500 transition-colors disabled:opacity-50"
208
+ @click="openEditUser(userItem)"
209
+ :disabled="isSubmitting"
210
+ >
211
+ Edit
212
+ </button>
213
+ <button
214
+ class="px-4 py-2 text-xs font-bold uppercase tracking-wider border border-danger-500/20 text-danger-500 rounded-full hover:bg-danger-500 hover:text-white transition-colors disabled:opacity-50"
215
+ @click="deleteUser(userItem.id)"
216
+ :disabled="isSubmitting"
217
+ >
218
+ Delete
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Empty State -->
226
+ <div v-else class="text-center py-20 border border-dashed border-slate-200 dark:border-slate-700 rounded-2xl">
227
+ <div class="w-16 h-16 mx-auto mb-6 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
228
+ <svg class="w-8 h-8 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
229
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke-linecap="round" stroke-linejoin="round"/>
230
+ <circle cx="9" cy="7" r="4" stroke-linecap="round" stroke-linejoin="round"/>
231
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke-linecap="round" stroke-linejoin="round"/>
232
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" stroke-linecap="round" stroke-linejoin="round"/>
233
+ </svg>
234
+ </div>
235
+ <h3 class="text-xl font-bold mb-2">No Users Yet</h3>
236
+ <p class="text-sm text-slate-500 dark:text-slate-400 mb-6">Start by adding your first user to the system.</p>
237
+ <button
238
+ v-if="currentUser?.is_admin"
239
+ class="px-6 py-3 bg-slate-900 dark:bg-white text-white dark:text-black text-xs font-bold uppercase tracking-wider rounded-full hover:scale-105 transition-transform"
240
+ @click="openCreateUser"
241
+ >
242
+ Add First User
243
+ </button>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Pagination -->
248
+ <div>
249
+ <Pagination :meta="paginationMeta" />
250
+ </div>
251
+
252
+ </div>
253
+ </section>
254
+
255
+ <UserModal
256
+ :show="showUserModal"
257
+ :mode="mode"
258
+ :form="form"
259
+ :isSubmitting="isSubmitting"
260
+ @close="closeUserModal"
261
+ @submit="handleSubmit"
262
+ />
263
+ </div>
264
+ </template>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NARA App</title>
7
+ @vite
8
+ </head>
9
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
10
+ @inertia
11
+ </body>
12
+ </html>
@@ -0,0 +1,17 @@
1
+ import type { NaraApp } from '@nara-web/core';
2
+
3
+ export function registerRoutes(app: NaraApp) {
4
+ app.get('/', (req, res) => {
5
+ return res.inertia('landing', {
6
+ title: 'Welcome to NARA'
7
+ });
8
+ });
9
+
10
+ app.get('/dashboard', (req, res) => {
11
+ return res.inertia('dashboard');
12
+ });
13
+
14
+ app.get('/login', (req, res) => {
15
+ return res.inertia('auth/login');
16
+ });
17
+ }