create-mantiq 0.7.0 → 0.7.2
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/package.json +2 -1
- package/skeleton/.env.example +64 -0
- package/skeleton/README.md +46 -0
- package/skeleton/app/Console/Commands/.gitkeep +0 -0
- package/skeleton/app/Enums/UserStatus.ts +7 -0
- package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
- package/skeleton/app/Http/Middleware/.gitkeep +0 -0
- package/skeleton/app/Models/User.ts +7 -0
- package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
- package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
- package/skeleton/bootstrap/.gitkeep +0 -0
- package/skeleton/config/ai.ts +51 -0
- package/skeleton/config/app.ts +108 -0
- package/skeleton/config/auth.ts +51 -0
- package/skeleton/config/broadcasting.ts +93 -0
- package/skeleton/config/cache.ts +61 -0
- package/skeleton/config/cors.ts +77 -0
- package/skeleton/config/database.ts +120 -0
- package/skeleton/config/filesystem.ts +58 -0
- package/skeleton/config/hashing.ts +47 -0
- package/skeleton/config/heartbeat.ts +112 -0
- package/skeleton/config/logging.ts +58 -0
- package/skeleton/config/mail.ts +93 -0
- package/skeleton/config/notify.ts +141 -0
- package/skeleton/config/queue.ts +59 -0
- package/skeleton/config/search.ts +96 -0
- package/skeleton/config/services.ts +110 -0
- package/skeleton/config/session.ts +84 -0
- package/skeleton/config/vite.ts +33 -0
- package/skeleton/database/factories/.gitkeep +0 -0
- package/skeleton/database/migrations/001_create_users_table.ts +19 -0
- package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
- package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
- package/skeleton/index.ts +20 -0
- package/skeleton/mantiq.ts +8 -0
- package/skeleton/package.json +34 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/routes/api.ts +8 -0
- package/skeleton/routes/channels.ts +23 -0
- package/skeleton/routes/console.ts +24 -0
- package/skeleton/routes/web.ts +6 -0
- package/skeleton/storage/cache/.gitkeep +0 -0
- package/skeleton/storage/framework/.gitkeep +0 -0
- package/skeleton/tests/feature/api.test.ts +14 -0
- package/skeleton/tests/feature/home.test.ts +17 -0
- package/skeleton/tests/unit/example.test.ts +11 -0
- package/skeleton/tsconfig.json +27 -0
- package/src/index.ts +289 -25
- package/src/templates.ts +141 -945
- package/src/terminal.ts +64 -0
- package/stubs/api-only/routes/api.ts.stub +24 -0
- package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/auth/api/routes/api.ts.stub +24 -0
- package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
- package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
- package/stubs/auth/web/routes/web.ts.stub +25 -0
- package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
- package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
- package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/auth/web/vue/src/App.vue.stub +74 -0
- package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
- package/stubs/manifest.json +630 -2
- package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
- package/stubs/noauth/app/Models/User.ts.stub +5 -0
- package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
- package/stubs/noauth/routes/api.ts.stub +16 -0
- package/stubs/noauth/routes/web.ts.stub +15 -0
- package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
- package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
- package/stubs/noauth/vue/src/App.vue.stub +62 -0
- package/stubs/noauth/vue/src/pages.ts.stub +7 -0
- package/stubs/react/src/App.tsx.stub +4 -2
- package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
- package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
- package/stubs/react/src/lib/api.ts.stub +30 -6
- package/stubs/react/src/pages/Login.tsx.stub +3 -3
- package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
- package/stubs/react/vite.config.ts.stub +26 -3
- package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
- package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
- package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
- package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
- package/stubs/shared/config/app.ts.stub +36 -0
- package/stubs/shared/config/vite.ts.stub +8 -0
- package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
- package/stubs/shared/routes/api.ts.stub +12 -102
- package/stubs/shared/routes/web.ts.stub +5 -3
- package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
- package/stubs/svelte/src/App.svelte.stub +1 -1
- package/stubs/svelte/src/lib/api.ts.stub +30 -6
- package/stubs/svelte/src/main.ts.stub +3 -1
- package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
- package/stubs/svelte/vite.config.ts.stub +20 -1
- package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
- package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
- package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
- package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
- package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
- package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
- package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
- package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
- package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
- package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
- package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
- package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
- package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
- package/stubs/tailwind-only/react/src/style.css.stub +14 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
- package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
- package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
- package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
- package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
- package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
- package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
- package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
- package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
- package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
- package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
- package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
- package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
- package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
- package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
- package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
- package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
- package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
- package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
- package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
- package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
- package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
- package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
- package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
- package/stubs/themes/corporate/react/src/style.css.stub +135 -0
- package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
- package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
- package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
- package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
- package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
- package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
- package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
- package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
- package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
- package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
- package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
- package/stubs/themes/minimal/react/src/style.css.stub +142 -0
- package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
- package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
- package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
- package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
- package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
- package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
- package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
- package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
- package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
- package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
- package/stubs/themes/starter/react/src/style.css.stub +141 -0
- package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
- package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
- package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
- package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
- package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
- package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
- package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
- package/stubs/themes/starter/vue/src/style.css.stub +141 -0
- package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
- package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
- package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
- package/stubs/themes/workspace/react/src/style.css.stub +138 -0
- package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
- package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
- package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
- package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
- package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
- package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
- package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
- package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
- package/stubs/vue/src/App.vue.stub +2 -1
- package/stubs/vue/src/lib/api.ts.stub +30 -6
- package/stubs/vue/src/main.ts.stub +3 -1
- package/stubs/vue/src/pages/Login.vue.stub +3 -3
- package/stubs/vue/vite.config.ts.stub +20 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { api, post, del } from '@/lib/api'
|
|
4
|
+
import { AuthenticatedLayout, Header, Main } from '@/components/layout'
|
|
5
|
+
import { Plus, Search, MoreHorizontal, ArrowUpDown, ChevronLeft, ChevronRight, X } from 'lucide-vue-next'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
appName?: string
|
|
9
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
10
|
+
navigate: (href: string) => void
|
|
11
|
+
}>(), {
|
|
12
|
+
appName: 'Mantiq',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
interface UserRecord {
|
|
16
|
+
id: number
|
|
17
|
+
name: string
|
|
18
|
+
email: string
|
|
19
|
+
created_at?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const users = ref<UserRecord[]>([])
|
|
23
|
+
const loading = ref(false)
|
|
24
|
+
const search = ref('')
|
|
25
|
+
const page = ref(1)
|
|
26
|
+
const perPage = ref(10)
|
|
27
|
+
const sortBy = ref('created_at')
|
|
28
|
+
const sortDir = ref<'asc' | 'desc'>('desc')
|
|
29
|
+
const meta = ref({ total: 0, filtered_total: 0, page: 1, per_page: 10, last_page: 1 })
|
|
30
|
+
|
|
31
|
+
// Dialogs
|
|
32
|
+
const addOpen = ref(false)
|
|
33
|
+
const editOpen = ref(false)
|
|
34
|
+
const deleteOpen = ref(false)
|
|
35
|
+
const selectedUser = ref<UserRecord | null>(null)
|
|
36
|
+
|
|
37
|
+
// Action dropdown
|
|
38
|
+
const actionMenuId = ref<number | null>(null)
|
|
39
|
+
|
|
40
|
+
// Form state
|
|
41
|
+
const formName = ref('')
|
|
42
|
+
const formEmail = ref('')
|
|
43
|
+
const formPassword = ref('')
|
|
44
|
+
const formError = ref('')
|
|
45
|
+
const formLoading = ref(false)
|
|
46
|
+
|
|
47
|
+
async function fetchUsers() {
|
|
48
|
+
loading.value = true
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
search: search.value,
|
|
51
|
+
page: String(page.value),
|
|
52
|
+
per_page: String(perPage.value),
|
|
53
|
+
sort: sortBy.value,
|
|
54
|
+
dir: sortDir.value,
|
|
55
|
+
})
|
|
56
|
+
const { ok, data } = await api(`/api/users?${params}`)
|
|
57
|
+
if (ok) {
|
|
58
|
+
users.value = data.data ?? []
|
|
59
|
+
meta.value = data.meta ?? meta.value
|
|
60
|
+
}
|
|
61
|
+
loading.value = false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toggleSort(col: string) {
|
|
65
|
+
if (sortBy.value === col) {
|
|
66
|
+
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
|
|
67
|
+
} else {
|
|
68
|
+
sortBy.value = col
|
|
69
|
+
sortDir.value = 'asc'
|
|
70
|
+
}
|
|
71
|
+
page.value = 1
|
|
72
|
+
fetchUsers()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
|
76
|
+
function onSearchInput() {
|
|
77
|
+
if (searchTimeout) clearTimeout(searchTimeout)
|
|
78
|
+
searchTimeout = setTimeout(() => {
|
|
79
|
+
page.value = 1
|
|
80
|
+
fetchUsers()
|
|
81
|
+
}, 300)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function openAdd() {
|
|
85
|
+
formName.value = ''
|
|
86
|
+
formEmail.value = ''
|
|
87
|
+
formPassword.value = ''
|
|
88
|
+
formError.value = ''
|
|
89
|
+
addOpen.value = true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function openEdit(user: UserRecord) {
|
|
93
|
+
selectedUser.value = user
|
|
94
|
+
formName.value = user.name
|
|
95
|
+
formEmail.value = user.email
|
|
96
|
+
formPassword.value = ''
|
|
97
|
+
formError.value = ''
|
|
98
|
+
editOpen.value = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function openDelete(user: UserRecord) {
|
|
102
|
+
selectedUser.value = user
|
|
103
|
+
formError.value = ''
|
|
104
|
+
deleteOpen.value = true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleAdd() {
|
|
108
|
+
formError.value = ''
|
|
109
|
+
formLoading.value = true
|
|
110
|
+
const { ok, data } = await post('/api/users', {
|
|
111
|
+
name: formName.value,
|
|
112
|
+
email: formEmail.value,
|
|
113
|
+
password: formPassword.value,
|
|
114
|
+
})
|
|
115
|
+
formLoading.value = false
|
|
116
|
+
if (ok) {
|
|
117
|
+
addOpen.value = false
|
|
118
|
+
fetchUsers()
|
|
119
|
+
} else {
|
|
120
|
+
formError.value = data?.error ?? 'Failed to create user.'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function handleEdit() {
|
|
125
|
+
if (!selectedUser.value) return
|
|
126
|
+
formError.value = ''
|
|
127
|
+
formLoading.value = true
|
|
128
|
+
const body: Record<string, string> = { name: formName.value, email: formEmail.value }
|
|
129
|
+
if (formPassword.value) body.password = formPassword.value
|
|
130
|
+
const { ok, data } = await api(`/api/users/${selectedUser.value.id}`, {
|
|
131
|
+
method: 'PUT',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
})
|
|
135
|
+
formLoading.value = false
|
|
136
|
+
if (ok) {
|
|
137
|
+
editOpen.value = false
|
|
138
|
+
fetchUsers()
|
|
139
|
+
} else {
|
|
140
|
+
formError.value = data?.error ?? 'Failed to update user.'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function handleDelete() {
|
|
145
|
+
if (!selectedUser.value) return
|
|
146
|
+
formError.value = ''
|
|
147
|
+
formLoading.value = true
|
|
148
|
+
const { ok, data } = await del(`/api/users/${selectedUser.value.id}`)
|
|
149
|
+
formLoading.value = false
|
|
150
|
+
if (ok) {
|
|
151
|
+
deleteOpen.value = false
|
|
152
|
+
fetchUsers()
|
|
153
|
+
} else {
|
|
154
|
+
formError.value = data?.error ?? 'Failed to delete user.'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onMounted(fetchUsers)
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<template>
|
|
162
|
+
<AuthenticatedLayout
|
|
163
|
+
:current-user="currentUser"
|
|
164
|
+
:app-name="appName"
|
|
165
|
+
:navigate="navigate"
|
|
166
|
+
active-path="/users"
|
|
167
|
+
>
|
|
168
|
+
<Header :navigate="navigate" />
|
|
169
|
+
<Main>
|
|
170
|
+
<div class="space-y-4">
|
|
171
|
+
<div class="flex items-center justify-between">
|
|
172
|
+
<h2 class="text-3xl font-bold tracking-tight">Users</h2>
|
|
173
|
+
<button
|
|
174
|
+
class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
|
|
175
|
+
@click="openAdd"
|
|
176
|
+
>
|
|
177
|
+
<Plus class="h-4 w-4" />
|
|
178
|
+
Add User
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Search -->
|
|
183
|
+
<div class="flex items-center gap-2">
|
|
184
|
+
<div class="relative max-w-sm flex-1">
|
|
185
|
+
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
186
|
+
<input
|
|
187
|
+
v-model="search"
|
|
188
|
+
placeholder="Search users..."
|
|
189
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pl-9 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
190
|
+
@input="onSearchInput"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<span class="text-sm text-muted-foreground">
|
|
194
|
+
{{ loading ? 'Loading...' : `${meta.filtered_total} of ${meta.total} users` }}
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Table -->
|
|
199
|
+
<div class="w-full overflow-auto rounded-md border">
|
|
200
|
+
<table class="w-full caption-bottom text-sm">
|
|
201
|
+
<thead class="border-b bg-muted/50">
|
|
202
|
+
<tr>
|
|
203
|
+
<th
|
|
204
|
+
class="h-10 px-4 text-left align-middle font-medium text-muted-foreground cursor-pointer select-none"
|
|
205
|
+
@click="toggleSort('name')"
|
|
206
|
+
>
|
|
207
|
+
<span class="flex items-center gap-1">
|
|
208
|
+
Name
|
|
209
|
+
<ArrowUpDown class="h-3 w-3" />
|
|
210
|
+
</span>
|
|
211
|
+
</th>
|
|
212
|
+
<th
|
|
213
|
+
class="h-10 px-4 text-left align-middle font-medium text-muted-foreground cursor-pointer select-none"
|
|
214
|
+
@click="toggleSort('email')"
|
|
215
|
+
>
|
|
216
|
+
<span class="flex items-center gap-1">
|
|
217
|
+
Email
|
|
218
|
+
<ArrowUpDown class="h-3 w-3" />
|
|
219
|
+
</span>
|
|
220
|
+
</th>
|
|
221
|
+
<th class="h-10 w-10 px-4 text-left align-middle font-medium text-muted-foreground" />
|
|
222
|
+
</tr>
|
|
223
|
+
</thead>
|
|
224
|
+
<tbody>
|
|
225
|
+
<tr
|
|
226
|
+
v-for="user in users"
|
|
227
|
+
:key="user.id"
|
|
228
|
+
class="border-b transition-colors hover:bg-muted/50"
|
|
229
|
+
>
|
|
230
|
+
<td class="p-4 align-middle font-medium">{{ user.name }}</td>
|
|
231
|
+
<td class="p-4 align-middle">{{ user.email }}</td>
|
|
232
|
+
<td class="p-4 align-middle">
|
|
233
|
+
<div class="relative">
|
|
234
|
+
<button
|
|
235
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
236
|
+
@click.stop="actionMenuId = actionMenuId === user.id ? null : user.id"
|
|
237
|
+
>
|
|
238
|
+
<MoreHorizontal class="h-4 w-4" />
|
|
239
|
+
</button>
|
|
240
|
+
<div
|
|
241
|
+
v-if="actionMenuId === user.id"
|
|
242
|
+
class="absolute right-0 top-full z-50 mt-1 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
243
|
+
>
|
|
244
|
+
<button
|
|
245
|
+
class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground"
|
|
246
|
+
@click="actionMenuId = null; openEdit(user)"
|
|
247
|
+
>
|
|
248
|
+
Edit
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-destructive hover:bg-destructive/10"
|
|
252
|
+
@click="actionMenuId = null; openDelete(user)"
|
|
253
|
+
>
|
|
254
|
+
Delete
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</td>
|
|
259
|
+
</tr>
|
|
260
|
+
<tr v-if="users.length === 0 && !loading">
|
|
261
|
+
<td colspan="3" class="h-24 text-center text-muted-foreground">
|
|
262
|
+
No users found.
|
|
263
|
+
</td>
|
|
264
|
+
</tr>
|
|
265
|
+
</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- Pagination -->
|
|
270
|
+
<div class="flex items-center justify-between">
|
|
271
|
+
<p class="text-sm text-muted-foreground">
|
|
272
|
+
Page {{ meta.page }} of {{ meta.last_page }}
|
|
273
|
+
</p>
|
|
274
|
+
<div class="flex items-center gap-2">
|
|
275
|
+
<button
|
|
276
|
+
class="inline-flex items-center gap-1 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
277
|
+
:disabled="meta.page <= 1"
|
|
278
|
+
@click="page--; fetchUsers()"
|
|
279
|
+
>
|
|
280
|
+
<ChevronLeft class="h-4 w-4" />
|
|
281
|
+
Previous
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
class="inline-flex items-center gap-1 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
285
|
+
:disabled="meta.page >= meta.last_page"
|
|
286
|
+
@click="page++; fetchUsers()"
|
|
287
|
+
>
|
|
288
|
+
Next
|
|
289
|
+
<ChevronRight class="h-4 w-4" />
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</Main>
|
|
295
|
+
|
|
296
|
+
<!-- Add User Dialog -->
|
|
297
|
+
<Teleport to="body">
|
|
298
|
+
<div v-if="addOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
|
299
|
+
<div class="fixed inset-0 bg-black/50" @click="addOpen = false" />
|
|
300
|
+
<div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
|
|
301
|
+
<button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="addOpen = false">
|
|
302
|
+
<X class="h-4 w-4" />
|
|
303
|
+
</button>
|
|
304
|
+
<div class="mb-4">
|
|
305
|
+
<h2 class="text-lg font-semibold leading-none tracking-tight">Add User</h2>
|
|
306
|
+
<p class="mt-1.5 text-sm text-muted-foreground">Create a new user account.</p>
|
|
307
|
+
</div>
|
|
308
|
+
<form class="space-y-4" @submit.prevent="handleAdd">
|
|
309
|
+
<div v-if="formError" class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
|
310
|
+
{{ formError }}
|
|
311
|
+
</div>
|
|
312
|
+
<div class="space-y-2">
|
|
313
|
+
<label for="add-name" class="text-sm font-medium leading-none">Name</label>
|
|
314
|
+
<input id="add-name" v-model="formName" required placeholder="Full name" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
315
|
+
</div>
|
|
316
|
+
<div class="space-y-2">
|
|
317
|
+
<label for="add-email" class="text-sm font-medium leading-none">Email</label>
|
|
318
|
+
<input id="add-email" v-model="formEmail" type="email" required placeholder="user@example.com" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
319
|
+
</div>
|
|
320
|
+
<div class="space-y-2">
|
|
321
|
+
<label for="add-password" class="text-sm font-medium leading-none">Password</label>
|
|
322
|
+
<input id="add-password" v-model="formPassword" type="password" required placeholder="Min 6 characters" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
323
|
+
</div>
|
|
324
|
+
<div class="flex justify-end gap-2">
|
|
325
|
+
<button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="addOpen = false">Cancel</button>
|
|
326
|
+
<button type="submit" :disabled="formLoading" class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50">
|
|
327
|
+
{{ formLoading ? 'Creating...' : 'Create User' }}
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
</form>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</Teleport>
|
|
334
|
+
|
|
335
|
+
<!-- Edit User Dialog -->
|
|
336
|
+
<Teleport to="body">
|
|
337
|
+
<div v-if="editOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
|
338
|
+
<div class="fixed inset-0 bg-black/50" @click="editOpen = false" />
|
|
339
|
+
<div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
|
|
340
|
+
<button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="editOpen = false">
|
|
341
|
+
<X class="h-4 w-4" />
|
|
342
|
+
</button>
|
|
343
|
+
<div class="mb-4">
|
|
344
|
+
<h2 class="text-lg font-semibold leading-none tracking-tight">Edit User</h2>
|
|
345
|
+
<p class="mt-1.5 text-sm text-muted-foreground">Update user information.</p>
|
|
346
|
+
</div>
|
|
347
|
+
<form class="space-y-4" @submit.prevent="handleEdit">
|
|
348
|
+
<div v-if="formError" class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
|
349
|
+
{{ formError }}
|
|
350
|
+
</div>
|
|
351
|
+
<div class="space-y-2">
|
|
352
|
+
<label for="edit-name" class="text-sm font-medium leading-none">Name</label>
|
|
353
|
+
<input id="edit-name" v-model="formName" required placeholder="Full name" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
354
|
+
</div>
|
|
355
|
+
<div class="space-y-2">
|
|
356
|
+
<label for="edit-email" class="text-sm font-medium leading-none">Email</label>
|
|
357
|
+
<input id="edit-email" v-model="formEmail" type="email" required placeholder="user@example.com" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
358
|
+
</div>
|
|
359
|
+
<div class="space-y-2">
|
|
360
|
+
<label for="edit-password" class="text-sm font-medium leading-none">Password</label>
|
|
361
|
+
<input id="edit-password" v-model="formPassword" type="password" placeholder="Leave blank to keep current" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
362
|
+
</div>
|
|
363
|
+
<div class="flex justify-end gap-2">
|
|
364
|
+
<button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="editOpen = false">Cancel</button>
|
|
365
|
+
<button type="submit" :disabled="formLoading" class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50">
|
|
366
|
+
{{ formLoading ? 'Saving...' : 'Save Changes' }}
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</form>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</Teleport>
|
|
373
|
+
|
|
374
|
+
<!-- Delete User Dialog -->
|
|
375
|
+
<Teleport to="body">
|
|
376
|
+
<div v-if="deleteOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
|
377
|
+
<div class="fixed inset-0 bg-black/50" @click="deleteOpen = false" />
|
|
378
|
+
<div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
|
|
379
|
+
<button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="deleteOpen = false">
|
|
380
|
+
<X class="h-4 w-4" />
|
|
381
|
+
</button>
|
|
382
|
+
<div class="mb-4">
|
|
383
|
+
<h2 class="text-lg font-semibold leading-none tracking-tight">Delete User</h2>
|
|
384
|
+
<p class="mt-1.5 text-sm text-muted-foreground">
|
|
385
|
+
Are you sure you want to delete <strong>{{ selectedUser?.name }}</strong>? This action cannot be undone.
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
<div v-if="formError" class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
|
389
|
+
{{ formError }}
|
|
390
|
+
</div>
|
|
391
|
+
<div class="flex justify-end gap-2">
|
|
392
|
+
<button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="deleteOpen = false">Cancel</button>
|
|
393
|
+
<button :disabled="formLoading" class="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-destructive/90 disabled:pointer-events-none disabled:opacity-50" @click="handleDelete">
|
|
394
|
+
{{ formLoading ? 'Deleting...' : 'Delete User' }}
|
|
395
|
+
</button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</Teleport>
|
|
400
|
+
</AuthenticatedLayout>
|
|
401
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { AuthenticatedLayout, Header, Main } from '@/components/layout'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
appName?: string
|
|
6
|
+
currentUser?: { name: string; email: string; role?: string } | null
|
|
7
|
+
navigate: (href: string) => void
|
|
8
|
+
activePath: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
}>(), {
|
|
12
|
+
appName: 'Mantiq',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const tabs = [
|
|
16
|
+
{ title: 'Profile', href: '/account/profile' },
|
|
17
|
+
{ title: 'Security', href: '/account/security' },
|
|
18
|
+
{ title: 'Preferences', href: '/account/preferences' },
|
|
19
|
+
]
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<AuthenticatedLayout
|
|
24
|
+
:current-user="currentUser"
|
|
25
|
+
:app-name="appName"
|
|
26
|
+
:navigate="navigate"
|
|
27
|
+
:active-path="activePath"
|
|
28
|
+
>
|
|
29
|
+
<Header :navigate="navigate" />
|
|
30
|
+
<Main>
|
|
31
|
+
<div class="space-y-6">
|
|
32
|
+
<div>
|
|
33
|
+
<h2 class="text-3xl font-bold tracking-tight">{{ title }}</h2>
|
|
34
|
+
<p class="text-muted-foreground">{{ description }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Tab nav -->
|
|
38
|
+
<nav class="flex gap-2 border-b">
|
|
39
|
+
<button
|
|
40
|
+
v-for="tab in tabs"
|
|
41
|
+
:key="tab.href"
|
|
42
|
+
class="relative px-4 py-2 text-sm font-medium transition-colors"
|
|
43
|
+
:class="activePath === tab.href
|
|
44
|
+
? 'text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-foreground'
|
|
45
|
+
: 'text-muted-foreground hover:text-foreground'"
|
|
46
|
+
@click="navigate(tab.href)"
|
|
47
|
+
>
|
|
48
|
+
{{ tab.title }}
|
|
49
|
+
</button>
|
|
50
|
+
</nav>
|
|
51
|
+
|
|
52
|
+
<slot />
|
|
53
|
+
</div>
|
|
54
|
+
</Main>
|
|
55
|
+
</AuthenticatedLayout>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import AccountLayout from './layout.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
appName?: string
|
|
7
|
+
currentUser?: { name: string; email: string; role?: string } | null
|
|
8
|
+
navigate: (href: string) => void
|
|
9
|
+
}>(), {
|
|
10
|
+
appName: 'Mantiq',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const theme = ref<'light' | 'dark' | 'system'>('system')
|
|
14
|
+
const success = ref('')
|
|
15
|
+
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
const stored = localStorage.getItem('theme')
|
|
18
|
+
if (stored === 'light' || stored === 'dark') {
|
|
19
|
+
theme.value = stored
|
|
20
|
+
} else {
|
|
21
|
+
theme.value = 'system'
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function setTheme(value: 'light' | 'dark' | 'system') {
|
|
26
|
+
theme.value = value
|
|
27
|
+
if (value === 'system') {
|
|
28
|
+
localStorage.removeItem('theme')
|
|
29
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
30
|
+
document.documentElement.classList.toggle('dark', prefersDark)
|
|
31
|
+
} else {
|
|
32
|
+
localStorage.setItem('theme', value)
|
|
33
|
+
document.documentElement.classList.toggle('dark', value === 'dark')
|
|
34
|
+
}
|
|
35
|
+
success.value = 'Preferences saved.'
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<AccountLayout
|
|
41
|
+
:app-name="appName"
|
|
42
|
+
:current-user="currentUser"
|
|
43
|
+
:navigate="navigate"
|
|
44
|
+
active-path="/account/preferences"
|
|
45
|
+
title="Settings"
|
|
46
|
+
description="Manage your account settings."
|
|
47
|
+
>
|
|
48
|
+
<div class="rounded-xl border bg-card text-card-foreground shadow-sm">
|
|
49
|
+
<div class="p-6 pb-2">
|
|
50
|
+
<h3 class="text-lg font-semibold leading-none tracking-tight">Preferences</h3>
|
|
51
|
+
<p class="mt-1.5 text-sm text-muted-foreground">Customize your experience.</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="p-6 pt-4">
|
|
54
|
+
<div
|
|
55
|
+
v-if="success"
|
|
56
|
+
class="mb-4 rounded-md border border-green-500/30 bg-green-500/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"
|
|
57
|
+
>
|
|
58
|
+
{{ success }}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="space-y-4 max-w-md">
|
|
61
|
+
<div class="space-y-2">
|
|
62
|
+
<label class="text-sm font-medium leading-none">Theme</label>
|
|
63
|
+
<div class="flex gap-2">
|
|
64
|
+
<button
|
|
65
|
+
v-for="opt in (['light', 'dark', 'system'] as const)"
|
|
66
|
+
:key="opt"
|
|
67
|
+
class="inline-flex h-8 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors"
|
|
68
|
+
:class="theme === opt
|
|
69
|
+
? 'bg-primary text-primary-foreground shadow'
|
|
70
|
+
: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground'"
|
|
71
|
+
@click="setTheme(opt)"
|
|
72
|
+
>
|
|
73
|
+
{{ opt.charAt(0).toUpperCase() + opt.slice(1) }}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</AccountLayout>
|
|
81
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import AccountLayout from './layout.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
appName?: string
|
|
7
|
+
currentUser?: { name: string; email: string; role?: string } | null
|
|
8
|
+
navigate: (href: string) => void
|
|
9
|
+
}>(), {
|
|
10
|
+
appName: 'Mantiq',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const name = ref(props.currentUser?.name ?? '')
|
|
14
|
+
const email = ref(props.currentUser?.email ?? '')
|
|
15
|
+
const success = ref('')
|
|
16
|
+
const error = ref('')
|
|
17
|
+
|
|
18
|
+
async function handleSubmit() {
|
|
19
|
+
success.value = ''
|
|
20
|
+
error.value = ''
|
|
21
|
+
// Profile update would call an API endpoint
|
|
22
|
+
success.value = 'Profile updated successfully.'
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<AccountLayout
|
|
28
|
+
:app-name="appName"
|
|
29
|
+
:current-user="currentUser"
|
|
30
|
+
:navigate="navigate"
|
|
31
|
+
active-path="/account/profile"
|
|
32
|
+
title="Settings"
|
|
33
|
+
description="Manage your account settings."
|
|
34
|
+
>
|
|
35
|
+
<div class="rounded-xl border bg-card text-card-foreground shadow-sm">
|
|
36
|
+
<div class="p-6 pb-2">
|
|
37
|
+
<h3 class="text-lg font-semibold leading-none tracking-tight">Profile</h3>
|
|
38
|
+
<p class="mt-1.5 text-sm text-muted-foreground">Update your personal information.</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="p-6 pt-4">
|
|
41
|
+
<div
|
|
42
|
+
v-if="success"
|
|
43
|
+
class="mb-4 rounded-md border border-green-500/30 bg-green-500/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"
|
|
44
|
+
>
|
|
45
|
+
{{ success }}
|
|
46
|
+
</div>
|
|
47
|
+
<div
|
|
48
|
+
v-if="error"
|
|
49
|
+
class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
|
50
|
+
>
|
|
51
|
+
{{ error }}
|
|
52
|
+
</div>
|
|
53
|
+
<form class="space-y-4 max-w-md" @submit.prevent="handleSubmit">
|
|
54
|
+
<div class="space-y-2">
|
|
55
|
+
<label for="profile-name" class="text-sm font-medium leading-none">Name</label>
|
|
56
|
+
<input id="profile-name" v-model="name" required class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
57
|
+
</div>
|
|
58
|
+
<div class="space-y-2">
|
|
59
|
+
<label for="profile-email" class="text-sm font-medium leading-none">Email</label>
|
|
60
|
+
<input id="profile-email" v-model="email" type="email" required class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
|
|
61
|
+
</div>
|
|
62
|
+
<button type="submit" class="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90">
|
|
63
|
+
Save Changes
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</AccountLayout>
|
|
69
|
+
</template>
|