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.
Files changed (220) hide show
  1. package/package.json +2 -1
  2. package/skeleton/.env.example +64 -0
  3. package/skeleton/README.md +46 -0
  4. package/skeleton/app/Console/Commands/.gitkeep +0 -0
  5. package/skeleton/app/Enums/UserStatus.ts +7 -0
  6. package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
  7. package/skeleton/app/Http/Middleware/.gitkeep +0 -0
  8. package/skeleton/app/Models/User.ts +7 -0
  9. package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
  10. package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
  11. package/skeleton/bootstrap/.gitkeep +0 -0
  12. package/skeleton/config/ai.ts +51 -0
  13. package/skeleton/config/app.ts +108 -0
  14. package/skeleton/config/auth.ts +51 -0
  15. package/skeleton/config/broadcasting.ts +93 -0
  16. package/skeleton/config/cache.ts +61 -0
  17. package/skeleton/config/cors.ts +77 -0
  18. package/skeleton/config/database.ts +120 -0
  19. package/skeleton/config/filesystem.ts +58 -0
  20. package/skeleton/config/hashing.ts +47 -0
  21. package/skeleton/config/heartbeat.ts +112 -0
  22. package/skeleton/config/logging.ts +58 -0
  23. package/skeleton/config/mail.ts +93 -0
  24. package/skeleton/config/notify.ts +141 -0
  25. package/skeleton/config/queue.ts +59 -0
  26. package/skeleton/config/search.ts +96 -0
  27. package/skeleton/config/services.ts +110 -0
  28. package/skeleton/config/session.ts +84 -0
  29. package/skeleton/config/vite.ts +33 -0
  30. package/skeleton/database/factories/.gitkeep +0 -0
  31. package/skeleton/database/migrations/001_create_users_table.ts +19 -0
  32. package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
  33. package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
  34. package/skeleton/index.ts +20 -0
  35. package/skeleton/mantiq.ts +8 -0
  36. package/skeleton/package.json +34 -0
  37. package/skeleton/public/.gitkeep +0 -0
  38. package/skeleton/routes/api.ts +8 -0
  39. package/skeleton/routes/channels.ts +23 -0
  40. package/skeleton/routes/console.ts +24 -0
  41. package/skeleton/routes/web.ts +6 -0
  42. package/skeleton/storage/cache/.gitkeep +0 -0
  43. package/skeleton/storage/framework/.gitkeep +0 -0
  44. package/skeleton/tests/feature/api.test.ts +14 -0
  45. package/skeleton/tests/feature/home.test.ts +17 -0
  46. package/skeleton/tests/unit/example.test.ts +11 -0
  47. package/skeleton/tsconfig.json +27 -0
  48. package/src/index.ts +289 -25
  49. package/src/templates.ts +141 -945
  50. package/src/terminal.ts +64 -0
  51. package/stubs/api-only/routes/api.ts.stub +24 -0
  52. package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
  53. package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  54. package/stubs/auth/api/routes/api.ts.stub +24 -0
  55. package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
  56. package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  57. package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  58. package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
  59. package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
  60. package/stubs/auth/web/routes/web.ts.stub +25 -0
  61. package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
  62. package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
  63. package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
  64. package/stubs/auth/web/vue/src/App.vue.stub +74 -0
  65. package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
  66. package/stubs/manifest.json +630 -2
  67. package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
  68. package/stubs/noauth/app/Models/User.ts.stub +5 -0
  69. package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
  70. package/stubs/noauth/routes/api.ts.stub +16 -0
  71. package/stubs/noauth/routes/web.ts.stub +15 -0
  72. package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
  73. package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
  74. package/stubs/noauth/vue/src/App.vue.stub +62 -0
  75. package/stubs/noauth/vue/src/pages.ts.stub +7 -0
  76. package/stubs/react/src/App.tsx.stub +4 -2
  77. package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
  78. package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
  79. package/stubs/react/src/lib/api.ts.stub +30 -6
  80. package/stubs/react/src/pages/Login.tsx.stub +3 -3
  81. package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
  82. package/stubs/react/vite.config.ts.stub +26 -3
  83. package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  84. package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
  85. package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
  86. package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
  87. package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  88. package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  89. package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
  90. package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
  91. package/stubs/shared/config/app.ts.stub +36 -0
  92. package/stubs/shared/config/vite.ts.stub +8 -0
  93. package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
  94. package/stubs/shared/routes/api.ts.stub +12 -102
  95. package/stubs/shared/routes/web.ts.stub +5 -3
  96. package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
  97. package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
  98. package/stubs/svelte/src/App.svelte.stub +1 -1
  99. package/stubs/svelte/src/lib/api.ts.stub +30 -6
  100. package/stubs/svelte/src/main.ts.stub +3 -1
  101. package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
  102. package/stubs/svelte/vite.config.ts.stub +20 -1
  103. package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  104. package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
  105. package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
  106. package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
  107. package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
  108. package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
  109. package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
  110. package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
  111. package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
  112. package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
  113. package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
  114. package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
  115. package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
  116. package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
  117. package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
  118. package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
  119. package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
  120. package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
  121. package/stubs/tailwind-only/react/src/style.css.stub +14 -0
  122. package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
  123. package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
  124. package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
  125. package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
  126. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
  127. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
  128. package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
  129. package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
  130. package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
  131. package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
  132. package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
  133. package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
  134. package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
  135. package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
  136. package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
  137. package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
  138. package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
  139. package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
  140. package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
  141. package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
  142. package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
  143. package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
  144. package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
  145. package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
  146. package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
  147. package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
  148. package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
  149. package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
  150. package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
  151. package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
  152. package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
  153. package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
  154. package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
  155. package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
  156. package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
  157. package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
  158. package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
  159. package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
  160. package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  161. package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  162. package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
  163. package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
  164. package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
  165. package/stubs/themes/corporate/react/src/style.css.stub +135 -0
  166. package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
  167. package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
  168. package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
  169. package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
  170. package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
  171. package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
  172. package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
  173. package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
  174. package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  175. package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  176. package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
  177. package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
  178. package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
  179. package/stubs/themes/minimal/react/src/style.css.stub +142 -0
  180. package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
  181. package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
  182. package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
  183. package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
  184. package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
  185. package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
  186. package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
  187. package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
  188. package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  189. package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  190. package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
  191. package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
  192. package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
  193. package/stubs/themes/starter/react/src/style.css.stub +141 -0
  194. package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
  195. package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
  196. package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
  197. package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
  198. package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
  199. package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
  200. package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
  201. package/stubs/themes/starter/vue/src/style.css.stub +141 -0
  202. package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
  203. package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  204. package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
  205. package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
  206. package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
  207. package/stubs/themes/workspace/react/src/style.css.stub +138 -0
  208. package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
  209. package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
  210. package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
  211. package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
  212. package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
  213. package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
  214. package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
  215. package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
  216. package/stubs/vue/src/App.vue.stub +2 -1
  217. package/stubs/vue/src/lib/api.ts.stub +30 -6
  218. package/stubs/vue/src/main.ts.stub +3 -1
  219. package/stubs/vue/src/pages/Login.vue.stub +3 -3
  220. package/stubs/vue/vite.config.ts.stub +20 -1
