@realtimex/email-automator 2.2.0 → 2.3.0
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/api/server.ts +4 -8
- package/api/src/config/index.ts +6 -3
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +7 -11
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +88 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -5
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { User, Shield, Database, Save, Loader2, LogOut, Trash2, Settings, CheckCircle, XCircle, ExternalLink, Key, Camera, Volume2, VolumeX } from 'lucide-react';
|
|
3
|
-
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from './ui/card';
|
|
4
|
-
import { Button } from './ui/button';
|
|
5
|
-
import { Input } from './ui/input';
|
|
6
|
-
import { Label } from './ui/label';
|
|
7
|
-
import { Alert, AlertDescription } from './ui/alert';
|
|
8
|
-
import { useApp } from '../context/AppContext';
|
|
9
|
-
import { supabase } from '../lib/supabase';
|
|
10
|
-
import { toast } from './Toast';
|
|
11
|
-
import { LoadingSpinner } from './LoadingSpinner';
|
|
12
|
-
import { getSupabaseConfig, clearSupabaseConfig, getConfigSource } from '../lib/supabase-config';
|
|
13
|
-
import { SetupWizard } from './SetupWizard';
|
|
14
|
-
import { sounds } from '../lib/sounds';
|
|
15
|
-
|
|
16
|
-
type SettingsTab = 'profile' | 'security' | 'database';
|
|
17
|
-
|
|
18
|
-
export function AccountSettingsPage() {
|
|
19
|
-
const [activeTab, setActiveTab] = useState<SettingsTab>('profile');
|
|
20
|
-
const { state, actions } = useApp();
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
actions.fetchProfile();
|
|
24
|
-
}, []);
|
|
25
|
-
|
|
26
|
-
const tabs = [
|
|
27
|
-
{ id: 'profile', label: 'Profile', icon: User },
|
|
28
|
-
{ id: 'security', label: 'Security', icon: Shield },
|
|
29
|
-
{ id: 'database', label: 'Supabase', icon: Database },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div className="max-w-4xl mx-auto space-y-6 animate-in fade-in duration-500">
|
|
34
|
-
<div className="flex flex-col gap-2">
|
|
35
|
-
<h1 className="text-3xl font-bold tracking-tight">Account Settings</h1>
|
|
36
|
-
<p className="text-muted-foreground">Manage your profile, security, and connection preferences.</p>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<div className="flex flex-col md:flex-row gap-8">
|
|
40
|
-
{/* Sidebar Tabs */}
|
|
41
|
-
<aside className="w-full md:w-64 space-y-1">
|
|
42
|
-
{tabs.map((tab) => {
|
|
43
|
-
const Icon = tab.icon;
|
|
44
|
-
const isActive = activeTab === tab.id;
|
|
45
|
-
return (
|
|
46
|
-
<button
|
|
47
|
-
key={tab.id}
|
|
48
|
-
onClick={() => setActiveTab(tab.id as SettingsTab)}
|
|
49
|
-
className={`w-full flex items-center gap-3 px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
|
50
|
-
isActive
|
|
51
|
-
? 'bg-primary text-primary-foreground'
|
|
52
|
-
: 'hover:bg-secondary text-muted-foreground'
|
|
53
|
-
}`}
|
|
54
|
-
>
|
|
55
|
-
<Icon className="w-4 h-4" />
|
|
56
|
-
{tab.label}
|
|
57
|
-
</button>
|
|
58
|
-
);
|
|
59
|
-
})}
|
|
60
|
-
</aside>
|
|
61
|
-
|
|
62
|
-
{/* Content Area */}
|
|
63
|
-
<div className="flex-1 space-y-6">
|
|
64
|
-
{activeTab === 'profile' && <ProfileSection />}
|
|
65
|
-
{activeTab === 'security' && <SecuritySection />}
|
|
66
|
-
{activeTab === 'database' && <DatabaseSection />}
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function ProfileSection() {
|
|
74
|
-
const { state, actions } = useApp();
|
|
75
|
-
const [firstName, setFirstName] = useState('');
|
|
76
|
-
const [lastName, setLastName] = useState('');
|
|
77
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
78
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
79
|
-
const [soundsEnabled, setSoundsEnabled] = useState(sounds.isEnabled());
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (state.profile) {
|
|
83
|
-
setFirstName(state.profile.first_name || '');
|
|
84
|
-
setLastName(state.profile.last_name || '');
|
|
85
|
-
}
|
|
86
|
-
}, [state.profile]);
|
|
87
|
-
|
|
88
|
-
const toggleSounds = () => {
|
|
89
|
-
const next = !soundsEnabled;
|
|
90
|
-
sounds.setEnabled(next);
|
|
91
|
-
setSoundsEnabled(next);
|
|
92
|
-
if (next) {
|
|
93
|
-
sounds.playSuccess();
|
|
94
|
-
toast.success('Sound effects enabled');
|
|
95
|
-
} else {
|
|
96
|
-
toast.info('Sound effects disabled');
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const handleSave = async () => {
|
|
101
|
-
setIsSaving(true);
|
|
102
|
-
const success = await actions.updateProfile({
|
|
103
|
-
first_name: firstName,
|
|
104
|
-
last_name: lastName
|
|
105
|
-
});
|
|
106
|
-
setIsSaving(false);
|
|
107
|
-
if (success) {
|
|
108
|
-
toast.success('Profile updated');
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
113
|
-
const file = e.target.files?.[0];
|
|
114
|
-
if (!file || !state.user) return;
|
|
115
|
-
|
|
116
|
-
setIsUploading(true);
|
|
117
|
-
try {
|
|
118
|
-
const fileExt = file.name.split('.').pop();
|
|
119
|
-
const fileName = `${state.user.id}/avatar.${fileExt}`;
|
|
120
|
-
|
|
121
|
-
// Upload to Supabase Storage
|
|
122
|
-
const { data, error: uploadError } = await supabase.storage
|
|
123
|
-
.from('avatars')
|
|
124
|
-
.upload(fileName, file, { upsert: true });
|
|
125
|
-
|
|
126
|
-
if (uploadError) throw uploadError;
|
|
127
|
-
|
|
128
|
-
// Get public URL
|
|
129
|
-
const { data: { publicUrl } } = supabase.storage
|
|
130
|
-
.from('avatars')
|
|
131
|
-
.getPublicUrl(fileName);
|
|
132
|
-
|
|
133
|
-
// Update profile
|
|
134
|
-
await actions.updateProfile({ avatar_url: publicUrl });
|
|
135
|
-
toast.success('Avatar updated');
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error('Avatar upload error:', error);
|
|
138
|
-
toast.error('Failed to upload avatar');
|
|
139
|
-
} finally {
|
|
140
|
-
setIsUploading(false);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<Card>
|
|
146
|
-
<CardHeader>
|
|
147
|
-
<CardTitle>Profile Information</CardTitle>
|
|
148
|
-
<CardDescription>Update your personal details and avatar.</CardDescription>
|
|
149
|
-
</CardHeader>
|
|
150
|
-
<CardContent className="space-y-6">
|
|
151
|
-
<div className="flex flex-col items-center sm:flex-row sm:items-start gap-6">
|
|
152
|
-
<div className="relative group">
|
|
153
|
-
<div className="w-24 h-24 rounded-full bg-secondary overflow-hidden border-2 border-border flex items-center justify-center">
|
|
154
|
-
{state.profile?.avatar_url ? (
|
|
155
|
-
<img src={state.profile.avatar_url} alt="Avatar" className="w-full h-full object-cover" />
|
|
156
|
-
) : (
|
|
157
|
-
<User className="w-12 h-12 text-muted-foreground" />
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
<label className="absolute inset-0 flex items-center justify-center bg-black/40 rounded-full opacity-0 group-hover:opacity-100 cursor-pointer transition-opacity">
|
|
161
|
-
<Camera className="w-6 h-6 text-white" />
|
|
162
|
-
<input type="file" className="hidden" accept="image/*" onChange={handleAvatarUpload} disabled={isUploading} />
|
|
163
|
-
</label>
|
|
164
|
-
{isUploading && (
|
|
165
|
-
<div className="absolute inset-0 flex items-center justify-center bg-background/60 rounded-full">
|
|
166
|
-
<LoadingSpinner size="sm" />
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
<div className="flex-1 space-y-4 w-full">
|
|
172
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
173
|
-
<div className="space-y-2">
|
|
174
|
-
<Label htmlFor="first-name">First Name</Label>
|
|
175
|
-
<Input
|
|
176
|
-
id="first-name"
|
|
177
|
-
value={firstName}
|
|
178
|
-
onChange={(e) => setFirstName(e.target.value)}
|
|
179
|
-
placeholder="John"
|
|
180
|
-
/>
|
|
181
|
-
</div>
|
|
182
|
-
<div className="space-y-2">
|
|
183
|
-
<Label htmlFor="last-name">Last Name</Label>
|
|
184
|
-
<Input
|
|
185
|
-
id="last-name"
|
|
186
|
-
value={lastName}
|
|
187
|
-
onChange={(e) => setLastName(e.target.value)}
|
|
188
|
-
placeholder="Doe"
|
|
189
|
-
/>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
<div className="space-y-2">
|
|
193
|
-
<Label htmlFor="email">Email Address</Label>
|
|
194
|
-
<Input
|
|
195
|
-
id="email"
|
|
196
|
-
value={state.profile?.email || ''}
|
|
197
|
-
disabled
|
|
198
|
-
className="bg-secondary/50"
|
|
199
|
-
/>
|
|
200
|
-
<p className="text-[10px] text-muted-foreground">Email cannot be changed directly.</p>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<div className="pt-6 border-t flex items-center justify-between">
|
|
206
|
-
<div className="space-y-0.5">
|
|
207
|
-
<label className="text-sm font-medium">Sound & Haptics</label>
|
|
208
|
-
<p className="text-xs text-muted-foreground">Audio feedback when processing emails and completing tasks.</p>
|
|
209
|
-
</div>
|
|
210
|
-
<Button
|
|
211
|
-
variant={soundsEnabled ? 'default' : 'outline'}
|
|
212
|
-
size="sm"
|
|
213
|
-
onClick={toggleSounds}
|
|
214
|
-
className="gap-2"
|
|
215
|
-
>
|
|
216
|
-
{soundsEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
|
|
217
|
-
{soundsEnabled ? 'Enabled' : 'Disabled'}
|
|
218
|
-
</Button>
|
|
219
|
-
</div>
|
|
220
|
-
|
|
221
|
-
<div className="flex justify-end pt-4 border-t">
|
|
222
|
-
<Button onClick={handleSave} disabled={isSaving}>
|
|
223
|
-
{isSaving ? <LoadingSpinner size="sm" className="mr-2" /> : <Save className="w-4 h-4 mr-2" />}
|
|
224
|
-
Save Changes
|
|
225
|
-
</Button>
|
|
226
|
-
</div>
|
|
227
|
-
</CardContent>
|
|
228
|
-
</Card>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function SecuritySection() {
|
|
233
|
-
const [password, setPassword] = useState('');
|
|
234
|
-
const [confirmPassword, setConfirmPassword] = useState('');
|
|
235
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
236
|
-
|
|
237
|
-
const handlePasswordChange = async () => {
|
|
238
|
-
if (!password) {
|
|
239
|
-
toast.error('Please enter a new password');
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (password !== confirmPassword) {
|
|
243
|
-
toast.error('Passwords do not match');
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (password.length < 6) {
|
|
247
|
-
toast.error('Password must be at least 6 characters');
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
setIsSaving(true);
|
|
252
|
-
try {
|
|
253
|
-
const { api } = await import('../lib/api');
|
|
254
|
-
const result = await api.changePassword(password);
|
|
255
|
-
if (result.success) {
|
|
256
|
-
toast.success('Password changed successfully');
|
|
257
|
-
setPassword('');
|
|
258
|
-
setConfirmPassword('');
|
|
259
|
-
} else {
|
|
260
|
-
toast.error(result.error?.message || 'Failed to change password');
|
|
261
|
-
}
|
|
262
|
-
} catch (error) {
|
|
263
|
-
toast.error('An error occurred');
|
|
264
|
-
} finally {
|
|
265
|
-
setIsSaving(false);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
return (
|
|
270
|
-
<Card>
|
|
271
|
-
<CardHeader>
|
|
272
|
-
<CardTitle>Security</CardTitle>
|
|
273
|
-
<CardDescription>Manage your password and account security.</CardDescription>
|
|
274
|
-
</CardHeader>
|
|
275
|
-
<CardContent className="space-y-4">
|
|
276
|
-
<div className="space-y-2">
|
|
277
|
-
<Label htmlFor="new-password">New Password</Label>
|
|
278
|
-
<Input
|
|
279
|
-
id="new-password"
|
|
280
|
-
type="password"
|
|
281
|
-
value={password}
|
|
282
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
283
|
-
placeholder="••••••••"
|
|
284
|
-
/>
|
|
285
|
-
</div>
|
|
286
|
-
<div className="space-y-2">
|
|
287
|
-
<Label htmlFor="confirm-password">Confirm New Password</Label>
|
|
288
|
-
<Input
|
|
289
|
-
id="confirm-password"
|
|
290
|
-
type="password"
|
|
291
|
-
value={confirmPassword}
|
|
292
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
293
|
-
placeholder="••••••••"
|
|
294
|
-
/>
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
<div className="flex justify-end pt-4 border-t">
|
|
298
|
-
<Button onClick={handlePasswordChange} disabled={isSaving}>
|
|
299
|
-
{isSaving ? <LoadingSpinner size="sm" className="mr-2" /> : <Key className="w-4 h-4 mr-2" />}
|
|
300
|
-
Update Password
|
|
301
|
-
</Button>
|
|
302
|
-
</div>
|
|
303
|
-
</CardContent>
|
|
304
|
-
</Card>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function DatabaseSection() {
|
|
309
|
-
const [showWizard, setShowWizard] = useState(false);
|
|
310
|
-
const config = getSupabaseConfig();
|
|
311
|
-
const source = getConfigSource();
|
|
312
|
-
|
|
313
|
-
const handleClearConfig = () => {
|
|
314
|
-
if (confirm('Are you sure you want to disconnect from this Supabase project? This will log you out and clear local configuration.')) {
|
|
315
|
-
clearSupabaseConfig();
|
|
316
|
-
window.location.reload();
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
return (
|
|
321
|
-
<>
|
|
322
|
-
<Card>
|
|
323
|
-
<CardHeader>
|
|
324
|
-
<CardTitle className="flex items-center gap-2">
|
|
325
|
-
<Database className="w-5 h-5 text-primary" />
|
|
326
|
-
Supabase Connection
|
|
327
|
-
</CardTitle>
|
|
328
|
-
<CardDescription>Manage your database configuration (BYOK - Bring Your Own Keys).</CardDescription>
|
|
329
|
-
</CardHeader>
|
|
330
|
-
<CardContent className="space-y-6">
|
|
331
|
-
{config ? (
|
|
332
|
-
<>
|
|
333
|
-
{/* Status Card */}
|
|
334
|
-
<div className="flex items-start gap-4 p-4 border rounded-xl bg-emerald-500/5 border-emerald-500/20">
|
|
335
|
-
<CheckCircle className="w-6 h-6 text-emerald-500 mt-0.5" />
|
|
336
|
-
<div className="flex-1 space-y-1">
|
|
337
|
-
<p className="font-semibold text-emerald-700 dark:text-emerald-400">Connected</p>
|
|
338
|
-
<p className="text-sm font-mono break-all opacity-80">{config.url}</p>
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
{source === 'env' && (
|
|
343
|
-
<Alert className="bg-amber-500/5 border-amber-500/20">
|
|
344
|
-
<Settings className="h-4 w-4 text-amber-500" />
|
|
345
|
-
<AlertDescription className="text-amber-700 dark:text-amber-400 text-xs">
|
|
346
|
-
Configuration is loaded from environment variables. Use the UI to override them.
|
|
347
|
-
</AlertDescription>
|
|
348
|
-
</Alert>
|
|
349
|
-
)}
|
|
350
|
-
|
|
351
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
352
|
-
<Button variant="outline" onClick={() => setShowWizard(true)} className="w-full">
|
|
353
|
-
<Settings className="w-4 h-4 mr-2" />
|
|
354
|
-
Change Connection
|
|
355
|
-
</Button>
|
|
356
|
-
{source === 'ui' && (
|
|
357
|
-
<Button variant="outline" onClick={handleClearConfig} className="w-full text-destructive hover:bg-destructive/10">
|
|
358
|
-
<Trash2 className="w-4 h-4 mr-2" />
|
|
359
|
-
Clear Config
|
|
360
|
-
</Button>
|
|
361
|
-
)}
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<div className="space-y-2 pt-4 border-t">
|
|
365
|
-
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Anon Public Key</Label>
|
|
366
|
-
<div className="p-2 bg-secondary/50 rounded-lg font-mono text-xs break-all">
|
|
367
|
-
{config.anonKey.substring(0, 20)}...{config.anonKey.substring(config.anonKey.length - 10)}
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</>
|
|
371
|
-
) : (
|
|
372
|
-
<div className="flex flex-col items-center justify-center py-8 text-center space-y-4">
|
|
373
|
-
<XCircle className="w-12 h-12 text-destructive opacity-20" />
|
|
374
|
-
<div>
|
|
375
|
-
<p className="font-medium">No Connection Detected</p>
|
|
376
|
-
<p className="text-sm text-muted-foreground">Configure a Supabase project to get started.</p>
|
|
377
|
-
</div>
|
|
378
|
-
<Button onClick={() => setShowWizard(true)}>
|
|
379
|
-
<Database className="w-4 h-4 mr-2" />
|
|
380
|
-
Setup Supabase
|
|
381
|
-
</Button>
|
|
382
|
-
</div>
|
|
383
|
-
)}
|
|
384
|
-
</CardContent>
|
|
385
|
-
</Card>
|
|
386
|
-
|
|
387
|
-
<SetupWizard open={showWizard} onComplete={() => setShowWizard(false)} canClose={true} />
|
|
388
|
-
</>
|
|
389
|
-
);
|
|
390
|
-
}
|