@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/commands/create.d.ts +7 -0
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +32 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/template-generator.d.ts +9 -0
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +15 -1
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/templates/web/ui-auth-ai/template/.env.example +18 -0
- package/dist/templates/web/ui-auth-ai/template/README.md +170 -0
- package/dist/templates/web/ui-auth-ai/template/next.config.js +13 -0
- package/dist/templates/web/ui-auth-ai/template/package.json +39 -0
- package/dist/templates/web/ui-auth-ai/template/postcss.config.js +6 -0
- package/dist/templates/web/ui-auth-ai/template/src/app/globals.css +40 -0
- package/dist/templates/web/ui-auth-ai/template/src/app/layout.tsx +41 -0
- package/dist/templates/web/ui-auth-ai/template/src/app/page.tsx +193 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ai/audio-generator.tsx +141 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ai/video-generator.tsx +158 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/auth/login-form.tsx +106 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ui/button.tsx +44 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ui/input.tsx +24 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ui/label.tsx +21 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ui/select.tsx +37 -0
- package/dist/templates/web/ui-auth-ai/template/src/components/ui/textarea.tsx +23 -0
- package/dist/templates/web/ui-auth-ai/template/src/hooks/useAuth.ts +1 -0
- package/dist/templates/web/ui-auth-ai/template/src/lib/supabase.ts +8 -0
- package/dist/templates/web/ui-auth-ai/template/src/lib/utils.ts +5 -0
- package/dist/templates/web/ui-auth-ai/template/src/providers/auth-provider.tsx +55 -0
- package/dist/templates/web/ui-auth-ai/template/tailwind.config.js +22 -0
- package/dist/templates/web/ui-auth-ai/template/tsconfig.json +28 -0
- package/dist/templates/web/ui-auth-payments-audio/template/src/app/page.tsx +15 -15
- package/package.json +1 -1
- package/src/templates/web/ui-auth-ai/template/.env.example +18 -0
- package/src/templates/web/ui-auth-ai/template/README.md +170 -0
- package/src/templates/web/ui-auth-ai/template/next.config.js +13 -0
- package/src/templates/web/ui-auth-ai/template/package.json +39 -0
- package/src/templates/web/ui-auth-ai/template/postcss.config.js +6 -0
- package/src/templates/web/ui-auth-ai/template/src/app/globals.css +40 -0
- package/src/templates/web/ui-auth-ai/template/src/app/layout.tsx +41 -0
- package/src/templates/web/ui-auth-ai/template/src/app/page.tsx +193 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ai/audio-generator.tsx +141 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ai/video-generator.tsx +158 -0
- package/src/templates/web/ui-auth-ai/template/src/components/auth/login-form.tsx +106 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ui/button.tsx +44 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ui/input.tsx +24 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ui/label.tsx +21 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ui/select.tsx +37 -0
- package/src/templates/web/ui-auth-ai/template/src/components/ui/textarea.tsx +23 -0
- package/src/templates/web/ui-auth-ai/template/src/hooks/useAuth.ts +1 -0
- package/src/templates/web/ui-auth-ai/template/src/lib/supabase.ts +8 -0
- package/src/templates/web/ui-auth-ai/template/src/lib/utils.ts +5 -0
- package/src/templates/web/ui-auth-ai/template/src/providers/auth-provider.tsx +55 -0
- package/src/templates/web/ui-auth-ai/template/tailwind.config.js +22 -0
- package/src/templates/web/ui-auth-ai/template/tsconfig.json +28 -0
- 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 }
|