@@ -0,0 +1,85 @@
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 currentPassword = ref('')
14
+ const newPassword = ref('')
15
+ const confirmPassword = ref('')
16
+ const success = ref('')
17
+ const error = ref('')
18
+
19
+ async function handleSubmit() {
20
+ success.value = ''
21
+ error.value = ''
22
+ if (newPassword.value !== confirmPassword.value) {
23
+ error.value = 'Passwords do not match.'
24
+ return
25
+ }
26
+ if (newPassword.value.length < 6) {
27
+ error.value = 'Password must be at least 6 characters.'
28
+ return
29
+ }
30
+ // Password change would call an API endpoint
31
+ success.value = 'Password updated successfully.'
32
+ currentPassword.value = ''
33
+ newPassword.value = ''
34
+ confirmPassword.value = ''
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <AccountLayout
40
+ :app-name="appName"
41
+ :current-user="currentUser"
42
+ :navigate="navigate"
43
+ active-path="/account/security"
44
+ title="Settings"
45
+ description="Manage your account settings."
46
+ >
47
+ <div class="rounded-xl border bg-card text-card-foreground shadow-sm">
48
+ <div class="p-6 pb-2">
49
+ <h3 class="text-lg font-semibold leading-none tracking-tight">Security</h3>
50
+ <p class="mt-1.5 text-sm text-muted-foreground">Change your password.</p>
51
+ </div>
52
+ <div class="p-6 pt-4">
53
+ <div
54
+ v-if="success"
55
+ 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"
56
+ >
57
+ {{ success }}
58
+ </div>
59
+ <div
60
+ v-if="error"
61
+ class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
62
+ >
63
+ {{ error }}
64
+ </div>
65
+ <form class="space-y-4 max-w-md" @submit.prevent="handleSubmit">
66
+ <div class="space-y-2">
67
+ <label for="current-password" class="text-sm font-medium leading-none">Current Password</label>
68
+ <input id="current-password" v-model="currentPassword" type="password" 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" />
69
+ </div>
70
+ <div class="space-y-2">
71
+ <label for="new-password" class="text-sm font-medium leading-none">New Password</label>
72
+ <input id="new-password" v-model="newPassword" 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" />
73
+ </div>
74
+ <div class="space-y-2">
75
+ <label for="confirm-password" class="text-sm font-medium leading-none">Confirm New Password</label>
76
+ <input id="confirm-password" v-model="confirmPassword" type="password" 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" />
77
+ </div>
78
+ <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">
79
+ Update Password
80
+ </button>
81
+ </form>
82
+ </div>
83
+ </div>
84
+ </AccountLayout>
85
+ </template>
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+ @custom-variant dark (&:where(.dark, .dark *));
3
+
4
+ @keyframes fadeUp {
5
+ from { opacity: 0; transform: translateY(8px); }
6
+ to { opacity: 1; transform: translateY(0); }
7
+ }
8
+ .animate-fade-up { animation: fadeUp 0.4s ease-out; }
9
+
10
+ @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring);
11
+ }
12
+
13
+ :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0);
14
+ }
15
+
16
+ .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0);
17
+ }
18
+
19
+ @layer base {
20
+ * {
21
+ @apply border-border outline-ring/50;
22
+ }
23
+ body {
24
+ @apply bg-background text-foreground;
25
+ }
26
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ Sidebar,
3
+ SidebarContent,
4
+ SidebarFooter,
5
+ SidebarHeader,
6
+ SidebarMenu,
7
+ SidebarMenuButton,
8
+ SidebarMenuItem,
9
+ SidebarRail,
10
+ } from '@/components/ui/sidebar'
11
+ import { NavGroup } from './nav-group'
12
+ import { NavUser, type NavUserProps } from './nav-user'
13
+ import { sidebarData } from './sidebar-data'
14
+ import { Command } from 'lucide-react'
15
+
16
+ export interface AppSidebarProps {
17
+ user: NavUserProps['user']
18
+ appName: string
19
+ activePath: string
20
+ navigate: (href: string) => void
21
+ onLogout: () => void
22
+ }
23
+
24
+ export function AppSidebar({
25
+ user,
26
+ appName,
27
+ activePath,
28
+ navigate,
29
+ onLogout,
30
+ }: AppSidebarProps) {
31
+ return (
32
+ <Sidebar variant="floating" collapsible="icon">
33
+ <SidebarHeader>
34
+ <SidebarMenu>
35
+ <SidebarMenuItem>
36
+ <SidebarMenuButton
37
+ size="lg"
38
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
39
+ onClick={() => navigate('/dashboard')}
40
+ tooltip={appName}
41
+ >
42
+ <div className="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
43
+ <Command className="size-4" />
44
+ </div>
45
+ <div className="grid flex-1 text-left text-sm leading-tight">
46
+ <span className="truncate font-semibold">{appName}</span>
47
+ <span className="truncate text-xs text-muted-foreground">
48
+ Admin Panel
49
+ </span>
50
+ </div>
51
+ </SidebarMenuButton>
52
+ </SidebarMenuItem>
53
+ </SidebarMenu>
54
+ </SidebarHeader>
55
+
56
+ <SidebarContent>
57
+ {sidebarData.map((group) => (
58
+ <NavGroup
59
+ key={group.title}
60
+ group={group}
61
+ activePath={activePath}
62
+ navigate={navigate}
63
+ />
64
+ ))}
65
+ </SidebarContent>
66
+
67
+ <SidebarFooter>
68
+ <NavUser user={user} navigate={navigate} onLogout={onLogout} />
69
+ </SidebarFooter>
70
+
71
+ <SidebarRail />
72
+ </Sidebar>
73
+ )
74
+ }
@@ -0,0 +1,42 @@
1
+ import { useCallback } from 'react'
2
+ import { post } from '@/lib/api'
3
+ import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
4
+ import { AppSidebar } from './app-sidebar'
5
+
6
+ interface AuthenticatedLayoutProps {
7
+ children: React.ReactNode
8
+ currentUser?: { name: string; email: string; role?: string } | null
9
+ appName?: string
10
+ navigate: (href: string) => void
11
+ activePath: string
12
+ }
13
+
14
+ export function AuthenticatedLayout({
15
+ children,
16
+ currentUser,
17
+ appName = 'Mantiq',
18
+ navigate,
19
+ activePath,
20
+ }: AuthenticatedLayoutProps) {
21
+ const handleLogout = useCallback(async () => {
22
+ await post('/logout', {})
23
+ navigate('/login')
24
+ }, [navigate])
25
+
26
+ const user = currentUser ?? { name: 'User', email: 'user@example.com' }
27
+
28
+ return (
29
+ <SidebarProvider defaultOpen={true}>
30
+ <AppSidebar
31
+ user={user}
32
+ appName={appName}
33
+ activePath={activePath}
34
+ navigate={navigate}
35
+ onLogout={handleLogout}
36
+ />
37
+ <SidebarInset>
38
+ {children}
39
+ </SidebarInset>
40
+ </SidebarProvider>
41
+ )
42
+ }
@@ -0,0 +1,310 @@
1
+ import { useState } from 'react'
2
+ import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
3
+ import { Header } from '@/components/layout/header'
4
+ import { Main } from '@/components/layout/main'
5
+ import { TopNav } from '@/components/layout/top-nav'
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardHeader,
11
+ CardTitle,
12
+ CardDescription,
13
+ } from '@/components/ui/card'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import { Separator } from '@/components/ui/separator'
16
+ import {
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableHead,
21
+ TableHeader,
22
+ TableRow,
23
+ } from '@/components/ui/table'
24
+ import {
25
+ TrendingUp,
26
+ TrendingDown,
27
+ ArrowUpRight,
28
+ ArrowDownRight,
29
+ CreditCard,
30
+ DollarSign,
31
+ Users,
32
+ Activity,
33
+ } from 'lucide-react'
34
+
35
+ interface DashboardProps {
36
+ appName?: string
37
+ currentUser?: { id: number; name: string; email: string; role: string } | null
38
+ navigate: (href: string) => void
39
+ [key: string]: any
40
+ }
41
+
42
+ const topNav = [
43
+ { title: 'Overview', href: '/dashboard', isActive: true },
44
+ { title: 'Sales', href: '/dashboard', isActive: false, disabled: true },
45
+ { title: 'Tickets', href: '/dashboard', isActive: false, disabled: true },
46
+ { title: 'Performance', href: '/dashboard', isActive: false, disabled: true },
47
+ ]
48
+
49
+ const transactions = [
50
+ { customer: 'Olivia Martin', email: 'olivia@email.com', amount: '$1,999.00', status: 'Completed', date: 'Mar 15, 2025' },
51
+ { customer: 'Jackson Lee', email: 'jackson@email.com', amount: '$39.00', status: 'Completed', date: 'Mar 14, 2025' },
52
+ { customer: 'Isabella Nguyen', email: 'isabella@email.com', amount: '$299.00', status: 'Pending', date: 'Mar 14, 2025' },
53
+ { customer: 'William Kim', email: 'will@email.com', amount: '$99.00', status: 'Failed', date: 'Mar 13, 2025' },
54
+ { customer: 'Sofia Davis', email: 'sofia@email.com', amount: '$39.00', status: 'Completed', date: 'Mar 13, 2025' },
55
+ ]
56
+
57
+ function statusColor(status: string) {
58
+ if (status === 'Completed') return 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/20'
59
+ if (status === 'Pending') return 'bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/20'
60
+ return 'bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/20'
61
+ }
62
+
63
+ function RevenueChart() {
64
+ const data = [1200, 2100, 1800, 3200, 2800, 4100, 3600, 5200, 4800, 5800, 5100, 6200, 5600]
65
+ const maxVal = Math.max(...data)
66
+ const svgW = 700
67
+ const svgH = 220
68
+ const padL = 40
69
+ const padR = 10
70
+ const padT = 10
71
+ const padB = 30
72
+ const chartW = svgW - padL - padR
73
+ const chartH = svgH - padT - padB
74
+ const stepX = chartW / (data.length - 1)
75
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
76
+ const yLabels = ['$0', '$2k', '$4k', '$6k']
77
+
78
+ const points = data.map((val, i) => ({
79
+ x: padL + i * stepX,
80
+ y: padT + chartH - (val / maxVal) * chartH,
81
+ }))
82
+
83
+ const linePath = points.map((p, i) => {
84
+ if (i === 0) return `M ${p.x} ${p.y}`
85
+ const prev = points[i - 1]
86
+ const cpx1 = prev.x + stepX * 0.4
87
+ const cpx2 = p.x - stepX * 0.4
88
+ return `C ${cpx1} ${prev.y}, ${cpx2} ${p.y}, ${p.x} ${p.y}`
89
+ }).join(' ')
90
+
91
+ const areaPath = `${linePath} L ${points[points.length - 1].x} ${padT + chartH} L ${points[0].x} ${padT + chartH} Z`
92
+
93
+ return (
94
+ <svg viewBox={`0 0 ${svgW} ${svgH}`} className="h-[280px] w-full" preserveAspectRatio="xMidYMid meet">
95
+ <defs>
96
+ <linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
97
+ <stop offset="0%" className="[stop-color:var(--color-primary)]" stopOpacity={0.3} />
98
+ <stop offset="100%" className="[stop-color:var(--color-primary)]" stopOpacity={0.02} />
99
+ </linearGradient>
100
+ </defs>
101
+
102
+ {/* Grid lines */}
103
+ {yLabels.map((_, i) => {
104
+ const y = padT + chartH - (i / (yLabels.length - 1)) * chartH
105
+ return (
106
+ <line key={i} x1={padL} y1={y} x2={svgW - padR} y2={y} className="stroke-border" strokeWidth={0.5} strokeDasharray="4 4" />
107
+ )
108
+ })}
109
+
110
+ {/* Y-axis labels */}
111
+ {yLabels.map((label, i) => {
112
+ const y = padT + chartH - (i / (yLabels.length - 1)) * chartH
113
+ return (
114
+ <text key={i} x={padL - 6} y={y + 3} textAnchor="end" className="fill-muted-foreground text-[9px]">
115
+ {label}
116
+ </text>
117
+ )
118
+ })}
119
+
120
+ {/* Area fill */}
121
+ <path d={areaPath} fill="url(#areaGradient)" />
122
+
123
+ {/* Line */}
124
+ <path d={linePath} fill="none" className="stroke-primary" strokeWidth={2} strokeLinecap="round" />
125
+
126
+ {/* X-axis labels */}
127
+ {days.map((day, i) => {
128
+ const idx = Math.round((i / (days.length - 1)) * (data.length - 1))
129
+ return (
130
+ <text key={i} x={points[idx].x} y={svgH - 6} textAnchor="middle" className="fill-muted-foreground text-[9px]">
131
+ {day}
132
+ </text>
133
+ )
134
+ })}
135
+ </svg>
136
+ )
137
+ }
138
+
139
+ export default function Dashboard({
140
+ appName = 'Mantiq',
141
+ currentUser,
142
+ navigate,
143
+ }: DashboardProps) {
144
+ const [activeRange, setActiveRange] = useState('7d')
145
+
146
+ return (
147
+ <AuthenticatedLayout
148
+ currentUser={currentUser}
149
+ appName={appName}
150
+ navigate={navigate}
151
+ activePath="/dashboard"
152
+ >
153
+ <Header navigate={navigate}>
154
+ <TopNav links={topNav} onLinkClick={navigate} />
155
+ </Header>
156
+ <Main>
157
+ <div className="space-y-6">
158
+ {/* Page title row */}
159
+ <div className="flex items-center justify-between">
160
+ <h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
161
+ <div className="flex items-center gap-1 rounded-lg border bg-card p-1">
162
+ {(['7d', '30d', '90d'] as const).map((range) => (
163
+ <button
164
+ key={range}
165
+ onClick={() => setActiveRange(range)}
166
+ className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
167
+ activeRange === range
168
+ ? 'bg-primary text-primary-foreground shadow-sm'
169
+ : 'text-muted-foreground hover:text-foreground'
170
+ }`}
171
+ >
172
+ {range === '7d' ? '7 days' : range === '30d' ? '30 days' : '90 days'}
173
+ </button>
174
+ ))}
175
+ </div>
176
+ </div>
177
+
178
+ {/* Stat cards */}
179
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
180
+ <Card>
181
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
182
+ <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
183
+ <DollarSign className="h-4 w-4 text-muted-foreground" />
184
+ </CardHeader>
185
+ <CardContent>
186
+ <div className="text-2xl font-bold">$45,231.89</div>
187
+ <div className="mt-1 flex items-center gap-1">
188
+ <Badge variant="outline" className="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
189
+ <ArrowUpRight className="h-3 w-3" />
190
+ +20.1%
191
+ </Badge>
192
+ <span className="text-xs text-muted-foreground">from last period</span>
193
+ </div>
194
+ </CardContent>
195
+ </Card>
196
+
197
+ <Card>
198
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
199
+ <CardTitle className="text-sm font-medium">New Customers</CardTitle>
200
+ <Users className="h-4 w-4 text-muted-foreground" />
201
+ </CardHeader>
202
+ <CardContent>
203
+ <div className="text-2xl font-bold">+2,350</div>
204
+ <div className="mt-1 flex items-center gap-1">
205
+ <Badge variant="outline" className="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
206
+ <ArrowUpRight className="h-3 w-3" />
207
+ +180.1%
208
+ </Badge>
209
+ <span className="text-xs text-muted-foreground">from last period</span>
210
+ </div>
211
+ </CardContent>
212
+ </Card>
213
+
214
+ <Card>
215
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
216
+ <CardTitle className="text-sm font-medium">Active Subscriptions</CardTitle>
217
+ <CreditCard className="h-4 w-4 text-muted-foreground" />
218
+ </CardHeader>
219
+ <CardContent>
220
+ <div className="text-2xl font-bold">+12,234</div>
221
+ <div className="mt-1 flex items-center gap-1">
222
+ <Badge variant="outline" className="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
223
+ <ArrowUpRight className="h-3 w-3" />
224
+ +19%
225
+ </Badge>
226
+ <span className="text-xs text-muted-foreground">from last period</span>
227
+ </div>
228
+ </CardContent>
229
+ </Card>
230
+
231
+ <Card>
232
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
233
+ <CardTitle className="text-sm font-medium">Churn Rate</CardTitle>
234
+ <Activity className="h-4 w-4 text-muted-foreground" />
235
+ </CardHeader>
236
+ <CardContent>
237
+ <div className="text-2xl font-bold">2.4%</div>
238
+ <div className="mt-1 flex items-center gap-1">
239
+ <Badge variant="outline" className="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
240
+ <ArrowDownRight className="h-3 w-3" />
241
+ -0.3%
242
+ </Badge>
243
+ <span className="text-xs text-muted-foreground">from last period</span>
244
+ </div>
245
+ </CardContent>
246
+ </Card>
247
+ </div>
248
+
249
+ {/* Revenue chart */}
250
+ <Card>
251
+ <CardHeader>
252
+ <div className="flex items-center justify-between">
253
+ <div>
254
+ <CardTitle>Revenue Overview</CardTitle>
255
+ <CardDescription>Daily revenue for the selected period</CardDescription>
256
+ </div>
257
+ <div className="flex items-center gap-2 text-sm">
258
+ <TrendingUp className="h-4 w-4 text-emerald-600" />
259
+ <span className="font-medium text-emerald-600">+12.5%</span>
260
+ </div>
261
+ </div>
262
+ </CardHeader>
263
+ <CardContent>
264
+ <RevenueChart />
265
+ </CardContent>
266
+ </Card>
267
+
268
+ {/* Recent Transactions */}
269
+ <Card>
270
+ <CardHeader>
271
+ <CardTitle>Recent Transactions</CardTitle>
272
+ <CardDescription>Latest payment activity across all channels</CardDescription>
273
+ </CardHeader>
274
+ <CardContent>
275
+ <Table>
276
+ <TableHeader>
277
+ <TableRow>
278
+ <TableHead>Customer</TableHead>
279
+ <TableHead>Amount</TableHead>
280
+ <TableHead>Status</TableHead>
281
+ <TableHead className="text-right">Date</TableHead>
282
+ </TableRow>
283
+ </TableHeader>
284
+ <TableBody>
285
+ {transactions.map((tx) => (
286
+ <TableRow key={tx.email}>
287
+ <TableCell>
288
+ <div>
289
+ <p className="font-medium">{tx.customer}</p>
290
+ <p className="text-sm text-muted-foreground">{tx.email}</p>
291
+ </div>
292
+ </TableCell>
293
+ <TableCell className="font-medium">{tx.amount}</TableCell>
294
+ <TableCell>
295
+ <Badge variant="outline" className={statusColor(tx.status)}>
296
+ {tx.status}
297
+ </Badge>
298
+ </TableCell>
299
+ <TableCell className="text-right text-muted-foreground">{tx.date}</TableCell>
300
+ </TableRow>
301
+ ))}
302
+ </TableBody>
303
+ </Table>
304
+ </CardContent>
305
+ </Card>
306
+ </div>
307
+ </Main>
308
+ </AuthenticatedLayout>
309
+ )
310
+ }
@@ -0,0 +1,122 @@
1
+ import { useState } from 'react'
2
+ import { post } from '../lib/api.ts'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Input } from '@/components/ui/input'
5
+ import { Label } from '@/components/ui/label'
6
+ import { Separator } from '@/components/ui/separator'
7
+
8
+ interface LoginProps {
9
+ appName?: string
10
+ navigate: (href: string) => void
11
+ [key: string]: any
12
+ }
13
+
14
+ export default function Login({ appName = 'Mantiq', navigate }: LoginProps) {
15
+ const [email, setEmail] = useState('')
16
+ const [password, setPassword] = useState('')
17
+ const [error, setError] = useState('')
18
+ const [loading, setLoading] = useState(false)
19
+
20
+ const handleSubmit = async (e: React.FormEvent) => {
21
+ e.preventDefault()
22
+ setError('')
23
+ setLoading(true)
24
+ const { ok, data } = await post('/login', { email, password })
25
+ if (ok) navigate('/dashboard')
26
+ else setError(data?.error ?? 'Invalid email or password. Please try again.')
27
+ setLoading(false)
28
+ }
29
+
30
+ return (
31
+ <div className="min-h-screen bg-muted/50 flex items-center justify-center p-4">
32
+ <div className="w-full max-w-sm">
33
+ {/* Logo */}
34
+ <div className="mb-8 flex flex-col items-center gap-3">
35
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold shadow-sm">
36
+ M
37
+ </div>
38
+ <span className="text-lg font-semibold tracking-tight">{appName}</span>
39
+ </div>
40
+
41
+ {/* Card */}
42
+ <div className="rounded-lg border bg-card shadow-sm p-6">
43
+ <div className="mb-6">
44
+ <h1 className="text-xl font-semibold tracking-tight">Sign in</h1>
45
+ <p className="mt-1 text-sm text-muted-foreground">
46
+ Enter your credentials to continue
47
+ </p>
48
+ </div>
49
+
50
+ {error && (
51
+ <div className="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
52
+ {error}
53
+ </div>
54
+ )}
55
+
56
+ <form onSubmit={handleSubmit} className="space-y-4">
57
+ <div className="space-y-2">
58
+ <Label htmlFor="email">Email</Label>
59
+ <Input
60
+ id="email"
61
+ type="email"
62
+ value={email}
63
+ onChange={(e) => setEmail(e.target.value)}
64
+ required
65
+ placeholder="you@example.com"
66
+ autoComplete="email"
67
+ />
68
+ </div>
69
+ <div className="space-y-2">
70
+ <div className="flex items-center justify-between">
71
+ <Label htmlFor="password">Password</Label>
72
+ <button
73
+ type="button"
74
+ className="text-xs font-medium text-primary hover:text-primary/80"
75
+ tabIndex={-1}
76
+ >
77
+ Forgot password?
78
+ </button>
79
+ </div>
80
+ <Input
81
+ id="password"
82
+ type="password"
83
+ value={password}
84
+ onChange={(e) => setPassword(e.target.value)}
85
+ required
86
+ placeholder="Enter your password"
87
+ autoComplete="current-password"
88
+ />
89
+ </div>
90
+ <Button type="submit" className="w-full" disabled={loading}>
91
+ {loading ? 'Signing in...' : 'Sign in'}
92
+ </Button>
93
+ </form>
94
+
95
+ <div className="relative my-6">
96
+ <div className="absolute inset-0 flex items-center">
97
+ <Separator className="w-full" />
98
+ </div>
99
+ <div className="relative flex justify-center text-xs uppercase">
100
+ <span className="bg-card px-2 text-muted-foreground">or</span>
101
+ </div>
102
+ </div>
103
+
104
+ <p className="text-center text-sm text-muted-foreground">
105
+ <button
106
+ type="button"
107
+ className="font-medium text-primary hover:text-primary/80"
108
+ onClick={() => navigate('/register')}
109
+ >
110
+ Create an account
111
+ </button>
112
+ </p>
113
+ </div>
114
+
115
+ {/* Footer */}
116
+ <p className="mt-6 text-center text-xs text-muted-foreground">
117
+ Powered by {appName}
118
+ </p>
119
+ </div>
120
+ </div>
121
+ )
122
+ }