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,293 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { api } from '$lib/api'
|
|
3
|
+
import AuthenticatedLayout from '$lib/components/layout/authenticated-layout.svelte'
|
|
4
|
+
import Header from '$lib/components/layout/header.svelte'
|
|
5
|
+
import Main from '$lib/components/layout/main.svelte'
|
|
6
|
+
import {
|
|
7
|
+
MoreHorizontal,
|
|
8
|
+
UserPlus,
|
|
9
|
+
Mail,
|
|
10
|
+
Pencil,
|
|
11
|
+
Trash2,
|
|
12
|
+
ChevronLeft,
|
|
13
|
+
ChevronRight,
|
|
14
|
+
Search,
|
|
15
|
+
} from 'lucide-svelte'
|
|
16
|
+
|
|
17
|
+
export interface UserType {
|
|
18
|
+
id: number
|
|
19
|
+
name: string
|
|
20
|
+
email: string
|
|
21
|
+
status: string
|
|
22
|
+
created_at: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
appName = 'MantiqJS',
|
|
27
|
+
currentUser = null,
|
|
28
|
+
users: initialUsers = [],
|
|
29
|
+
navigate,
|
|
30
|
+
}: {
|
|
31
|
+
appName?: string
|
|
32
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
33
|
+
users?: UserType[]
|
|
34
|
+
navigate: (href: string) => void
|
|
35
|
+
[key: string]: any
|
|
36
|
+
} = $props()
|
|
37
|
+
|
|
38
|
+
// Mock data
|
|
39
|
+
const mockUsers: UserType[] = [
|
|
40
|
+
{ id: 1, name: 'Olivia Martin', email: 'olivia@example.com', status: 'active', created_at: '2024-01-15' },
|
|
41
|
+
{ id: 2, name: 'Jackson Lee', email: 'jackson@example.com', status: 'active', created_at: '2024-02-20' },
|
|
42
|
+
{ id: 3, name: 'Isabella Nguyen', email: 'isabella@example.com', status: 'active', created_at: '2024-02-28' },
|
|
43
|
+
{ id: 4, name: 'William Kim', email: 'william@example.com', status: 'active', created_at: '2024-03-05' },
|
|
44
|
+
{ id: 5, name: 'Sofia Davis', email: 'sofia@example.com', status: 'inactive', created_at: '2024-03-12' },
|
|
45
|
+
{ id: 6, name: 'Liam Johnson', email: 'liam@example.com', status: 'active', created_at: '2024-03-20' },
|
|
46
|
+
{ id: 7, name: 'Emma Wilson', email: 'emma@example.com', status: 'active', created_at: '2024-04-01' },
|
|
47
|
+
{ id: 8, name: 'Noah Brown', email: 'noah@example.com', status: 'inactive', created_at: '2024-04-10' },
|
|
48
|
+
{ id: 9, name: 'Ava Garcia', email: 'ava@example.com', status: 'active', created_at: '2024-04-18' },
|
|
49
|
+
{ id: 10, name: 'Ethan Martinez', email: 'ethan@example.com', status: 'active', created_at: '2024-05-02' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const getInitialUsers = (): UserType[] => {
|
|
53
|
+
if (initialUsers && initialUsers.length > 0) {
|
|
54
|
+
return initialUsers.map(u => ({ ...u, status: (u as any).status ?? 'active', created_at: u.created_at ?? '' }))
|
|
55
|
+
}
|
|
56
|
+
return mockUsers
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let users = $state<UserType[]>(getInitialUsers())
|
|
60
|
+
let loading = $state(false)
|
|
61
|
+
let search = $state('')
|
|
62
|
+
let debouncedSearch = $state('')
|
|
63
|
+
let page = $state(1)
|
|
64
|
+
let perPage = $state(10)
|
|
65
|
+
let total = $state(0)
|
|
66
|
+
|
|
67
|
+
// Action menus
|
|
68
|
+
let openMenuId = $state<number | null>(null)
|
|
69
|
+
|
|
70
|
+
// Debounce search
|
|
71
|
+
let debounceTimer: ReturnType<typeof setTimeout>
|
|
72
|
+
$effect(() => {
|
|
73
|
+
clearTimeout(debounceTimer)
|
|
74
|
+
debounceTimer = setTimeout(() => {
|
|
75
|
+
debouncedSearch = search
|
|
76
|
+
page = 1
|
|
77
|
+
}, 300)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Fetch from API
|
|
81
|
+
async function fetchUsers() {
|
|
82
|
+
loading = true
|
|
83
|
+
const params = new URLSearchParams({
|
|
84
|
+
page: String(page),
|
|
85
|
+
per_page: String(perPage),
|
|
86
|
+
sort: 'created_at',
|
|
87
|
+
dir: 'desc',
|
|
88
|
+
})
|
|
89
|
+
if (debouncedSearch) params.set('search', debouncedSearch)
|
|
90
|
+
|
|
91
|
+
const { ok, data } = await api(`/api/users?${params}`)
|
|
92
|
+
if (ok && data) {
|
|
93
|
+
users = (data.data as any[]).map((u: any) => ({
|
|
94
|
+
id: u.id,
|
|
95
|
+
name: u.name,
|
|
96
|
+
email: u.email,
|
|
97
|
+
status: u.status ?? 'active',
|
|
98
|
+
created_at: u.created_at ?? '',
|
|
99
|
+
}))
|
|
100
|
+
if (data.meta) {
|
|
101
|
+
total = data.meta.total
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
loading = false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$effect(() => {
|
|
108
|
+
void page, perPage, debouncedSearch
|
|
109
|
+
fetchUsers()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function getInitials(name: string): string {
|
|
113
|
+
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const totalPages = $derived(Math.max(1, Math.ceil(total / perPage)))
|
|
117
|
+
|
|
118
|
+
function handleClickOutside(e: MouseEvent) {
|
|
119
|
+
const target = e.target as HTMLElement
|
|
120
|
+
if (!target.closest('[data-action-menu]')) {
|
|
121
|
+
openMenuId = null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (openMenuId !== null) {
|
|
127
|
+
document.addEventListener('click', handleClickOutside, true)
|
|
128
|
+
return () => document.removeEventListener('click', handleClickOutside, true)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<AuthenticatedLayout
|
|
134
|
+
{currentUser}
|
|
135
|
+
{appName}
|
|
136
|
+
{navigate}
|
|
137
|
+
activePath="/users"
|
|
138
|
+
>
|
|
139
|
+
<Header fixed {navigate} />
|
|
140
|
+
|
|
141
|
+
<Main>
|
|
142
|
+
<div class="space-y-4">
|
|
143
|
+
<!-- Title row -->
|
|
144
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
145
|
+
<div>
|
|
146
|
+
<h2 class="text-2xl font-bold tracking-tight">User List</h2>
|
|
147
|
+
<p class="text-sm text-muted-foreground">
|
|
148
|
+
{total > 0 ? `${total} users total.` : 'Manage your users here.'}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="flex items-center gap-2">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
disabled
|
|
155
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
156
|
+
>
|
|
157
|
+
<Mail class="h-4 w-4" />
|
|
158
|
+
Invite User
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
163
|
+
>
|
|
164
|
+
<UserPlus class="h-4 w-4" />
|
|
165
|
+
Add User
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Search -->
|
|
171
|
+
<div class="relative max-w-sm">
|
|
172
|
+
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
173
|
+
<input
|
|
174
|
+
type="text"
|
|
175
|
+
placeholder="Filter users..."
|
|
176
|
+
bind:value={search}
|
|
177
|
+
class="h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Table -->
|
|
182
|
+
<div class="rounded-lg border border-border">
|
|
183
|
+
<table class="w-full text-sm">
|
|
184
|
+
<thead>
|
|
185
|
+
<tr class="border-b border-border bg-muted/50">
|
|
186
|
+
<th class="px-4 py-3 text-left font-medium text-muted-foreground">User</th>
|
|
187
|
+
<th class="hidden px-4 py-3 text-left font-medium text-muted-foreground sm:table-cell">Status</th>
|
|
188
|
+
<th class="hidden px-4 py-3 text-left font-medium text-muted-foreground lg:table-cell">Created</th>
|
|
189
|
+
<th class="w-10 px-4 py-3"></th>
|
|
190
|
+
</tr>
|
|
191
|
+
</thead>
|
|
192
|
+
<tbody>
|
|
193
|
+
{#if loading}
|
|
194
|
+
<tr>
|
|
195
|
+
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">Loading...</td>
|
|
196
|
+
</tr>
|
|
197
|
+
{:else if users.length === 0}
|
|
198
|
+
<tr>
|
|
199
|
+
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
|
|
200
|
+
{search ? 'No users match the current filters.' : 'No users found.'}
|
|
201
|
+
</td>
|
|
202
|
+
</tr>
|
|
203
|
+
{:else}
|
|
204
|
+
{#each users as user}
|
|
205
|
+
<tr class="border-b border-border last:border-0 hover:bg-muted/50 transition-colors">
|
|
206
|
+
<td class="px-4 py-3">
|
|
207
|
+
<div class="flex items-center gap-3">
|
|
208
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold">
|
|
209
|
+
{getInitials(user.name)}
|
|
210
|
+
</div>
|
|
211
|
+
<div class="min-w-0">
|
|
212
|
+
<div class="truncate text-sm font-medium">{user.name}</div>
|
|
213
|
+
<div class="truncate text-xs text-muted-foreground">{user.email}</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</td>
|
|
217
|
+
<td class="hidden px-4 py-3 sm:table-cell">
|
|
218
|
+
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium capitalize {user.status === 'active' ? 'bg-primary text-primary-foreground' : 'border border-border text-muted-foreground'}">
|
|
219
|
+
{user.status}
|
|
220
|
+
</span>
|
|
221
|
+
</td>
|
|
222
|
+
<td class="hidden px-4 py-3 text-sm text-muted-foreground lg:table-cell">
|
|
223
|
+
{user.created_at
|
|
224
|
+
? new Date(user.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
225
|
+
: '\u2014'}
|
|
226
|
+
</td>
|
|
227
|
+
<td class="px-4 py-3">
|
|
228
|
+
<div class="relative" data-action-menu>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onclick={() => openMenuId = openMenuId === user.id ? null : user.id}
|
|
232
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent"
|
|
233
|
+
>
|
|
234
|
+
<MoreHorizontal class="h-4 w-4" />
|
|
235
|
+
<span class="sr-only">Open menu</span>
|
|
236
|
+
</button>
|
|
237
|
+
{#if openMenuId === user.id}
|
|
238
|
+
<div class="absolute right-0 top-full z-50 mt-1 w-40 rounded-md border border-border bg-popover p-1 shadow-lg">
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
242
|
+
onclick={() => openMenuId = null}
|
|
243
|
+
>
|
|
244
|
+
<Pencil class="h-4 w-4" />
|
|
245
|
+
Edit
|
|
246
|
+
</button>
|
|
247
|
+
<div class="my-1 h-px bg-border"></div>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive hover:bg-accent transition-colors"
|
|
251
|
+
onclick={() => openMenuId = null}
|
|
252
|
+
>
|
|
253
|
+
<Trash2 class="h-4 w-4" />
|
|
254
|
+
Delete
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
</td>
|
|
260
|
+
</tr>
|
|
261
|
+
{/each}
|
|
262
|
+
{/if}
|
|
263
|
+
</tbody>
|
|
264
|
+
</table>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Pagination -->
|
|
268
|
+
{#if total > perPage}
|
|
269
|
+
<div class="flex items-center justify-end gap-2">
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
disabled={page <= 1}
|
|
273
|
+
onclick={() => page = Math.max(1, page - 1)}
|
|
274
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
275
|
+
>
|
|
276
|
+
<ChevronLeft class="h-4 w-4" />
|
|
277
|
+
</button>
|
|
278
|
+
<span class="text-sm text-muted-foreground">
|
|
279
|
+
Page {page} of {totalPages}
|
|
280
|
+
</span>
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
disabled={page >= totalPages}
|
|
284
|
+
onclick={() => page = Math.min(totalPages, page + 1)}
|
|
285
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
286
|
+
>
|
|
287
|
+
<ChevronRight class="h-4 w-4" />
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
292
|
+
</Main>
|
|
293
|
+
</AuthenticatedLayout>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AuthenticatedLayout from '$lib/components/layout/authenticated-layout.svelte'
|
|
3
|
+
import Header from '$lib/components/layout/header.svelte'
|
|
4
|
+
import Main from '$lib/components/layout/main.svelte'
|
|
5
|
+
import { User, Lock, Palette } from 'lucide-svelte'
|
|
6
|
+
import type { Component, Snippet } from 'svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
children,
|
|
10
|
+
appName,
|
|
11
|
+
currentUser,
|
|
12
|
+
navigate,
|
|
13
|
+
activePath,
|
|
14
|
+
}: {
|
|
15
|
+
children: Snippet
|
|
16
|
+
appName?: string
|
|
17
|
+
currentUser?: any
|
|
18
|
+
navigate: (href: string) => void
|
|
19
|
+
activePath: string
|
|
20
|
+
} = $props()
|
|
21
|
+
|
|
22
|
+
const sidebarNav: { title: string; href: string; icon: Component }[] = [
|
|
23
|
+
{ title: 'Profile', href: '/account/profile', icon: User },
|
|
24
|
+
{ title: 'Security', href: '/account/security', icon: Lock },
|
|
25
|
+
{ title: 'Preferences', href: '/account/preferences', icon: Palette },
|
|
26
|
+
]
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<AuthenticatedLayout {currentUser} {appName} {navigate} {activePath}>
|
|
30
|
+
<Header fixed {navigate}>
|
|
31
|
+
<h1 class="text-lg font-semibold">Settings</h1>
|
|
32
|
+
</Header>
|
|
33
|
+
<Main>
|
|
34
|
+
<div class="space-y-0.5">
|
|
35
|
+
<h2 class="text-2xl font-bold tracking-tight">Settings</h2>
|
|
36
|
+
<p class="text-muted-foreground">
|
|
37
|
+
Manage your account settings and preferences.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="my-6 h-px bg-border"></div>
|
|
41
|
+
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
|
42
|
+
<aside class="lg:w-48">
|
|
43
|
+
<nav class="flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1 overflow-x-auto">
|
|
44
|
+
{#each sidebarNav as item}
|
|
45
|
+
{@const Icon = item.icon}
|
|
46
|
+
<a
|
|
47
|
+
href={item.href}
|
|
48
|
+
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground {activePath === item.href ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'}"
|
|
49
|
+
>
|
|
50
|
+
<Icon class="w-4 h-4" />
|
|
51
|
+
{item.title}
|
|
52
|
+
</a>
|
|
53
|
+
{/each}
|
|
54
|
+
</nav>
|
|
55
|
+
</aside>
|
|
56
|
+
<div class="flex-1 lg:max-w-2xl">
|
|
57
|
+
{@render children()}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</Main>
|
|
61
|
+
</AuthenticatedLayout>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AccountLayout from './Layout.svelte'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
appName,
|
|
6
|
+
currentUser = null,
|
|
7
|
+
navigate,
|
|
8
|
+
}: {
|
|
9
|
+
appName?: string
|
|
10
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
11
|
+
navigate: (href: string) => void
|
|
12
|
+
[key: string]: any
|
|
13
|
+
} = $props()
|
|
14
|
+
|
|
15
|
+
let theme = $state<'light' | 'dark' | 'system'>(
|
|
16
|
+
typeof localStorage !== 'undefined'
|
|
17
|
+
? (localStorage.getItem('theme') as any) ?? 'system'
|
|
18
|
+
: 'system'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
function applyTheme(t: 'light' | 'dark' | 'system') {
|
|
22
|
+
theme = t
|
|
23
|
+
if (t === 'system') {
|
|
24
|
+
localStorage.removeItem('theme')
|
|
25
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
26
|
+
document.documentElement.classList.toggle('dark', prefersDark)
|
|
27
|
+
} else {
|
|
28
|
+
localStorage.setItem('theme', t)
|
|
29
|
+
document.documentElement.classList.toggle('dark', t === 'dark')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const themes: { value: 'light' | 'dark' | 'system'; label: string; description: string }[] = [
|
|
34
|
+
{ value: 'light', label: 'Light', description: 'A clean, bright appearance.' },
|
|
35
|
+
{ value: 'dark', label: 'Dark', description: 'Easy on the eyes in low light.' },
|
|
36
|
+
{ value: 'system', label: 'System', description: 'Follows your operating system setting.' },
|
|
37
|
+
]
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<AccountLayout {appName} {currentUser} {navigate} activePath="/account/preferences">
|
|
41
|
+
<div class="space-y-8">
|
|
42
|
+
<div>
|
|
43
|
+
<h3 class="text-lg font-medium">Preferences</h3>
|
|
44
|
+
<p class="text-sm text-muted-foreground">
|
|
45
|
+
Customize the appearance and behavior of the app.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Theme -->
|
|
50
|
+
<div class="space-y-3">
|
|
51
|
+
<label class="text-sm font-medium leading-none">Theme</label>
|
|
52
|
+
<p class="text-sm text-muted-foreground">Select your preferred theme.</p>
|
|
53
|
+
<div class="grid grid-cols-3 gap-3">
|
|
54
|
+
{#each themes as t}
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onclick={() => applyTheme(t.value)}
|
|
58
|
+
class="rounded-lg border p-4 text-left text-sm transition-colors hover:bg-accent {theme === t.value ? 'border-foreground' : 'border-border'}"
|
|
59
|
+
>
|
|
60
|
+
<div class="font-medium">{t.label}</div>
|
|
61
|
+
<div class="text-xs text-muted-foreground mt-1">{t.description}</div>
|
|
62
|
+
</button>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Language -->
|
|
68
|
+
<div class="space-y-3">
|
|
69
|
+
<label class="text-sm font-medium leading-none">Language</label>
|
|
70
|
+
<p class="text-sm text-muted-foreground">Select the language for the interface.</p>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
disabled
|
|
74
|
+
class="inline-flex items-center justify-between w-48 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
75
|
+
>
|
|
76
|
+
English
|
|
77
|
+
<span class="text-xs text-muted-foreground">Default</span>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</AccountLayout>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AccountLayout from './Layout.svelte'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
appName,
|
|
6
|
+
currentUser = null,
|
|
7
|
+
navigate,
|
|
8
|
+
}: {
|
|
9
|
+
appName?: string
|
|
10
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
11
|
+
navigate: (href: string) => void
|
|
12
|
+
[key: string]: any
|
|
13
|
+
} = $props()
|
|
14
|
+
|
|
15
|
+
let name = $state((() => currentUser?.name ?? '')())
|
|
16
|
+
let email = $state((() => currentUser?.email ?? '')())
|
|
17
|
+
let saving = $state(false)
|
|
18
|
+
let saved = $state(false)
|
|
19
|
+
|
|
20
|
+
async function handleSave(e: SubmitEvent) {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
saving = true
|
|
23
|
+
await new Promise(r => setTimeout(r, 500))
|
|
24
|
+
saving = false
|
|
25
|
+
saved = true
|
|
26
|
+
setTimeout(() => saved = false, 3000)
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<AccountLayout {appName} {currentUser} {navigate} activePath="/account/profile">
|
|
31
|
+
<div class="space-y-6">
|
|
32
|
+
<div>
|
|
33
|
+
<h3 class="text-lg font-medium">Profile</h3>
|
|
34
|
+
<p class="text-sm text-muted-foreground">
|
|
35
|
+
This is how others will see you on the site.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<form onsubmit={handleSave} class="space-y-6">
|
|
39
|
+
<div class="space-y-2">
|
|
40
|
+
<label for="name" class="text-sm font-medium leading-none">Name</label>
|
|
41
|
+
<input
|
|
42
|
+
id="name"
|
|
43
|
+
bind:value={name}
|
|
44
|
+
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:outline-none focus:ring-1 focus:ring-ring"
|
|
45
|
+
/>
|
|
46
|
+
<p class="text-xs text-muted-foreground">
|
|
47
|
+
This is your public display name.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="space-y-2">
|
|
51
|
+
<label for="email" class="text-sm font-medium leading-none">Email</label>
|
|
52
|
+
<input
|
|
53
|
+
id="email"
|
|
54
|
+
type="email"
|
|
55
|
+
bind:value={email}
|
|
56
|
+
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:outline-none focus:ring-1 focus:ring-ring"
|
|
57
|
+
/>
|
|
58
|
+
<p class="text-xs text-muted-foreground">
|
|
59
|
+
Your email address is used for notifications.
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex items-center gap-3">
|
|
63
|
+
<button
|
|
64
|
+
type="submit"
|
|
65
|
+
disabled={saving}
|
|
66
|
+
class="inline-flex items-center justify-center 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"
|
|
67
|
+
>
|
|
68
|
+
{saving ? 'Saving...' : 'Update profile'}
|
|
69
|
+
</button>
|
|
70
|
+
{#if saved}
|
|
71
|
+
<span class="text-sm text-muted-foreground">Saved.</span>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
</form>
|
|
75
|
+
</div>
|
|
76
|
+
</AccountLayout>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AccountLayout from './Layout.svelte'
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
appName,
|
|
6
|
+
currentUser = null,
|
|
7
|
+
navigate,
|
|
8
|
+
}: {
|
|
9
|
+
appName?: string
|
|
10
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
11
|
+
navigate: (href: string) => void
|
|
12
|
+
[key: string]: any
|
|
13
|
+
} = $props()
|
|
14
|
+
|
|
15
|
+
let currentPassword = $state('')
|
|
16
|
+
let newPassword = $state('')
|
|
17
|
+
let confirmPassword = $state('')
|
|
18
|
+
let saving = $state(false)
|
|
19
|
+
let saved = $state(false)
|
|
20
|
+
let error = $state('')
|
|
21
|
+
|
|
22
|
+
async function handleSave(e: SubmitEvent) {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
error = ''
|
|
25
|
+
if (newPassword.length < 8) { error = 'Password must be at least 8 characters.'; return }
|
|
26
|
+
if (newPassword !== confirmPassword) { error = 'Passwords do not match.'; return }
|
|
27
|
+
saving = true
|
|
28
|
+
await new Promise(r => setTimeout(r, 500))
|
|
29
|
+
saving = false
|
|
30
|
+
saved = true
|
|
31
|
+
currentPassword = ''
|
|
32
|
+
newPassword = ''
|
|
33
|
+
confirmPassword = ''
|
|
34
|
+
setTimeout(() => saved = false, 3000)
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<AccountLayout {appName} {currentUser} {navigate} activePath="/account/security">
|
|
39
|
+
<div class="space-y-8">
|
|
40
|
+
<div>
|
|
41
|
+
<h3 class="text-lg font-medium">Security</h3>
|
|
42
|
+
<p class="text-sm text-muted-foreground">
|
|
43
|
+
Manage your password and security settings.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Change Password -->
|
|
48
|
+
<form onsubmit={handleSave} class="space-y-4">
|
|
49
|
+
<div class="space-y-2">
|
|
50
|
+
<label for="current" class="text-sm font-medium leading-none">Current password</label>
|
|
51
|
+
<input
|
|
52
|
+
id="current"
|
|
53
|
+
type="password"
|
|
54
|
+
bind:value={currentPassword}
|
|
55
|
+
autocomplete="current-password"
|
|
56
|
+
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:outline-none focus:ring-1 focus:ring-ring"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="space-y-2">
|
|
60
|
+
<label for="new" class="text-sm font-medium leading-none">New password</label>
|
|
61
|
+
<input
|
|
62
|
+
id="new"
|
|
63
|
+
type="password"
|
|
64
|
+
bind:value={newPassword}
|
|
65
|
+
autocomplete="new-password"
|
|
66
|
+
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:outline-none focus:ring-1 focus:ring-ring"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="space-y-2">
|
|
70
|
+
<label for="confirm" class="text-sm font-medium leading-none">Confirm password</label>
|
|
71
|
+
<input
|
|
72
|
+
id="confirm"
|
|
73
|
+
type="password"
|
|
74
|
+
bind:value={confirmPassword}
|
|
75
|
+
autocomplete="new-password"
|
|
76
|
+
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:outline-none focus:ring-1 focus:ring-ring"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
{#if error}
|
|
80
|
+
<p class="text-sm text-destructive">{error}</p>
|
|
81
|
+
{/if}
|
|
82
|
+
<div class="flex items-center gap-3">
|
|
83
|
+
<button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={saving}
|
|
86
|
+
class="inline-flex items-center justify-center 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"
|
|
87
|
+
>
|
|
88
|
+
{saving ? 'Saving...' : 'Update password'}
|
|
89
|
+
</button>
|
|
90
|
+
{#if saved}
|
|
91
|
+
<span class="text-sm text-muted-foreground">Password updated.</span>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
|
|
96
|
+
<!-- 2FA -->
|
|
97
|
+
<div class="space-y-3">
|
|
98
|
+
<div>
|
|
99
|
+
<h4 class="text-sm font-medium">Two-factor authentication</h4>
|
|
100
|
+
<p class="text-sm text-muted-foreground">
|
|
101
|
+
Add an additional layer of security to your account.
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="rounded-xl border border-border bg-card text-card-foreground shadow-sm">
|
|
105
|
+
<div class="p-6 pb-3">
|
|
106
|
+
<h3 class="text-sm font-semibold leading-none tracking-tight">Authenticator app</h3>
|
|
107
|
+
<p class="text-sm text-muted-foreground mt-1.5">
|
|
108
|
+
Use an authenticator app to generate one-time codes.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="p-6 pt-0">
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
disabled
|
|
115
|
+
class="inline-flex items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
|
|
116
|
+
>
|
|
117
|
+
Enable 2FA
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Delete Account -->
|
|
124
|
+
<div class="space-y-3">
|
|
125
|
+
<div>
|
|
126
|
+
<h4 class="text-sm font-medium">Delete account</h4>
|
|
127
|
+
<p class="text-sm text-muted-foreground">
|
|
128
|
+
Permanently remove your account and all associated data. This action cannot be undone.
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
disabled
|
|
134
|
+
class="inline-flex items-center justify-center rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground shadow hover:bg-destructive/90 disabled:pointer-events-none disabled:opacity-50"
|
|
135
|
+
>
|
|
136
|
+
Delete account
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</AccountLayout>
|