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,144 @@
|
|
|
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 RegisterProps {
|
|
9
|
+
appName?: string
|
|
10
|
+
navigate: (href: string) => void
|
|
11
|
+
[key: string]: any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Register({ appName = 'Mantiq', navigate }: RegisterProps) {
|
|
15
|
+
const [name, setName] = useState('')
|
|
16
|
+
const [email, setEmail] = useState('')
|
|
17
|
+
const [password, setPassword] = useState('')
|
|
18
|
+
const [error, setError] = useState('')
|
|
19
|
+
const [loading, setLoading] = useState(false)
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
setError('')
|
|
24
|
+
setLoading(true)
|
|
25
|
+
const { ok, data } = await post('/register', { name, email, password })
|
|
26
|
+
if (ok) navigate('/dashboard')
|
|
27
|
+
else setError(data?.error?.message ?? data?.error ?? 'Registration failed. Please try again.')
|
|
28
|
+
setLoading(false)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="min-h-screen bg-muted/50 flex items-center justify-center p-4">
|
|
33
|
+
<div className="w-full max-w-sm">
|
|
34
|
+
{/* Logo */}
|
|
35
|
+
<div className="mb-8 flex flex-col items-center gap-3">
|
|
36
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold shadow-sm">
|
|
37
|
+
M
|
|
38
|
+
</div>
|
|
39
|
+
<span className="text-lg font-semibold tracking-tight">{appName}</span>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* Card */}
|
|
43
|
+
<div className="rounded-lg border bg-card shadow-sm p-6">
|
|
44
|
+
<div className="mb-6">
|
|
45
|
+
<h1 className="text-xl font-semibold tracking-tight">Create an account</h1>
|
|
46
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
47
|
+
Get started with {appName} today
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{error && (
|
|
52
|
+
<div className="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
|
53
|
+
{error}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
58
|
+
<div className="space-y-2">
|
|
59
|
+
<Label htmlFor="name">Name</Label>
|
|
60
|
+
<Input
|
|
61
|
+
id="name"
|
|
62
|
+
value={name}
|
|
63
|
+
onChange={(e) => setName(e.target.value)}
|
|
64
|
+
required
|
|
65
|
+
placeholder="John Doe"
|
|
66
|
+
autoComplete="name"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="space-y-2">
|
|
70
|
+
<Label htmlFor="email">Email</Label>
|
|
71
|
+
<Input
|
|
72
|
+
id="email"
|
|
73
|
+
type="email"
|
|
74
|
+
value={email}
|
|
75
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
76
|
+
required
|
|
77
|
+
placeholder="you@example.com"
|
|
78
|
+
autoComplete="email"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
<Label htmlFor="password">Password</Label>
|
|
83
|
+
<Input
|
|
84
|
+
id="password"
|
|
85
|
+
type="password"
|
|
86
|
+
value={password}
|
|
87
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
88
|
+
required
|
|
89
|
+
placeholder="Create a strong password"
|
|
90
|
+
autoComplete="new-password"
|
|
91
|
+
minLength={8}
|
|
92
|
+
/>
|
|
93
|
+
<p className="text-xs text-muted-foreground">Must be at least 8 characters</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="flex items-start gap-2">
|
|
97
|
+
<input
|
|
98
|
+
type="checkbox"
|
|
99
|
+
id="terms"
|
|
100
|
+
className="mt-1 h-3.5 w-3.5 rounded border-input accent-primary"
|
|
101
|
+
required
|
|
102
|
+
/>
|
|
103
|
+
<label htmlFor="terms" className="text-xs text-muted-foreground leading-relaxed">
|
|
104
|
+
I agree to the{' '}
|
|
105
|
+
<button type="button" className="font-medium text-primary hover:text-primary/80">Terms of Service</button>
|
|
106
|
+
{' '}and{' '}
|
|
107
|
+
<button type="button" className="font-medium text-primary hover:text-primary/80">Privacy Policy</button>
|
|
108
|
+
</label>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
112
|
+
{loading ? 'Creating account...' : 'Create account'}
|
|
113
|
+
</Button>
|
|
114
|
+
</form>
|
|
115
|
+
|
|
116
|
+
<div className="relative my-6">
|
|
117
|
+
<div className="absolute inset-0 flex items-center">
|
|
118
|
+
<Separator className="w-full" />
|
|
119
|
+
</div>
|
|
120
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
121
|
+
<span className="bg-card px-2 text-muted-foreground">or</span>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
126
|
+
Already have an account?{' '}
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
className="font-medium text-primary hover:text-primary/80"
|
|
130
|
+
onClick={() => navigate('/login')}
|
|
131
|
+
>
|
|
132
|
+
Sign in
|
|
133
|
+
</button>
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Footer */}
|
|
138
|
+
<p className="mt-6 text-center text-xs text-muted-foreground">
|
|
139
|
+
Powered by {appName}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* shadcn/ui theme — Corporate
|
|
6
|
+
*
|
|
7
|
+
* Dense, professional, data-focused. Small-medium radius. Indigo primary.
|
|
8
|
+
* Crisp borders, pure white cards on light gray background. Inspired by Stripe.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
@theme inline {
|
|
12
|
+
--color-background: var(--background);
|
|
13
|
+
--color-foreground: var(--foreground);
|
|
14
|
+
--color-card: var(--card);
|
|
15
|
+
--color-card-foreground: var(--card-foreground);
|
|
16
|
+
--color-popover: var(--popover);
|
|
17
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
18
|
+
--color-primary: var(--primary);
|
|
19
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
20
|
+
--color-secondary: var(--secondary);
|
|
21
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
22
|
+
--color-muted: var(--muted);
|
|
23
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
24
|
+
--color-accent: var(--accent);
|
|
25
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
26
|
+
--color-destructive: var(--destructive);
|
|
27
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
28
|
+
--color-border: var(--border);
|
|
29
|
+
--color-input: var(--input);
|
|
30
|
+
--color-ring: var(--ring);
|
|
31
|
+
--color-chart-1: var(--chart-1);
|
|
32
|
+
--color-chart-2: var(--chart-2);
|
|
33
|
+
--color-chart-3: var(--chart-3);
|
|
34
|
+
--color-chart-4: var(--chart-4);
|
|
35
|
+
--color-chart-5: var(--chart-5);
|
|
36
|
+
--color-sidebar: var(--sidebar);
|
|
37
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
38
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
39
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
40
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
41
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
42
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
43
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
44
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
45
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
46
|
+
--radius-lg: var(--radius);
|
|
47
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:root {
|
|
51
|
+
--radius: 0.375rem;
|
|
52
|
+
--background: oklch(0.98 0.001 247.858);
|
|
53
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
54
|
+
--card: oklch(1 0 0);
|
|
55
|
+
--card-foreground: oklch(0.141 0.005 285.823);
|
|
56
|
+
--popover: oklch(1 0 0);
|
|
57
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
58
|
+
--primary: oklch(0.585 0.233 277.117);
|
|
59
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--secondary: oklch(0.96 0.003 264);
|
|
61
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
62
|
+
--muted: oklch(0.96 0.003 264);
|
|
63
|
+
--muted-foreground: oklch(0.50 0.016 285.938);
|
|
64
|
+
--accent: oklch(0.96 0.003 264);
|
|
65
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
66
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
67
|
+
--destructive-foreground: oklch(0.577 0.245 27.325);
|
|
68
|
+
--border: oklch(0.895 0.006 264);
|
|
69
|
+
--input: oklch(0.895 0.006 264);
|
|
70
|
+
--ring: oklch(0.585 0.233 277.117);
|
|
71
|
+
--chart-1: oklch(0.585 0.233 277.117);
|
|
72
|
+
--chart-2: oklch(0.553 0.213 264.364);
|
|
73
|
+
--chart-3: oklch(0.496 0.265 301.924);
|
|
74
|
+
--chart-4: oklch(0.6 0.118 184.714);
|
|
75
|
+
--chart-5: oklch(0.828 0.189 84.429);
|
|
76
|
+
--sidebar: oklch(1 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
78
|
+
--sidebar-primary: oklch(0.585 0.233 277.117);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.96 0.003 264);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
82
|
+
--sidebar-border: oklch(0.895 0.006 264);
|
|
83
|
+
--sidebar-ring: oklch(0.585 0.233 277.117);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dark {
|
|
87
|
+
--background: oklch(0.141 0.005 285.823);
|
|
88
|
+
--foreground: oklch(0.985 0 0);
|
|
89
|
+
--card: oklch(0.178 0.005 285.823);
|
|
90
|
+
--card-foreground: oklch(0.985 0 0);
|
|
91
|
+
--popover: oklch(0.178 0.005 285.823);
|
|
92
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
93
|
+
--primary: oklch(0.65 0.21 277.117);
|
|
94
|
+
--primary-foreground: oklch(0.145 0.004 285.823);
|
|
95
|
+
--secondary: oklch(0.24 0.006 286.033);
|
|
96
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--muted: oklch(0.24 0.006 286.033);
|
|
98
|
+
--muted-foreground: oklch(0.65 0.014 286.067);
|
|
99
|
+
--accent: oklch(0.24 0.006 286.033);
|
|
100
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
101
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
102
|
+
--destructive-foreground: oklch(0.704 0.191 22.216);
|
|
103
|
+
--border: oklch(0.28 0.006 286.033);
|
|
104
|
+
--input: oklch(0.28 0.006 286.033);
|
|
105
|
+
--ring: oklch(0.65 0.21 277.117);
|
|
106
|
+
--chart-1: oklch(0.65 0.21 277.117);
|
|
107
|
+
--chart-2: oklch(0.623 0.214 262.881);
|
|
108
|
+
--chart-3: oklch(0.627 0.265 303.9);
|
|
109
|
+
--chart-4: oklch(0.696 0.17 162.48);
|
|
110
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
111
|
+
--sidebar: oklch(0.16 0.005 285.823);
|
|
112
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
113
|
+
--sidebar-primary: oklch(0.65 0.21 277.117);
|
|
114
|
+
--sidebar-primary-foreground: oklch(0.145 0.004 285.823);
|
|
115
|
+
--sidebar-accent: oklch(0.24 0.006 286.033);
|
|
116
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
117
|
+
--sidebar-border: oklch(0.28 0.006 286.033);
|
|
118
|
+
--sidebar-ring: oklch(0.65 0.21 277.117);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@layer base {
|
|
122
|
+
* {
|
|
123
|
+
@apply border-border;
|
|
124
|
+
}
|
|
125
|
+
body {
|
|
126
|
+
@apply bg-background text-foreground;
|
|
127
|
+
-webkit-font-smoothing: antialiased;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@keyframes fadeUp {
|
|
132
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
133
|
+
to { opacity: 1; transform: translateY(0); }
|
|
134
|
+
}
|
|
135
|
+
.animate-fade-up { animation: fadeUp 0.4s ease-out; }
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import AuthenticatedLayout from '$lib/components/layout/AuthenticatedLayout.svelte'
|
|
3
|
+
import Header from '$lib/components/layout/Header.svelte'
|
|
4
|
+
import Main from '$lib/components/layout/Main.svelte'
|
|
5
|
+
import TopNav from '$lib/components/layout/TopNav.svelte'
|
|
6
|
+
import { Button } from '$lib/components/ui/button'
|
|
7
|
+
import * as Card from '$lib/components/ui/card'
|
|
8
|
+
import { Badge } from '$lib/components/ui/badge'
|
|
9
|
+
import { Separator } from '$lib/components/ui/separator'
|
|
10
|
+
import * as Table from '$lib/components/ui/table'
|
|
11
|
+
import {
|
|
12
|
+
TrendingUp,
|
|
13
|
+
TrendingDown,
|
|
14
|
+
ArrowUpRight,
|
|
15
|
+
ArrowDownRight,
|
|
16
|
+
CreditCard,
|
|
17
|
+
DollarSign,
|
|
18
|
+
Users,
|
|
19
|
+
Activity,
|
|
20
|
+
} from 'lucide-svelte'
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
appName?: string
|
|
24
|
+
currentUser?: { id: number; name: string; email: string; role: string } | null
|
|
25
|
+
navigate?: (href: string) => void
|
|
26
|
+
}
|
|
27
|
+
let { appName = 'Mantiq', currentUser = null, navigate = () => {} }: Props = $props()
|
|
28
|
+
|
|
29
|
+
const topNav = [
|
|
30
|
+
{ title: 'Overview', href: '/dashboard', isActive: true },
|
|
31
|
+
{ title: 'Sales', href: '/dashboard', isActive: false, disabled: true },
|
|
32
|
+
{ title: 'Tickets', href: '/dashboard', isActive: false, disabled: true },
|
|
33
|
+
{ title: 'Performance', href: '/dashboard', isActive: false, disabled: true },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const transactions = [
|
|
37
|
+
{ customer: 'Olivia Martin', email: 'olivia@email.com', amount: '$1,999.00', status: 'Completed', date: 'Mar 15, 2025' },
|
|
38
|
+
{ customer: 'Jackson Lee', email: 'jackson@email.com', amount: '$39.00', status: 'Completed', date: 'Mar 14, 2025' },
|
|
39
|
+
{ customer: 'Isabella Nguyen', email: 'isabella@email.com', amount: '$299.00', status: 'Pending', date: 'Mar 14, 2025' },
|
|
40
|
+
{ customer: 'William Kim', email: 'will@email.com', amount: '$99.00', status: 'Failed', date: 'Mar 13, 2025' },
|
|
41
|
+
{ customer: 'Sofia Davis', email: 'sofia@email.com', amount: '$39.00', status: 'Completed', date: 'Mar 13, 2025' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function statusColor(status: string) {
|
|
45
|
+
if (status === 'Completed') return 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-emerald-500/20'
|
|
46
|
+
if (status === 'Pending') return 'bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/20'
|
|
47
|
+
return 'bg-red-500/15 text-red-700 dark:text-red-400 border-red-500/20'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let activeRange = $state('7d')
|
|
51
|
+
|
|
52
|
+
// Revenue chart data
|
|
53
|
+
const chartData = [1200, 2100, 1800, 3200, 2800, 4100, 3600, 5200, 4800, 5800, 5100, 6200, 5600]
|
|
54
|
+
const maxVal = Math.max(...chartData)
|
|
55
|
+
const svgW = 700
|
|
56
|
+
const svgH = 220
|
|
57
|
+
const padL = 40
|
|
58
|
+
const padR = 10
|
|
59
|
+
const padT = 10
|
|
60
|
+
const padB = 30
|
|
61
|
+
const chartW = svgW - padL - padR
|
|
62
|
+
const chartH = svgH - padT - padB
|
|
63
|
+
const stepX = chartW / (chartData.length - 1)
|
|
64
|
+
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
65
|
+
const yLabels = ['$0', '$2k', '$4k', '$6k']
|
|
66
|
+
|
|
67
|
+
const points = chartData.map((val, i) => ({
|
|
68
|
+
x: padL + i * stepX,
|
|
69
|
+
y: padT + chartH - (val / maxVal) * chartH,
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
const linePath = points.map((p, i) => {
|
|
73
|
+
if (i === 0) return `M ${p.x} ${p.y}`
|
|
74
|
+
const prev = points[i - 1]
|
|
75
|
+
const cpx1 = prev.x + stepX * 0.4
|
|
76
|
+
const cpx2 = p.x - stepX * 0.4
|
|
77
|
+
return `C ${cpx1} ${prev.y}, ${cpx2} ${p.y}, ${p.x} ${p.y}`
|
|
78
|
+
}).join(' ')
|
|
79
|
+
|
|
80
|
+
const areaPath = `${linePath} L ${points[points.length - 1].x} ${padT + chartH} L ${points[0].x} ${padT + chartH} Z`
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<AuthenticatedLayout {currentUser} {appName} {navigate} activePath="/dashboard">
|
|
84
|
+
<Header {navigate}>
|
|
85
|
+
<TopNav links={topNav} onLinkClick={navigate} />
|
|
86
|
+
</Header>
|
|
87
|
+
<Main>
|
|
88
|
+
<div class="space-y-6">
|
|
89
|
+
<!-- Page title row -->
|
|
90
|
+
<div class="flex items-center justify-between">
|
|
91
|
+
<h2 class="text-3xl font-bold tracking-tight">Dashboard</h2>
|
|
92
|
+
<div class="flex items-center gap-1 rounded-lg border bg-card p-1">
|
|
93
|
+
{#each ['7d', '30d', '90d'] as range}
|
|
94
|
+
<button
|
|
95
|
+
onclick={() => activeRange = range}
|
|
96
|
+
class="rounded-md px-3 py-1.5 text-xs font-medium transition-colors {activeRange === range
|
|
97
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
98
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
99
|
+
>
|
|
100
|
+
{range === '7d' ? '7 days' : range === '30d' ? '30 days' : '90 days'}
|
|
101
|
+
</button>
|
|
102
|
+
{/each}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Stat cards -->
|
|
107
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
108
|
+
<Card.Root>
|
|
109
|
+
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
110
|
+
<Card.Title class="text-sm font-medium">Total Revenue</Card.Title>
|
|
111
|
+
<DollarSign class="h-4 w-4 text-muted-foreground" />
|
|
112
|
+
</Card.Header>
|
|
113
|
+
<Card.Content>
|
|
114
|
+
<div class="text-2xl font-bold">$45,231.89</div>
|
|
115
|
+
<div class="mt-1 flex items-center gap-1">
|
|
116
|
+
<Badge variant="outline" class="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
|
|
117
|
+
<ArrowUpRight class="h-3 w-3" />
|
|
118
|
+
+20.1%
|
|
119
|
+
</Badge>
|
|
120
|
+
<span class="text-xs text-muted-foreground">from last period</span>
|
|
121
|
+
</div>
|
|
122
|
+
</Card.Content>
|
|
123
|
+
</Card.Root>
|
|
124
|
+
|
|
125
|
+
<Card.Root>
|
|
126
|
+
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
127
|
+
<Card.Title class="text-sm font-medium">New Customers</Card.Title>
|
|
128
|
+
<Users class="h-4 w-4 text-muted-foreground" />
|
|
129
|
+
</Card.Header>
|
|
130
|
+
<Card.Content>
|
|
131
|
+
<div class="text-2xl font-bold">+2,350</div>
|
|
132
|
+
<div class="mt-1 flex items-center gap-1">
|
|
133
|
+
<Badge variant="outline" class="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
|
|
134
|
+
<ArrowUpRight class="h-3 w-3" />
|
|
135
|
+
+180.1%
|
|
136
|
+
</Badge>
|
|
137
|
+
<span class="text-xs text-muted-foreground">from last period</span>
|
|
138
|
+
</div>
|
|
139
|
+
</Card.Content>
|
|
140
|
+
</Card.Root>
|
|
141
|
+
|
|
142
|
+
<Card.Root>
|
|
143
|
+
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
144
|
+
<Card.Title class="text-sm font-medium">Active Subscriptions</Card.Title>
|
|
145
|
+
<CreditCard class="h-4 w-4 text-muted-foreground" />
|
|
146
|
+
</Card.Header>
|
|
147
|
+
<Card.Content>
|
|
148
|
+
<div class="text-2xl font-bold">+12,234</div>
|
|
149
|
+
<div class="mt-1 flex items-center gap-1">
|
|
150
|
+
<Badge variant="outline" class="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
|
|
151
|
+
<ArrowUpRight class="h-3 w-3" />
|
|
152
|
+
+19%
|
|
153
|
+
</Badge>
|
|
154
|
+
<span class="text-xs text-muted-foreground">from last period</span>
|
|
155
|
+
</div>
|
|
156
|
+
</Card.Content>
|
|
157
|
+
</Card.Root>
|
|
158
|
+
|
|
159
|
+
<Card.Root>
|
|
160
|
+
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
161
|
+
<Card.Title class="text-sm font-medium">Churn Rate</Card.Title>
|
|
162
|
+
<Activity class="h-4 w-4 text-muted-foreground" />
|
|
163
|
+
</Card.Header>
|
|
164
|
+
<Card.Content>
|
|
165
|
+
<div class="text-2xl font-bold">2.4%</div>
|
|
166
|
+
<div class="mt-1 flex items-center gap-1">
|
|
167
|
+
<Badge variant="outline" class="border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 gap-0.5 text-xs font-medium">
|
|
168
|
+
<ArrowDownRight class="h-3 w-3" />
|
|
169
|
+
-0.3%
|
|
170
|
+
</Badge>
|
|
171
|
+
<span class="text-xs text-muted-foreground">from last period</span>
|
|
172
|
+
</div>
|
|
173
|
+
</Card.Content>
|
|
174
|
+
</Card.Root>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- Revenue chart -->
|
|
178
|
+
<Card.Root>
|
|
179
|
+
<Card.Header>
|
|
180
|
+
<div class="flex items-center justify-between">
|
|
181
|
+
<div>
|
|
182
|
+
<Card.Title>Revenue Overview</Card.Title>
|
|
183
|
+
<Card.Description>Daily revenue for the selected period</Card.Description>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="flex items-center gap-2 text-sm">
|
|
186
|
+
<TrendingUp class="h-4 w-4 text-emerald-600" />
|
|
187
|
+
<span class="font-medium text-emerald-600">+12.5%</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</Card.Header>
|
|
191
|
+
<Card.Content>
|
|
192
|
+
<svg viewBox="0 0 {svgW} {svgH}" class="h-[280px] w-full" preserveAspectRatio="xMidYMid meet">
|
|
193
|
+
<defs>
|
|
194
|
+
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
|
|
195
|
+
<stop offset="0%" class="[stop-color:var(--color-primary)]" stop-opacity="0.3" />
|
|
196
|
+
<stop offset="100%" class="[stop-color:var(--color-primary)]" stop-opacity="0.02" />
|
|
197
|
+
</linearGradient>
|
|
198
|
+
</defs>
|
|
199
|
+
|
|
200
|
+
<!-- Grid lines -->
|
|
201
|
+
{#each yLabels as _, i}
|
|
202
|
+
{@const y = padT + chartH - (i / (yLabels.length - 1)) * chartH}
|
|
203
|
+
<line x1={padL} y1={y} x2={svgW - padR} y2={y} class="stroke-border" stroke-width="0.5" stroke-dasharray="4 4" />
|
|
204
|
+
{/each}
|
|
205
|
+
|
|
206
|
+
<!-- Y-axis labels -->
|
|
207
|
+
{#each yLabels as label, i}
|
|
208
|
+
{@const y = padT + chartH - (i / (yLabels.length - 1)) * chartH}
|
|
209
|
+
<text x={padL - 6} y={y + 3} text-anchor="end" class="fill-muted-foreground text-[9px]">
|
|
210
|
+
{label}
|
|
211
|
+
</text>
|
|
212
|
+
{/each}
|
|
213
|
+
|
|
214
|
+
<!-- Area fill -->
|
|
215
|
+
<path d={areaPath} fill="url(#areaGradient)" />
|
|
216
|
+
|
|
217
|
+
<!-- Line -->
|
|
218
|
+
<path d={linePath} fill="none" class="stroke-primary" stroke-width="2" stroke-linecap="round" />
|
|
219
|
+
|
|
220
|
+
<!-- X-axis labels -->
|
|
221
|
+
{#each days as day, i}
|
|
222
|
+
{@const idx = Math.round((i / (days.length - 1)) * (chartData.length - 1))}
|
|
223
|
+
<text x={points[idx].x} y={svgH - 6} text-anchor="middle" class="fill-muted-foreground text-[9px]">
|
|
224
|
+
{day}
|
|
225
|
+
</text>
|
|
226
|
+
{/each}
|
|
227
|
+
</svg>
|
|
228
|
+
</Card.Content>
|
|
229
|
+
</Card.Root>
|
|
230
|
+
|
|
231
|
+
<!-- Recent Transactions -->
|
|
232
|
+
<Card.Root>
|
|
233
|
+
<Card.Header>
|
|
234
|
+
<Card.Title>Recent Transactions</Card.Title>
|
|
235
|
+
<Card.Description>Latest payment activity across all channels</Card.Description>
|
|
236
|
+
</Card.Header>
|
|
237
|
+
<Card.Content>
|
|
238
|
+
<Table.Root>
|
|
239
|
+
<Table.Header>
|
|
240
|
+
<Table.Row>
|
|
241
|
+
<Table.Head>Customer</Table.Head>
|
|
242
|
+
<Table.Head>Amount</Table.Head>
|
|
243
|
+
<Table.Head>Status</Table.Head>
|
|
244
|
+
<Table.Head class="text-right">Date</Table.Head>
|
|
245
|
+
</Table.Row>
|
|
246
|
+
</Table.Header>
|
|
247
|
+
<Table.Body>
|
|
248
|
+
{#each transactions as tx (tx.email)}
|
|
249
|
+
<Table.Row>
|
|
250
|
+
<Table.Cell>
|
|
251
|
+
<div>
|
|
252
|
+
<p class="font-medium">{tx.customer}</p>
|
|
253
|
+
<p class="text-sm text-muted-foreground">{tx.email}</p>
|
|
254
|
+
</div>
|
|
255
|
+
</Table.Cell>
|
|
256
|
+
<Table.Cell class="font-medium">{tx.amount}</Table.Cell>
|
|
257
|
+
<Table.Cell>
|
|
258
|
+
<Badge variant="outline" class={statusColor(tx.status)}>
|
|
259
|
+
{tx.status}
|
|
260
|
+
</Badge>
|
|
261
|
+
</Table.Cell>
|
|
262
|
+
<Table.Cell class="text-right text-muted-foreground">{tx.date}</Table.Cell>
|
|
263
|
+
</Table.Row>
|
|
264
|
+
{/each}
|
|
265
|
+
</Table.Body>
|
|
266
|
+
</Table.Root>
|
|
267
|
+
</Card.Content>
|
|
268
|
+
</Card.Root>
|
|
269
|
+
</div>
|
|
270
|
+
</Main>
|
|
271
|
+
</AuthenticatedLayout>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { post } from '$lib/api'
|
|
3
|
+
import { Button } from '$lib/components/ui/button'
|
|
4
|
+
import { Input } from '$lib/components/ui/input'
|
|
5
|
+
import { Label } from '$lib/components/ui/label'
|
|
6
|
+
import { Separator } from '$lib/components/ui/separator'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
appName?: string
|
|
10
|
+
navigate?: (href: string) => void
|
|
11
|
+
}
|
|
12
|
+
let { appName = 'Mantiq', navigate = () => {} }: Props = $props()
|
|
13
|
+
|
|
14
|
+
let email = $state('')
|
|
15
|
+
let password = $state('')
|
|
16
|
+
let error = $state('')
|
|
17
|
+
let loading = $state(false)
|
|
18
|
+
|
|
19
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
20
|
+
e.preventDefault()
|
|
21
|
+
error = ''
|
|
22
|
+
loading = true
|
|
23
|
+
const { ok, data } = await post('/login', { email, password })
|
|
24
|
+
if (ok) navigate('/dashboard')
|
|
25
|
+
else error = data?.error ?? 'Invalid email or password. Please try again.'
|
|
26
|
+
loading = false
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="min-h-screen bg-muted/50 flex items-center justify-center p-4">
|
|
31
|
+
<div class="w-full max-w-sm">
|
|
32
|
+
<!-- Logo -->
|
|
33
|
+
<div class="mb-8 flex flex-col items-center gap-3">
|
|
34
|
+
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold shadow-sm">
|
|
35
|
+
M
|
|
36
|
+
</div>
|
|
37
|
+
<span class="text-lg font-semibold tracking-tight">{appName}</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Card -->
|
|
41
|
+
<div class="rounded-lg border bg-card shadow-sm p-6">
|
|
42
|
+
<div class="mb-6">
|
|
43
|
+
<h1 class="text-xl font-semibold tracking-tight">Sign in</h1>
|
|
44
|
+
<p class="mt-1 text-sm text-muted-foreground">
|
|
45
|
+
Enter your credentials to continue
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{#if error}
|
|
50
|
+
<div class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
|
51
|
+
{error}
|
|
52
|
+
</div>
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
56
|
+
<div class="space-y-2">
|
|
57
|
+
<Label for="email">Email</Label>
|
|
58
|
+
<Input
|
|
59
|
+
id="email"
|
|
60
|
+
type="email"
|
|
61
|
+
bind:value={email}
|
|
62
|
+
required
|
|
63
|
+
placeholder="you@example.com"
|
|
64
|
+
autocomplete="email"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="space-y-2">
|
|
68
|
+
<div class="flex items-center justify-between">
|
|
69
|
+
<Label for="password">Password</Label>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="text-xs font-medium text-primary hover:text-primary/80"
|
|
73
|
+
tabindex="-1"
|
|
74
|
+
>
|
|
75
|
+
Forgot password?
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
<Input
|
|
79
|
+
id="password"
|
|
80
|
+
type="password"
|
|
81
|
+
bind:value={password}
|
|
82
|
+
required
|
|
83
|
+
placeholder="Enter your password"
|
|
84
|
+
autocomplete="current-password"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<Button type="submit" class="w-full" disabled={loading}>
|
|
88
|
+
{loading ? 'Signing in...' : 'Sign in'}
|
|
89
|
+
</Button>
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
<div class="relative my-6">
|
|
93
|
+
<div class="absolute inset-0 flex items-center">
|
|
94
|
+
<Separator class="w-full" />
|
|
95
|
+
</div>
|
|
96
|
+
<div class="relative flex justify-center text-xs uppercase">
|
|
97
|
+
<span class="bg-card px-2 text-muted-foreground">or</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<p class="text-center text-sm text-muted-foreground">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="font-medium text-primary hover:text-primary/80"
|
|
105
|
+
onclick={() => navigate('/register')}
|
|
106
|
+
>
|
|
107
|
+
Create an account
|
|
108
|
+
</button>
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Footer -->
|
|
113
|
+
<p class="mt-6 text-center text-xs text-muted-foreground">
|
|
114
|
+
Powered by {appName}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|