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,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
|
+
}
|