@digilogiclabs/create-saas-app 1.10.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/commands/create.d.ts +7 -0
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +32 -1
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +2 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/generators/template-generator.d.ts +9 -0
  10. package/dist/generators/template-generator.d.ts.map +1 -1
  11. package/dist/generators/template-generator.js +15 -1
  12. package/dist/generators/template-generator.js.map +1 -1
  13. package/dist/templates/web/ui-auth-ai/template/.env.example +18 -0
  14. package/dist/templates/web/ui-auth-ai/template/README.md +170 -0
  15. package/dist/templates/web/ui-auth-ai/template/next.config.js +13 -0
  16. package/dist/templates/web/ui-auth-ai/template/package.json +39 -0
  17. package/dist/templates/web/ui-auth-ai/template/postcss.config.js +6 -0
  18. package/dist/templates/web/ui-auth-ai/template/src/app/globals.css +40 -0
  19. package/dist/templates/web/ui-auth-ai/template/src/app/layout.tsx +41 -0
  20. package/dist/templates/web/ui-auth-ai/template/src/app/page.tsx +193 -0
  21. package/dist/templates/web/ui-auth-ai/template/src/components/ai/audio-generator.tsx +141 -0
  22. package/dist/templates/web/ui-auth-ai/template/src/components/ai/video-generator.tsx +158 -0
  23. package/dist/templates/web/ui-auth-ai/template/src/components/auth/login-form.tsx +106 -0
  24. package/dist/templates/web/ui-auth-ai/template/src/components/ui/button.tsx +44 -0
  25. package/dist/templates/web/ui-auth-ai/template/src/components/ui/input.tsx +24 -0
  26. package/dist/templates/web/ui-auth-ai/template/src/components/ui/label.tsx +21 -0
  27. package/dist/templates/web/ui-auth-ai/template/src/components/ui/select.tsx +37 -0
  28. package/dist/templates/web/ui-auth-ai/template/src/components/ui/textarea.tsx +23 -0
  29. package/dist/templates/web/ui-auth-ai/template/src/hooks/useAuth.ts +1 -0
  30. package/dist/templates/web/ui-auth-ai/template/src/lib/supabase.ts +8 -0
  31. package/dist/templates/web/ui-auth-ai/template/src/lib/utils.ts +5 -0
  32. package/dist/templates/web/ui-auth-ai/template/src/providers/auth-provider.tsx +55 -0
  33. package/dist/templates/web/ui-auth-ai/template/tailwind.config.js +22 -0
  34. package/dist/templates/web/ui-auth-ai/template/tsconfig.json +28 -0
  35. package/dist/templates/web/ui-auth-payments-audio/template/src/app/page.tsx +15 -15
  36. package/package.json +1 -1
  37. package/src/templates/web/ui-auth-ai/template/.env.example +18 -0
  38. package/src/templates/web/ui-auth-ai/template/README.md +170 -0
  39. package/src/templates/web/ui-auth-ai/template/next.config.js +13 -0
  40. package/src/templates/web/ui-auth-ai/template/package.json +39 -0
  41. package/src/templates/web/ui-auth-ai/template/postcss.config.js +6 -0
  42. package/src/templates/web/ui-auth-ai/template/src/app/globals.css +40 -0
  43. package/src/templates/web/ui-auth-ai/template/src/app/layout.tsx +41 -0
  44. package/src/templates/web/ui-auth-ai/template/src/app/page.tsx +193 -0
  45. package/src/templates/web/ui-auth-ai/template/src/components/ai/audio-generator.tsx +141 -0
  46. package/src/templates/web/ui-auth-ai/template/src/components/ai/video-generator.tsx +158 -0
  47. package/src/templates/web/ui-auth-ai/template/src/components/auth/login-form.tsx +106 -0
  48. package/src/templates/web/ui-auth-ai/template/src/components/ui/button.tsx +44 -0
  49. package/src/templates/web/ui-auth-ai/template/src/components/ui/input.tsx +24 -0
  50. package/src/templates/web/ui-auth-ai/template/src/components/ui/label.tsx +21 -0
  51. package/src/templates/web/ui-auth-ai/template/src/components/ui/select.tsx +37 -0
  52. package/src/templates/web/ui-auth-ai/template/src/components/ui/textarea.tsx +23 -0
  53. package/src/templates/web/ui-auth-ai/template/src/hooks/useAuth.ts +1 -0
  54. package/src/templates/web/ui-auth-ai/template/src/lib/supabase.ts +8 -0
  55. package/src/templates/web/ui-auth-ai/template/src/lib/utils.ts +5 -0
  56. package/src/templates/web/ui-auth-ai/template/src/providers/auth-provider.tsx +55 -0
  57. package/src/templates/web/ui-auth-ai/template/tailwind.config.js +22 -0
  58. package/src/templates/web/ui-auth-ai/template/tsconfig.json +28 -0
  59. package/src/templates/web/ui-auth-payments-audio/template/src/app/page.tsx +15 -15
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAuth } from '@/hooks/useAuth'
5
+ import { LoginForm } from '@/components/auth/login-form'
6
+ import { Button } from '@/components/ui/button'
7
+ {{#ai.enabled}}
8
+ import { ChatPanel } from '@digilogiclabs/saas-factory-ai/react'
9
+ {{#ai.hasAudio}}
10
+ import { AudioGenerator } from '@/components/ai/audio-generator'
11
+ {{/ai.hasAudio}}
12
+ {{#ai.hasVideo}}
13
+ import { VideoGenerator } from '@/components/ai/video-generator'
14
+ {{/ai.hasVideo}}
15
+ {{/ai.enabled}}
16
+
17
+ export default function HomePage() {
18
+ const { user, signOut } = useAuth()
19
+ {{#ai.enabled}}
20
+ const [activeTab, setActiveTab] = useState<'chat' | 'audio' | 'video'>('chat')
21
+ {{/ai.enabled}}
22
+
23
+ if (!user) {
24
+ return (
25
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
26
+ <div className="max-w-md w-full space-y-8">
27
+ <div className="text-center">
28
+ <h2 className="text-3xl font-extrabold text-gray-900">
29
+ Welcome to {{titleCaseName}}
30
+ </h2>
31
+ {{#ai.enabled}}
32
+ <p className="mt-2 text-sm text-gray-600">
33
+ AI-powered SaaS application with {{ai.capabilities.join(', ')}} capabilities
34
+ </p>
35
+ {{/ai.enabled}}
36
+ {{^ai.enabled}}
37
+ <p className="mt-2 text-sm text-gray-600">
38
+ Your modern SaaS application
39
+ </p>
40
+ {{/ai.enabled}}
41
+ </div>
42
+ <LoginForm />
43
+ </div>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ return (
49
+ <div className="min-h-screen bg-gray-50">
50
+ {/* Header */}
51
+ <header className="bg-white shadow">
52
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
53
+ <div className="flex justify-between items-center py-6">
54
+ <div className="flex items-center">
55
+ <h1 className="text-3xl font-bold text-gray-900">
56
+ {{titleCaseName}}
57
+ </h1>
58
+ {{#ai.enabled}}
59
+ <span className="ml-3 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
60
+ AI-Powered
61
+ </span>
62
+ {{/ai.enabled}}
63
+ </div>
64
+ <div className="flex items-center space-x-4">
65
+ <span className="text-sm text-gray-700">
66
+ Welcome, {user.email}
67
+ </span>
68
+ <Button onClick={() => signOut()}>
69
+ Sign Out
70
+ </Button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </header>
75
+
76
+ {/* Main Content */}
77
+ <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
78
+ {{#ai.enabled}}
79
+ {/* AI Capabilities Navigation */}
80
+ <div className="px-4 py-6 sm:px-0">
81
+ <div className="border-b border-gray-200">
82
+ <nav className="-mb-px flex space-x-8">
83
+ {{#ai.hasText}}
84
+ <button
85
+ onClick={() => setActiveTab('chat')}
86
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
87
+ activeTab === 'chat'
88
+ ? 'border-blue-500 text-blue-600'
89
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
90
+ }`}
91
+ >
92
+ 💬 AI Chat
93
+ </button>
94
+ {{/ai.hasText}}
95
+ {{#ai.hasAudio}}
96
+ <button
97
+ onClick={() => setActiveTab('audio')}
98
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
99
+ activeTab === 'audio'
100
+ ? 'border-blue-500 text-blue-600'
101
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
102
+ }`}
103
+ >
104
+ 🎵 Audio Generation
105
+ </button>
106
+ {{/ai.hasAudio}}
107
+ {{#ai.hasVideo}}
108
+ <button
109
+ onClick={() => setActiveTab('video')}
110
+ className={`py-2 px-1 border-b-2 font-medium text-sm ${
111
+ activeTab === 'video'
112
+ ? 'border-blue-500 text-blue-600'
113
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
114
+ }`}
115
+ >
116
+ 🎥 Video Generation
117
+ </button>
118
+ {{/ai.hasVideo}}
119
+ </nav>
120
+ </div>
121
+ </div>
122
+
123
+ {/* AI Content */}
124
+ <div className="px-4 py-6 sm:px-0">
125
+ {{#ai.hasText}}
126
+ {activeTab === 'chat' && (
127
+ <div className="bg-white shadow rounded-lg">
128
+ <div className="px-4 py-5 sm:p-6">
129
+ <h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
130
+ AI Assistant powered by {{ai.provider}}
131
+ </h3>
132
+ <ChatPanel
133
+ className="h-96"
134
+ placeholder="Ask me anything..."
135
+ onError={(error) => console.error('Chat error:', error)}
136
+ />
137
+ </div>
138
+ </div>
139
+ )}
140
+ {{/ai.hasText}}
141
+
142
+ {{#ai.hasAudio}}
143
+ {activeTab === 'audio' && (
144
+ <div className="bg-white shadow rounded-lg">
145
+ <div className="px-4 py-5 sm:p-6">
146
+ <h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
147
+ AI Audio Generation
148
+ </h3>
149
+ <AudioGenerator />
150
+ </div>
151
+ </div>
152
+ )}
153
+ {{/ai.hasAudio}}
154
+
155
+ {{#ai.hasVideo}}
156
+ {activeTab === 'video' && (
157
+ <div className="bg-white shadow rounded-lg">
158
+ <div className="px-4 py-5 sm:p-6">
159
+ <h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
160
+ AI Video Generation
161
+ </h3>
162
+ <VideoGenerator />
163
+ </div>
164
+ </div>
165
+ )}
166
+ {{/ai.hasVideo}}
167
+ </div>
168
+ {{/ai.enabled}}
169
+
170
+ {{^ai.enabled}}
171
+ {/* Non-AI Content */}
172
+ <div className="px-4 py-6 sm:px-0">
173
+ <div className="bg-white shadow rounded-lg">
174
+ <div className="px-4 py-5 sm:p-6">
175
+ <h3 className="text-lg leading-6 font-medium text-gray-900">
176
+ Welcome to your SaaS application
177
+ </h3>
178
+ <p className="mt-1 text-sm text-gray-600">
179
+ This is your dashboard. Start building your application features here.
180
+ </p>
181
+ <div className="mt-5">
182
+ <Button>
183
+ Get Started
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ {{/ai.enabled}}
190
+ </main>
191
+ </div>
192
+ )
193
+ }
@@ -0,0 +1,141 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useGenerateAudio, useJobStatus } from '@digilogiclabs/saas-factory-ai/react'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
9
+ import { JobProgress } from '@digilogiclabs/saas-factory-ai/react'
10
+
11
+ export function AudioGenerator() {
12
+ const [prompt, setPrompt] = useState('')
13
+ const [style, setStyle] = useState('lofi')
14
+ const [duration, setDuration] = useState(30)
15
+ const [jobId, setJobId] = useState<string | null>(null)
16
+ const [audioUrl, setAudioUrl] = useState<string | null>(null)
17
+
18
+ const { generate, loading, error } = useGenerateAudio()
19
+ const { status } = useJobStatus(jobId)
20
+
21
+ const handleGenerate = async () => {
22
+ if (!prompt.trim()) return
23
+
24
+ try {
25
+ const result = await generate(prompt, {
26
+ style,
27
+ durationSec: duration,
28
+ format: 'mp3'
29
+ })
30
+ setJobId(result.jobId)
31
+ setAudioUrl(null)
32
+ } catch (err) {
33
+ console.error('Audio generation failed:', err)
34
+ }
35
+ }
36
+
37
+ const handleJobComplete = (result: any) => {
38
+ if (result.url) {
39
+ setAudioUrl(result.url)
40
+ }
41
+ }
42
+
43
+ return (
44
+ <div className="space-y-6">
45
+ <div className="grid grid-cols-1 gap-4">
46
+ <div>
47
+ <Label htmlFor="prompt">Audio Description</Label>
48
+ <Input
49
+ id="prompt"
50
+ value={prompt}
51
+ onChange={(e) => setPrompt(e.target.value)}
52
+ placeholder="Describe the audio you want to generate..."
53
+ className="mt-1"
54
+ />
55
+ </div>
56
+
57
+ <div className="grid grid-cols-2 gap-4">
58
+ <div>
59
+ <Label htmlFor="style">Style</Label>
60
+ <Select value={style} onValueChange={setStyle}>
61
+ <SelectTrigger className="mt-1">
62
+ <SelectValue />
63
+ </SelectTrigger>
64
+ <SelectContent>
65
+ <SelectItem value="lofi">Lo-Fi</SelectItem>
66
+ <SelectItem value="jazz">Jazz</SelectItem>
67
+ <SelectItem value="classical">Classical</SelectItem>
68
+ <SelectItem value="electronic">Electronic</SelectItem>
69
+ <SelectItem value="ambient">Ambient</SelectItem>
70
+ </SelectContent>
71
+ </Select>
72
+ </div>
73
+
74
+ <div>
75
+ <Label htmlFor="duration">Duration (seconds)</Label>
76
+ <Input
77
+ id="duration"
78
+ type="number"
79
+ value={duration}
80
+ onChange={(e) => setDuration(parseInt(e.target.value) || 30)}
81
+ min={10}
82
+ max={300}
83
+ className="mt-1"
84
+ />
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div className="flex justify-center">
90
+ <Button
91
+ onClick={handleGenerate}
92
+ disabled={loading || !prompt.trim() || !!jobId}
93
+ className="px-8"
94
+ >
95
+ {loading ? 'Starting Generation...' : 'Generate Audio'}
96
+ </Button>
97
+ </div>
98
+
99
+ {error && (
100
+ <div className="p-4 bg-red-50 border border-red-200 rounded-md">
101
+ <p className="text-sm text-red-600">{error.message}</p>
102
+ </div>
103
+ )}
104
+
105
+ {jobId && (
106
+ <div className="space-y-4">
107
+ <JobProgress
108
+ jobId={jobId}
109
+ onComplete={handleJobComplete}
110
+ className="p-4 bg-gray-50 rounded-md"
111
+ />
112
+ </div>
113
+ )}
114
+
115
+ {audioUrl && (
116
+ <div className="p-4 bg-green-50 border border-green-200 rounded-md">
117
+ <h4 className="font-medium text-green-800 mb-3">Generated Audio</h4>
118
+ <audio controls className="w-full">
119
+ <source src={audioUrl} type="audio/mpeg" />
120
+ Your browser does not support the audio element.
121
+ </audio>
122
+ <div className="mt-3">
123
+ <Button
124
+ variant="outline"
125
+ onClick={() => {
126
+ const link = document.createElement('a')
127
+ link.href = audioUrl
128
+ link.download = 'generated-audio.mp3'
129
+ document.body.appendChild(link)
130
+ link.click()
131
+ document.body.removeChild(link)
132
+ }}
133
+ >
134
+ Download Audio
135
+ </Button>
136
+ </div>
137
+ </div>
138
+ )}
139
+ </div>
140
+ )
141
+ }
@@ -0,0 +1,158 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useGenerateVideo, useJobStatus } from '@digilogiclabs/saas-factory-ai/react'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
9
+ import { Textarea } from '@/components/ui/textarea'
10
+ import { JobProgress } from '@digilogiclabs/saas-factory-ai/react'
11
+
12
+ export function VideoGenerator() {
13
+ const [prompt, setPrompt] = useState('')
14
+ const [style, setStyle] = useState('realistic')
15
+ const [duration, setDuration] = useState(10)
16
+ const [resolution, setResolution] = useState('720p')
17
+ const [jobId, setJobId] = useState<string | null>(null)
18
+ const [videoUrl, setVideoUrl] = useState<string | null>(null)
19
+
20
+ const { generate, loading, error } = useGenerateVideo()
21
+ const { status } = useJobStatus(jobId)
22
+
23
+ const handleGenerate = async () => {
24
+ if (!prompt.trim()) return
25
+
26
+ try {
27
+ const result = await generate(prompt, {
28
+ style,
29
+ durationSec: duration,
30
+ resolution,
31
+ format: 'mp4'
32
+ })
33
+ setJobId(result.jobId)
34
+ setVideoUrl(null)
35
+ } catch (err) {
36
+ console.error('Video generation failed:', err)
37
+ }
38
+ }
39
+
40
+ const handleJobComplete = (result: any) => {
41
+ if (result.url) {
42
+ setVideoUrl(result.url)
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div className="space-y-6">
48
+ <div className="grid grid-cols-1 gap-4">
49
+ <div>
50
+ <Label htmlFor="video-prompt">Video Description</Label>
51
+ <Textarea
52
+ id="video-prompt"
53
+ value={prompt}
54
+ onChange={(e) => setPrompt(e.target.value)}
55
+ placeholder="Describe the video you want to generate in detail..."
56
+ className="mt-1 min-h-[100px]"
57
+ />
58
+ </div>
59
+
60
+ <div className="grid grid-cols-3 gap-4">
61
+ <div>
62
+ <Label htmlFor="video-style">Style</Label>
63
+ <Select value={style} onValueChange={setStyle}>
64
+ <SelectTrigger className="mt-1">
65
+ <SelectValue />
66
+ </SelectTrigger>
67
+ <SelectContent>
68
+ <SelectItem value="realistic">Realistic</SelectItem>
69
+ <SelectItem value="animated">Animated</SelectItem>
70
+ <SelectItem value="cinematic">Cinematic</SelectItem>
71
+ <SelectItem value="artistic">Artistic</SelectItem>
72
+ <SelectItem value="documentary">Documentary</SelectItem>
73
+ </SelectContent>
74
+ </Select>
75
+ </div>
76
+
77
+ <div>
78
+ <Label htmlFor="video-duration">Duration (seconds)</Label>
79
+ <Input
80
+ id="video-duration"
81
+ type="number"
82
+ value={duration}
83
+ onChange={(e) => setDuration(parseInt(e.target.value) || 10)}
84
+ min={5}
85
+ max={60}
86
+ className="mt-1"
87
+ />
88
+ </div>
89
+
90
+ <div>
91
+ <Label htmlFor="video-resolution">Resolution</Label>
92
+ <Select value={resolution} onValueChange={setResolution}>
93
+ <SelectTrigger className="mt-1">
94
+ <SelectValue />
95
+ </SelectTrigger>
96
+ <SelectContent>
97
+ <SelectItem value="480p">480p (SD)</SelectItem>
98
+ <SelectItem value="720p">720p (HD)</SelectItem>
99
+ <SelectItem value="1080p">1080p (Full HD)</SelectItem>
100
+ </SelectContent>
101
+ </Select>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div className="flex justify-center">
107
+ <Button
108
+ onClick={handleGenerate}
109
+ disabled={loading || !prompt.trim() || !!jobId}
110
+ className="px-8"
111
+ >
112
+ {loading ? 'Starting Generation...' : 'Generate Video'}
113
+ </Button>
114
+ </div>
115
+
116
+ {error && (
117
+ <div className="p-4 bg-red-50 border border-red-200 rounded-md">
118
+ <p className="text-sm text-red-600">{error.message}</p>
119
+ </div>
120
+ )}
121
+
122
+ {jobId && (
123
+ <div className="space-y-4">
124
+ <JobProgress
125
+ jobId={jobId}
126
+ onComplete={handleJobComplete}
127
+ className="p-4 bg-gray-50 rounded-md"
128
+ />
129
+ </div>
130
+ )}
131
+
132
+ {videoUrl && (
133
+ <div className="p-4 bg-green-50 border border-green-200 rounded-md">
134
+ <h4 className="font-medium text-green-800 mb-3">Generated Video</h4>
135
+ <video controls className="w-full rounded-md" style={{ maxHeight: '400px' }}>
136
+ <source src={videoUrl} type="video/mp4" />
137
+ Your browser does not support the video element.
138
+ </video>
139
+ <div className="mt-3">
140
+ <Button
141
+ variant="outline"
142
+ onClick={() => {
143
+ const link = document.createElement('a')
144
+ link.href = videoUrl
145
+ link.download = 'generated-video.mp4'
146
+ document.body.appendChild(link)
147
+ link.click()
148
+ document.body.removeChild(link)
149
+ }}
150
+ >
151
+ Download Video
152
+ </Button>
153
+ </div>
154
+ </div>
155
+ )}
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { createClient } from '@/lib/supabase'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+
9
+ export function LoginForm() {
10
+ const [email, setEmail] = useState('')
11
+ const [password, setPassword] = useState('')
12
+ const [loading, setLoading] = useState(false)
13
+ const [isSignUp, setIsSignUp] = useState(false)
14
+ const [message, setMessage] = useState('')
15
+ const supabase = createClient()
16
+
17
+ const handleSubmit = async (e: React.FormEvent) => {
18
+ e.preventDefault()
19
+ setLoading(true)
20
+ setMessage('')
21
+
22
+ try {
23
+ if (isSignUp) {
24
+ const { error } = await supabase.auth.signUp({
25
+ email,
26
+ password,
27
+ })
28
+ if (error) throw error
29
+ setMessage('Check your email for the confirmation link!')
30
+ } else {
31
+ const { error } = await supabase.auth.signInWithPassword({
32
+ email,
33
+ password,
34
+ })
35
+ if (error) throw error
36
+ }
37
+ } catch (error: any) {
38
+ setMessage(error.message)
39
+ } finally {
40
+ setLoading(false)
41
+ }
42
+ }
43
+
44
+ return (
45
+ <div className="bg-white py-8 px-6 shadow rounded-lg">
46
+ <form onSubmit={handleSubmit} className="space-y-6">
47
+ <div>
48
+ <Label htmlFor="email">Email address</Label>
49
+ <Input
50
+ id="email"
51
+ name="email"
52
+ type="email"
53
+ autoComplete="email"
54
+ required
55
+ value={email}
56
+ onChange={(e) => setEmail(e.target.value)}
57
+ className="mt-1"
58
+ />
59
+ </div>
60
+
61
+ <div>
62
+ <Label htmlFor="password">Password</Label>
63
+ <Input
64
+ id="password"
65
+ name="password"
66
+ type="password"
67
+ autoComplete={isSignUp ? 'new-password' : 'current-password'}
68
+ required
69
+ value={password}
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ className="mt-1"
72
+ />
73
+ </div>
74
+
75
+ {message && (
76
+ <div className={`text-sm ${message.includes('Check your email') ? 'text-green-600' : 'text-red-600'}`}>
77
+ {message}
78
+ </div>
79
+ )}
80
+
81
+ <div>
82
+ <Button
83
+ type="submit"
84
+ disabled={loading}
85
+ className="w-full"
86
+ >
87
+ {loading ? 'Loading...' : isSignUp ? 'Sign Up' : 'Sign In'}
88
+ </Button>
89
+ </div>
90
+
91
+ <div className="text-center">
92
+ <button
93
+ type="button"
94
+ onClick={() => {
95
+ setIsSignUp(!isSignUp)
96
+ setMessage('')
97
+ }}
98
+ className="text-sm text-blue-600 hover:text-blue-500"
99
+ >
100
+ {isSignUp ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
101
+ </button>
102
+ </div>
103
+ </form>
104
+ </div>
105
+ )
106
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export interface ButtonProps
5
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
7
+ size?: 'default' | 'sm' | 'lg' | 'icon'
8
+ }
9
+
10
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
11
+ ({ className, variant = 'default', size = 'default', ...props }, ref) => {
12
+ const variants = {
13
+ default: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
14
+ destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
15
+ outline: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
16
+ secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
17
+ ghost: 'hover:bg-gray-100 text-gray-700 focus:ring-gray-500',
18
+ link: 'underline-offset-4 hover:underline text-blue-600'
19
+ }
20
+
21
+ const sizes = {
22
+ default: 'h-10 px-4 py-2',
23
+ sm: 'h-8 px-3 text-sm',
24
+ lg: 'h-12 px-8 text-lg',
25
+ icon: 'h-10 w-10'
26
+ }
27
+
28
+ return (
29
+ <button
30
+ className={cn(
31
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
32
+ variants[variant],
33
+ sizes[size],
34
+ className
35
+ )}
36
+ ref={ref}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+ )
42
+ Button.displayName = "Button"
43
+
44
+ export { Button }
@@ -0,0 +1,24 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export interface InputProps
5
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
6
+
7
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
8
+ ({ className, type, ...props }, ref) => {
9
+ return (
10
+ <input
11
+ type={type}
12
+ className={cn(
13
+ "flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+ )
22
+ Input.displayName = "Input"
23
+
24
+ export { Input }
@@ -0,0 +1,21 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export interface LabelProps
5
+ extends React.LabelHTMLAttributes<HTMLLabelElement> {}
6
+
7
+ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
8
+ ({ className, ...props }, ref) => (
9
+ <label
10
+ ref={ref}
11
+ className={cn(
12
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ )
19
+ Label.displayName = "Label"
20
+
21
+ export { Label }