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