@amirulabu/create-recurring-rabbit-app 0.0.0-alpha
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/bin/index.js +2 -0
- package/dist/index.js +592 -0
- package/package.json +43 -0
- package/templates/default/.editorconfig +21 -0
- package/templates/default/.env.example +15 -0
- package/templates/default/.eslintrc.json +35 -0
- package/templates/default/.prettierrc.json +7 -0
- package/templates/default/README.md +346 -0
- package/templates/default/app.config.ts +20 -0
- package/templates/default/docs/adding-features.md +439 -0
- package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
- package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
- package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
- package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
- package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
- package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
- package/templates/default/docs/architecture.md +241 -0
- package/templates/default/docs/database.md +376 -0
- package/templates/default/docs/deployment.md +435 -0
- package/templates/default/docs/troubleshooting.md +668 -0
- package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
- package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
- package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
- package/templates/default/drizzle.config.ts +10 -0
- package/templates/default/lighthouserc.json +78 -0
- package/templates/default/src/app/__root.tsx +32 -0
- package/templates/default/src/app/api/auth/$.ts +15 -0
- package/templates/default/src/app/api/trpc.server.ts +12 -0
- package/templates/default/src/app/auth/forgot-password.tsx +107 -0
- package/templates/default/src/app/auth/login.tsx +34 -0
- package/templates/default/src/app/auth/register.tsx +34 -0
- package/templates/default/src/app/auth/reset-password.tsx +171 -0
- package/templates/default/src/app/auth/verify-email.tsx +111 -0
- package/templates/default/src/app/dashboard/index.tsx +122 -0
- package/templates/default/src/app/dashboard/settings.tsx +161 -0
- package/templates/default/src/app/globals.css +55 -0
- package/templates/default/src/app/index.tsx +83 -0
- package/templates/default/src/components/features/auth/login-form.tsx +172 -0
- package/templates/default/src/components/features/auth/register-form.tsx +202 -0
- package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
- package/templates/default/src/components/layout/header.tsx +29 -0
- package/templates/default/src/components/layout/sidebar.tsx +38 -0
- package/templates/default/src/components/ui/button.tsx +57 -0
- package/templates/default/src/components/ui/card.tsx +79 -0
- package/templates/default/src/components/ui/input.tsx +24 -0
- package/templates/default/src/lib/api.ts +42 -0
- package/templates/default/src/lib/auth.ts +292 -0
- package/templates/default/src/lib/email.ts +221 -0
- package/templates/default/src/lib/env.ts +119 -0
- package/templates/default/src/lib/hydration-timing.ts +289 -0
- package/templates/default/src/lib/monitoring.ts +336 -0
- package/templates/default/src/lib/utils.ts +6 -0
- package/templates/default/src/server/api/root.ts +10 -0
- package/templates/default/src/server/api/routers/dashboard.ts +37 -0
- package/templates/default/src/server/api/routers/user.ts +31 -0
- package/templates/default/src/server/api/trpc.ts +132 -0
- package/templates/default/src/server/auth/config.ts +241 -0
- package/templates/default/src/server/db/index.ts +153 -0
- package/templates/default/src/server/db/migrate.ts +125 -0
- package/templates/default/src/server/db/schema.ts +170 -0
- package/templates/default/src/server/db/seed.ts +130 -0
- package/templates/default/src/types/global.d.ts +25 -0
- package/templates/default/tailwind.config.js +46 -0
- package/templates/default/tsconfig.json +36 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute()({
|
|
4
|
+
component: Index,
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
function Index() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
|
|
10
|
+
<div className="container mx-auto px-4 py-16">
|
|
11
|
+
<nav className="flex justify-between items-center mb-16">
|
|
12
|
+
<h1 className="text-2xl font-bold text-blue-600">Recurring Rabbit</h1>
|
|
13
|
+
<div className="flex gap-4">
|
|
14
|
+
<Link
|
|
15
|
+
from="/"
|
|
16
|
+
to="/auth/login"
|
|
17
|
+
className="px-4 py-2 text-gray-700 hover:text-blue-600 transition-colors"
|
|
18
|
+
>
|
|
19
|
+
Sign In
|
|
20
|
+
</Link>
|
|
21
|
+
<Link
|
|
22
|
+
from="/"
|
|
23
|
+
to="/auth/register"
|
|
24
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
Get Started
|
|
27
|
+
</Link>
|
|
28
|
+
</div>
|
|
29
|
+
</nav>
|
|
30
|
+
|
|
31
|
+
<div className="max-w-3xl mx-auto text-center">
|
|
32
|
+
<h2 className="text-5xl font-bold text-gray-900 mb-6">
|
|
33
|
+
Start your SaaS business in minutes
|
|
34
|
+
</h2>
|
|
35
|
+
<p className="text-xl text-gray-600 mb-8">
|
|
36
|
+
A production-ready starter with authentication, database, and all
|
|
37
|
+
the tools you need to ship fast.
|
|
38
|
+
</p>
|
|
39
|
+
<div className="flex justify-center gap-4">
|
|
40
|
+
<Link
|
|
41
|
+
from="/"
|
|
42
|
+
to="/auth/register"
|
|
43
|
+
className="px-6 py-3 bg-blue-600 text-white rounded-lg text-lg font-medium hover:bg-blue-700 transition-colors"
|
|
44
|
+
>
|
|
45
|
+
Start Free Trial
|
|
46
|
+
</Link>
|
|
47
|
+
<Link
|
|
48
|
+
from="/"
|
|
49
|
+
to="/auth/login"
|
|
50
|
+
className="px-4 py-2 text-gray-700 hover:text-blue-600 transition-colors"
|
|
51
|
+
>
|
|
52
|
+
Sign In
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="mt-20 grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
|
58
|
+
<div className="p-6 bg-white rounded-lg shadow-md">
|
|
59
|
+
<h3 className="text-xl font-semibold mb-3">Authentication</h3>
|
|
60
|
+
<p className="text-gray-600">
|
|
61
|
+
Email/password authentication with verification, session
|
|
62
|
+
management, and secure defaults.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="p-6 bg-white rounded-lg shadow-md">
|
|
66
|
+
<h3 className="text-xl font-semibold mb-3">Database</h3>
|
|
67
|
+
<p className="text-gray-600">
|
|
68
|
+
Type-safe database with Drizzle ORM, SQLite for dev, and
|
|
69
|
+
PostgreSQL for production.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="p-6 bg-white rounded-lg shadow-md">
|
|
73
|
+
<h3 className="text-xl font-semibold mb-3">Type Safety</h3>
|
|
74
|
+
<p className="text-gray-600">
|
|
75
|
+
End-to-end type safety from database schema to UI components with
|
|
76
|
+
TypeScript.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { authClient } from '@/lib/auth'
|
|
3
|
+
import { useRouter, useSearch } from '@tanstack/react-router'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Input } from '@/components/ui/input'
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '@/components/ui/card'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* LoginForm Component
|
|
16
|
+
*
|
|
17
|
+
* Provides a user interface for authenticating existing users via email and password.
|
|
18
|
+
* This is the entry point for the authentication flow, allowing users to sign in to access
|
|
19
|
+
* protected resources and dashboard features.
|
|
20
|
+
*
|
|
21
|
+
* Authentication Flow:
|
|
22
|
+
* 1. User enters credentials (email and password)
|
|
23
|
+
* 2. Form validates HTML5 constraints (required fields, email format)
|
|
24
|
+
* 3. Credentials are sent to the auth service via signInEmail
|
|
25
|
+
* 4. On success, user is redirected to /dashboard
|
|
26
|
+
* 5. On failure, user-friendly error message is displayed
|
|
27
|
+
*
|
|
28
|
+
* Key Features:
|
|
29
|
+
* - Client-side form validation using HTML5 built-in validation
|
|
30
|
+
* - Loading state management to prevent double submissions
|
|
31
|
+
* - Clear error messaging with visual feedback
|
|
32
|
+
* - Automatic redirect on successful authentication
|
|
33
|
+
* - Form controls are disabled during submission to prevent user interaction
|
|
34
|
+
*
|
|
35
|
+
* UX Considerations:
|
|
36
|
+
* - Centered card layout with max-width for optimal readability
|
|
37
|
+
* - Password masking for security
|
|
38
|
+
* - Error messages displayed inline near the form for better visibility
|
|
39
|
+
* - Loading state shown in button text to communicate progress
|
|
40
|
+
* - Form reset not needed since redirect occurs on success
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* import { LoginForm } from '@/components/features/auth/login-form'
|
|
45
|
+
*
|
|
46
|
+
* function LoginPage() {
|
|
47
|
+
* return <LoginForm />
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function LoginForm() {
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
const search = useSearch({ from: '/auth/login' })
|
|
54
|
+
const redirect = (search.redirect as string) || '/dashboard'
|
|
55
|
+
const [loading, setLoading] = useState(false)
|
|
56
|
+
const [error, setError] = useState('')
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handles form submission for user authentication.
|
|
60
|
+
*
|
|
61
|
+
* Extracts email and password from the form, validates credentials through the auth service,
|
|
62
|
+
* and manages the user flow based on authentication result.
|
|
63
|
+
*
|
|
64
|
+
* Form Validation:
|
|
65
|
+
* - Email: Uses HTML5 type="email" for basic format validation
|
|
66
|
+
* - Password: Uses HTML5 required attribute
|
|
67
|
+
* - Additional validation happens server-side via auth service
|
|
68
|
+
*
|
|
69
|
+
* @param e - React form event from the form submission
|
|
70
|
+
* @returns Promise<void> - Does not return a value
|
|
71
|
+
*
|
|
72
|
+
* Side Effects:
|
|
73
|
+
* - Sets loading state to true during authentication, false after completion
|
|
74
|
+
* - Clears previous error state before attempting authentication
|
|
75
|
+
* - On success: Navigates to the redirect URL (from search params) or /dashboard
|
|
76
|
+
* - On failure: Sets error state with user-friendly message
|
|
77
|
+
*
|
|
78
|
+
* Error Handling:
|
|
79
|
+
* - API errors from auth service display the error message
|
|
80
|
+
* - Network/unexpected errors show generic "Invalid email or password"
|
|
81
|
+
* - Errors persist until user tries again or leaves page
|
|
82
|
+
*/
|
|
83
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
setLoading(true)
|
|
86
|
+
setError('')
|
|
87
|
+
|
|
88
|
+
const formData = new FormData(e.currentTarget)
|
|
89
|
+
const email = formData.get('email') as string
|
|
90
|
+
const password = formData.get('password') as string
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await authClient.signIn.email({
|
|
94
|
+
email,
|
|
95
|
+
password,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (result.error) {
|
|
99
|
+
setError(result.error.message || 'Invalid email or password')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
router.navigate({ to: redirect })
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setError('Invalid email or password')
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Card className="w-full max-w-md mx-auto">
|
|
113
|
+
<CardHeader>
|
|
114
|
+
<CardTitle>Welcome back</CardTitle>
|
|
115
|
+
<CardDescription>Sign in to your account to continue</CardDescription>
|
|
116
|
+
</CardHeader>
|
|
117
|
+
<CardContent>
|
|
118
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<label
|
|
121
|
+
htmlFor="email"
|
|
122
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
123
|
+
>
|
|
124
|
+
Email
|
|
125
|
+
</label>
|
|
126
|
+
<Input
|
|
127
|
+
id="email"
|
|
128
|
+
name="email"
|
|
129
|
+
type="email"
|
|
130
|
+
placeholder="john@example.com"
|
|
131
|
+
required
|
|
132
|
+
disabled={loading}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="space-y-2">
|
|
136
|
+
<label
|
|
137
|
+
htmlFor="password"
|
|
138
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
139
|
+
>
|
|
140
|
+
Password
|
|
141
|
+
</label>
|
|
142
|
+
<Input
|
|
143
|
+
id="password"
|
|
144
|
+
name="password"
|
|
145
|
+
type="password"
|
|
146
|
+
placeholder="•••••••"
|
|
147
|
+
required
|
|
148
|
+
disabled={loading}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
{error && (
|
|
152
|
+
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
|
153
|
+
{error}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
<Button type="submit" disabled={loading} className="w-full">
|
|
157
|
+
{loading ? 'Signing in...' : 'Sign in'}
|
|
158
|
+
</Button>
|
|
159
|
+
<div className="text-center">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => router.navigate({ to: '/auth/forgot-password' })}
|
|
163
|
+
className="text-sm text-gray-600 hover:text-gray-900"
|
|
164
|
+
>
|
|
165
|
+
Forgot your password?
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</form>
|
|
169
|
+
</CardContent>
|
|
170
|
+
</Card>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { authClient } from '@/lib/auth'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Input } from '@/components/ui/input'
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* RegisterForm Component
|
|
15
|
+
*
|
|
16
|
+
* Provides a user interface for new user registration, collecting essential user information
|
|
17
|
+
* to create an account in the system. This component initiates the user onboarding flow
|
|
18
|
+
* by creating a user account and triggering email verification.
|
|
19
|
+
*
|
|
20
|
+
* Authentication Flow:
|
|
21
|
+
* 1. User enters registration details (name, email, password)
|
|
22
|
+
* 2. Form validates HTML5 constraints (required fields, email format, password length)
|
|
23
|
+
* 3. User data is sent to auth service via signUpEmail
|
|
24
|
+
* 4. On success, verification email is sent and form displays confirmation message
|
|
25
|
+
* 5. User must verify email via link in confirmation email before signing in
|
|
26
|
+
*
|
|
27
|
+
* Key Features:
|
|
28
|
+
* - Client-side form validation with minimum password length requirement (8 characters)
|
|
29
|
+
* - Dual state messaging: success (green) for verification confirmation, error (red) for failures
|
|
30
|
+
* - Form reset after successful registration to allow new attempts
|
|
31
|
+
* - Loading state management to prevent duplicate submissions
|
|
32
|
+
* - Visual feedback using color-coded alerts for different message types
|
|
33
|
+
*
|
|
34
|
+
* UX Considerations:
|
|
35
|
+
* - Centered card layout with max-width for optimal readability
|
|
36
|
+
* - Password masking for security
|
|
37
|
+
* - Password length requirement (minLength={8}) shown to users via browser validation
|
|
38
|
+
* - Success message clearly indicates next steps (check email for verification)
|
|
39
|
+
* - Error messages displayed inline near the form for immediate visibility
|
|
40
|
+
* - Loading state in button text communicates ongoing operation
|
|
41
|
+
* - No automatic redirect - users stay on page to see confirmation message
|
|
42
|
+
*
|
|
43
|
+
* Security Notes:
|
|
44
|
+
* - Password must be at least 8 characters (enforced by HTML5 validation)
|
|
45
|
+
* - Email verification required before user can sign in
|
|
46
|
+
* - All fields are required to prevent incomplete registration
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* import { RegisterForm } from '@/components/features/auth/register-form'
|
|
51
|
+
*
|
|
52
|
+
* function RegisterPage() {
|
|
53
|
+
* return <RegisterForm />
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function RegisterForm() {
|
|
58
|
+
const [loading, setLoading] = useState(false)
|
|
59
|
+
const [message, setMessage] = useState('')
|
|
60
|
+
const [error, setError] = useState('')
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handles form submission for new user registration.
|
|
64
|
+
*
|
|
65
|
+
* Extracts user data from the form, validates inputs through the auth service,
|
|
66
|
+
* and manages the registration flow based on the result.
|
|
67
|
+
*
|
|
68
|
+
* Form Validation:
|
|
69
|
+
* - Name: Required field, no format constraints
|
|
70
|
+
* - Email: Uses HTML5 type="email" for basic format validation
|
|
71
|
+
* - Password: Required, minimum 8 characters (enforced via minLength={8})
|
|
72
|
+
* - Additional validation happens server-side via auth service
|
|
73
|
+
*
|
|
74
|
+
* @param e - React form event from the form submission
|
|
75
|
+
* @returns Promise<void> - Does not return a value
|
|
76
|
+
*
|
|
77
|
+
* Side Effects:
|
|
78
|
+
* - Sets loading state to true during registration, false after completion
|
|
79
|
+
* - Clears previous error and message states before attempting registration
|
|
80
|
+
* - On success: Sets success message and resets the form
|
|
81
|
+
* - On failure: Sets error state with user-friendly message
|
|
82
|
+
*
|
|
83
|
+
* Error Handling:
|
|
84
|
+
* - API errors from auth service display the specific error message
|
|
85
|
+
* - Network/unexpected errors show generic "Registration failed" message
|
|
86
|
+
* - Errors persist until user tries again or leaves page
|
|
87
|
+
*
|
|
88
|
+
* Success Behavior:
|
|
89
|
+
* - Displays green confirmation message prompting email verification
|
|
90
|
+
* - Resets form fields to allow another registration attempt
|
|
91
|
+
* - No redirect - user stays on page to see next steps
|
|
92
|
+
*/
|
|
93
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
94
|
+
e.preventDefault()
|
|
95
|
+
setLoading(true)
|
|
96
|
+
setError('')
|
|
97
|
+
setMessage('')
|
|
98
|
+
|
|
99
|
+
const formData = new FormData(e.currentTarget)
|
|
100
|
+
const email = formData.get('email') as string
|
|
101
|
+
const password = formData.get('password') as string
|
|
102
|
+
const name = formData.get('name') as string
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await authClient.signUp.email({
|
|
106
|
+
email,
|
|
107
|
+
password,
|
|
108
|
+
name,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (result.error) {
|
|
112
|
+
setError(
|
|
113
|
+
result.error.message || 'Registration failed. Please try again.'
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setMessage('Check your email to verify your account')
|
|
119
|
+
e.currentTarget.reset()
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setError('Registration failed. Please try again.')
|
|
122
|
+
} finally {
|
|
123
|
+
setLoading(false)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Card className="w-full max-w-md mx-auto">
|
|
129
|
+
<CardHeader>
|
|
130
|
+
<CardTitle>Create an account</CardTitle>
|
|
131
|
+
<CardDescription>
|
|
132
|
+
Enter your information to create your account
|
|
133
|
+
</CardDescription>
|
|
134
|
+
</CardHeader>
|
|
135
|
+
<CardContent>
|
|
136
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
<label
|
|
139
|
+
htmlFor="name"
|
|
140
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
141
|
+
>
|
|
142
|
+
Full Name
|
|
143
|
+
</label>
|
|
144
|
+
<Input
|
|
145
|
+
id="name"
|
|
146
|
+
name="name"
|
|
147
|
+
placeholder="John Doe"
|
|
148
|
+
required
|
|
149
|
+
disabled={loading}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<label
|
|
154
|
+
htmlFor="email"
|
|
155
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
156
|
+
>
|
|
157
|
+
Email
|
|
158
|
+
</label>
|
|
159
|
+
<Input
|
|
160
|
+
id="email"
|
|
161
|
+
name="email"
|
|
162
|
+
type="email"
|
|
163
|
+
placeholder="john@example.com"
|
|
164
|
+
required
|
|
165
|
+
disabled={loading}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<label
|
|
170
|
+
htmlFor="password"
|
|
171
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
172
|
+
>
|
|
173
|
+
Password
|
|
174
|
+
</label>
|
|
175
|
+
<Input
|
|
176
|
+
id="password"
|
|
177
|
+
name="password"
|
|
178
|
+
type="password"
|
|
179
|
+
placeholder="••••••••"
|
|
180
|
+
required
|
|
181
|
+
minLength={8}
|
|
182
|
+
disabled={loading}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
{error && (
|
|
186
|
+
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
|
|
187
|
+
{error}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
{message && (
|
|
191
|
+
<div className="p-3 text-sm text-green-600 bg-green-50 border border-green-200 rounded-md">
|
|
192
|
+
{message}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
<Button type="submit" disabled={loading} className="w-full">
|
|
196
|
+
{loading ? 'Creating account...' : 'Sign up'}
|
|
197
|
+
</Button>
|
|
198
|
+
</form>
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useLocation } from '@tanstack/react-router'
|
|
2
|
+
import { Header } from './header'
|
|
3
|
+
import { Sidebar } from './sidebar'
|
|
4
|
+
|
|
5
|
+
interface DashboardLayoutProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
userName?: string | null
|
|
8
|
+
onSignOut?: () => Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DashboardLayout({
|
|
12
|
+
children,
|
|
13
|
+
userName,
|
|
14
|
+
onSignOut,
|
|
15
|
+
}: DashboardLayoutProps) {
|
|
16
|
+
const location = useLocation()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-gray-50">
|
|
20
|
+
<Header userName={userName} onSignOut={onSignOut} />
|
|
21
|
+
<div className="flex">
|
|
22
|
+
<Sidebar currentPath={location.pathname} />
|
|
23
|
+
<main className="flex-1 p-8">{children}</main>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { Button } from '@/components/ui/button'
|
|
3
|
+
|
|
4
|
+
interface HeaderProps {
|
|
5
|
+
userName?: string | null
|
|
6
|
+
onSignOut?: () => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Header({ userName, onSignOut }: HeaderProps) {
|
|
10
|
+
return (
|
|
11
|
+
<header className="bg-white border-b border-gray-200 h-16">
|
|
12
|
+
<div className="container mx-auto px-4 h-full">
|
|
13
|
+
<div className="flex justify-between items-center h-full">
|
|
14
|
+
<Link to="/" className="text-xl font-bold text-blue-600">
|
|
15
|
+
Recurring Rabbit
|
|
16
|
+
</Link>
|
|
17
|
+
<div className="flex items-center gap-4">
|
|
18
|
+
{userName && <span className="text-gray-700">{userName}</span>}
|
|
19
|
+
{onSignOut && (
|
|
20
|
+
<Button variant="outline" size="sm" onClick={onSignOut}>
|
|
21
|
+
Sign Out
|
|
22
|
+
</Button>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
interface SidebarProps {
|
|
4
|
+
currentPath?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Sidebar({ currentPath }: SidebarProps) {
|
|
8
|
+
const navItems = [
|
|
9
|
+
{ to: '/dashboard', label: 'Dashboard' },
|
|
10
|
+
{ to: '/dashboard/settings', label: 'Settings' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<aside className="w-64 bg-white border-r border-gray-200 min-h-[calc(100vh-64px)]">
|
|
15
|
+
<nav className="p-4">
|
|
16
|
+
<ul className="space-y-2">
|
|
17
|
+
{navItems.map((item) => {
|
|
18
|
+
const isActive = currentPath?.startsWith(item.to)
|
|
19
|
+
return (
|
|
20
|
+
<li key={item.to}>
|
|
21
|
+
<Link
|
|
22
|
+
to={item.to}
|
|
23
|
+
className={`block px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
24
|
+
isActive
|
|
25
|
+
? 'bg-blue-50 text-blue-700'
|
|
26
|
+
: 'text-gray-700 hover:bg-gray-100'
|
|
27
|
+
}`}
|
|
28
|
+
>
|
|
29
|
+
{item.label}
|
|
30
|
+
</Link>
|
|
31
|
+
</li>
|
|
32
|
+
)
|
|
33
|
+
})}
|
|
34
|
+
</ul>
|
|
35
|
+
</nav>
|
|
36
|
+
</aside>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
15
|
+
outline:
|
|
16
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
20
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: 'h-10 px-4 py-2',
|
|
24
|
+
sm: 'h-9 rounded-md px-3',
|
|
25
|
+
lg: 'h-11 rounded-md px-8',
|
|
26
|
+
icon: 'h-10 w-10',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: 'default',
|
|
31
|
+
size: 'default',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export interface ButtonProps
|
|
37
|
+
extends
|
|
38
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
39
|
+
VariantProps<typeof buttonVariants> {
|
|
40
|
+
asChild?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
45
|
+
const Comp = asChild ? Slot : 'button'
|
|
46
|
+
return (
|
|
47
|
+
<Comp
|
|
48
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
|
+
ref={ref}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
Button.displayName = 'Button'
|
|
56
|
+
|
|
57
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = 'Card'
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = 'CardHeader'
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLParagraphElement,
|
|
34
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<h3
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
'text-2xl font-semibold leading-none tracking-tight',
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
))
|
|
45
|
+
CardTitle.displayName = 'CardTitle'
|
|
46
|
+
|
|
47
|
+
const CardDescription = React.forwardRef<
|
|
48
|
+
HTMLParagraphElement,
|
|
49
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
50
|
+
>(({ className, ...props }, ref) => (
|
|
51
|
+
<p
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
))
|
|
57
|
+
CardDescription.displayName = 'CardDescription'
|
|
58
|
+
|
|
59
|
+
const CardContent = React.forwardRef<
|
|
60
|
+
HTMLDivElement,
|
|
61
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
64
|
+
))
|
|
65
|
+
CardContent.displayName = 'CardContent'
|
|
66
|
+
|
|
67
|
+
const CardFooter = React.forwardRef<
|
|
68
|
+
HTMLDivElement,
|
|
69
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
))
|
|
77
|
+
CardFooter.displayName = 'CardFooter'
|
|
78
|
+
|
|
79
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|