@realtimex/email-automator 2.2.0 → 2.2.1
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 +0 -6
- package/api/src/config/index.ts +3 -0
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +23 -7
- package/package.json +1 -2
- 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,388 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Dialog,
|
|
4
|
-
DialogContent,
|
|
5
|
-
DialogDescription,
|
|
6
|
-
DialogHeader,
|
|
7
|
-
DialogTitle,
|
|
8
|
-
DialogFooter
|
|
9
|
-
} from './ui/dialog';
|
|
10
|
-
import { Button } from './ui/button';
|
|
11
|
-
import { Input } from './ui/input';
|
|
12
|
-
import { Label } from './ui/label';
|
|
13
|
-
import { Alert, AlertDescription } from './ui/alert';
|
|
14
|
-
import {
|
|
15
|
-
Loader2,
|
|
16
|
-
Database,
|
|
17
|
-
CheckCircle,
|
|
18
|
-
AlertCircle,
|
|
19
|
-
ExternalLink,
|
|
20
|
-
Check,
|
|
21
|
-
} from 'lucide-react';
|
|
22
|
-
import {
|
|
23
|
-
saveSupabaseConfig,
|
|
24
|
-
validateSupabaseConnection,
|
|
25
|
-
} from '../lib/supabase-config';
|
|
26
|
-
import { Logo } from './Logo';
|
|
27
|
-
|
|
28
|
-
type WizardStep = 'welcome' | 'credentials' | 'validating' | 'success';
|
|
29
|
-
|
|
30
|
-
interface SetupWizardProps {
|
|
31
|
-
onComplete: () => void;
|
|
32
|
-
open?: boolean;
|
|
33
|
-
canClose?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Normalizes Supabase URL input - accepts either full URL or just project ID
|
|
38
|
-
*/
|
|
39
|
-
function normalizeSupabaseUrl(input: string): string {
|
|
40
|
-
const trimmed = input.trim();
|
|
41
|
-
if (!trimmed) return '';
|
|
42
|
-
|
|
43
|
-
// If it starts with http:// or https://, treat as full URL
|
|
44
|
-
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
45
|
-
return trimmed;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Otherwise, treat as project ID and construct full URL
|
|
49
|
-
return `https://${trimmed}.supabase.co`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Validates if input looks like a valid Supabase URL or project ID
|
|
54
|
-
*/
|
|
55
|
-
function validateUrlFormat(input: string): {
|
|
56
|
-
valid: boolean;
|
|
57
|
-
message?: string;
|
|
58
|
-
} {
|
|
59
|
-
const trimmed = input.trim();
|
|
60
|
-
if (!trimmed) return { valid: false };
|
|
61
|
-
|
|
62
|
-
// Check if it's a full URL
|
|
63
|
-
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
64
|
-
try {
|
|
65
|
-
const url = new URL(trimmed);
|
|
66
|
-
if (url.hostname.endsWith('.supabase.co')) {
|
|
67
|
-
return {
|
|
68
|
-
valid: true,
|
|
69
|
-
message: "Valid Supabase URL",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
valid: false,
|
|
74
|
-
message: "URL must be a supabase.co domain",
|
|
75
|
-
};
|
|
76
|
-
} catch {
|
|
77
|
-
return {
|
|
78
|
-
valid: false,
|
|
79
|
-
message: "Invalid URL format",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check if it's a project ID (alphanumeric, typically 20 chars)
|
|
85
|
-
if (/^[a-z0-9]+$/.test(trimmed)) {
|
|
86
|
-
return {
|
|
87
|
-
valid: true,
|
|
88
|
-
message: "Project ID detected",
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { valid: false, message: "Enter a URL or Project ID" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Validates if input looks like a valid Supabase API key
|
|
97
|
-
*/
|
|
98
|
-
function validateKeyFormat(input: string): {
|
|
99
|
-
valid: boolean;
|
|
100
|
-
message?: string;
|
|
101
|
-
} {
|
|
102
|
-
const trimmed = input.trim();
|
|
103
|
-
if (!trimmed) return { valid: false };
|
|
104
|
-
|
|
105
|
-
// New publishable keys start with "sb_publishable_" followed by key content
|
|
106
|
-
if (trimmed.startsWith("sb_publishable_")) {
|
|
107
|
-
// Check that there's actual key content after the prefix
|
|
108
|
-
if (trimmed.length > "sb_publishable_".length + 10) {
|
|
109
|
-
return {
|
|
110
|
-
valid: true,
|
|
111
|
-
message: "Valid Publishable Key",
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
valid: false,
|
|
116
|
-
message: "Incomplete Publishable Key",
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Legacy anon keys are JWT tokens starting with "eyJ"
|
|
121
|
-
if (trimmed.startsWith("eyJ")) {
|
|
122
|
-
if (trimmed.length > 50) {
|
|
123
|
-
return {
|
|
124
|
-
valid: true,
|
|
125
|
-
message: "Valid Anon Key",
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
valid: false,
|
|
130
|
-
message: "Incomplete Anon Key",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
valid: false,
|
|
136
|
-
message: "Invalid API Key format",
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function SetupWizard({ onComplete, open = true, canClose = false }: SetupWizardProps) {
|
|
141
|
-
const [step, setStep] = useState<WizardStep>('welcome');
|
|
142
|
-
const [url, setUrl] = useState('');
|
|
143
|
-
const [anonKey, setAnonKey] = useState('');
|
|
144
|
-
const [error, setError] = useState<string | null>(null);
|
|
145
|
-
const [urlTouched, setUrlTouched] = useState(false);
|
|
146
|
-
const [keyTouched, setKeyTouched] = useState(false);
|
|
147
|
-
|
|
148
|
-
const handleValidateAndSave = async () => {
|
|
149
|
-
setError(null);
|
|
150
|
-
setStep('validating');
|
|
151
|
-
|
|
152
|
-
// Normalize the URL before validation
|
|
153
|
-
const normalizedUrl = normalizeSupabaseUrl(url);
|
|
154
|
-
const trimmedKey = anonKey.trim();
|
|
155
|
-
|
|
156
|
-
const result = await validateSupabaseConnection(normalizedUrl, trimmedKey);
|
|
157
|
-
|
|
158
|
-
if (result.valid) {
|
|
159
|
-
saveSupabaseConfig({ url: normalizedUrl, anonKey: trimmedKey });
|
|
160
|
-
setStep('success');
|
|
161
|
-
|
|
162
|
-
// Completes the flow by reloading to pick up new config
|
|
163
|
-
setTimeout(() => {
|
|
164
|
-
window.location.reload();
|
|
165
|
-
}, 1000);
|
|
166
|
-
} else {
|
|
167
|
-
setError(result.error || 'Connection failed. Please check your credentials.');
|
|
168
|
-
setStep('credentials');
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Get validation states
|
|
173
|
-
const urlValidation = url ? validateUrlFormat(url) : { valid: false };
|
|
174
|
-
const keyValidation = anonKey ? validateKeyFormat(anonKey) : { valid: false };
|
|
175
|
-
const normalizedUrl = url ? normalizeSupabaseUrl(url) : '';
|
|
176
|
-
const showUrlExpansion = url && !url.startsWith('http') && urlValidation.valid;
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<Dialog open={open} onOpenChange={(val) => {
|
|
180
|
-
if (!val && canClose) onComplete();
|
|
181
|
-
}}>
|
|
182
|
-
<DialogContent
|
|
183
|
-
className="sm:max-w-md"
|
|
184
|
-
onPointerDownOutside={(e) => { if (!canClose) e.preventDefault() }}
|
|
185
|
-
onEscapeKeyDown={(e) => { if (!canClose) e.preventDefault() }}
|
|
186
|
-
>
|
|
187
|
-
{step === 'welcome' && (
|
|
188
|
-
<>
|
|
189
|
-
<DialogHeader>
|
|
190
|
-
<div className="flex items-center gap-2 mb-2">
|
|
191
|
-
<Logo className="w-9 h-9" />
|
|
192
|
-
<DialogTitle>Welcome to Email Automator</DialogTitle>
|
|
193
|
-
</div>
|
|
194
|
-
<DialogDescription>
|
|
195
|
-
This application requires a Supabase database to store your emails and automation rules.
|
|
196
|
-
</DialogDescription>
|
|
197
|
-
</DialogHeader>
|
|
198
|
-
|
|
199
|
-
<div className="space-y-4 py-4">
|
|
200
|
-
<Alert>
|
|
201
|
-
<AlertDescription>
|
|
202
|
-
<strong>No Supabase Setup Detected</strong>
|
|
203
|
-
<br />
|
|
204
|
-
You can create a free project at{' '}
|
|
205
|
-
<a
|
|
206
|
-
href="https://supabase.com"
|
|
207
|
-
target="_blank"
|
|
208
|
-
rel="noopener noreferrer"
|
|
209
|
-
className="underline text-primary inline-flex items-center gap-1"
|
|
210
|
-
>
|
|
211
|
-
supabase.com
|
|
212
|
-
<ExternalLink className="h-3 w-3" />
|
|
213
|
-
</a>
|
|
214
|
-
</AlertDescription>
|
|
215
|
-
</Alert>
|
|
216
|
-
|
|
217
|
-
<div className="space-y-2">
|
|
218
|
-
<h4 className="font-medium text-sm">You will need:</h4>
|
|
219
|
-
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
|
220
|
-
<li>Project URL</li>
|
|
221
|
-
<li>Anon Public Key</li>
|
|
222
|
-
</ul>
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
<div className="flex gap-2">
|
|
226
|
-
{canClose && (
|
|
227
|
-
<Button variant="outline" onClick={onComplete} className="flex-1">
|
|
228
|
-
Close
|
|
229
|
-
</Button>
|
|
230
|
-
)}
|
|
231
|
-
<Button onClick={() => setStep('credentials')} className="flex-1">
|
|
232
|
-
Continue Setup
|
|
233
|
-
</Button>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</>
|
|
237
|
-
)}
|
|
238
|
-
|
|
239
|
-
{step === 'credentials' && (
|
|
240
|
-
<>
|
|
241
|
-
<DialogHeader>
|
|
242
|
-
<DialogTitle>Connect to Supabase</DialogTitle>
|
|
243
|
-
<DialogDescription>
|
|
244
|
-
Enter your connection details below.
|
|
245
|
-
</DialogDescription>
|
|
246
|
-
</DialogHeader>
|
|
247
|
-
|
|
248
|
-
<div className="space-y-4 py-4">
|
|
249
|
-
{error && (
|
|
250
|
-
<Alert variant="destructive">
|
|
251
|
-
<AlertCircle className="h-4 w-4" />
|
|
252
|
-
<AlertDescription>{error}</AlertDescription>
|
|
253
|
-
</Alert>
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
<div className="space-y-2">
|
|
257
|
-
<Label htmlFor="supabase-url">Project URL or ID</Label>
|
|
258
|
-
<div className="relative">
|
|
259
|
-
<Input
|
|
260
|
-
id="supabase-url"
|
|
261
|
-
placeholder="https://xxx.supabase.co or project-id"
|
|
262
|
-
value={url}
|
|
263
|
-
onChange={(e) => {
|
|
264
|
-
setUrl(e.target.value);
|
|
265
|
-
setUrlTouched(true);
|
|
266
|
-
}}
|
|
267
|
-
onBlur={() => setUrlTouched(true)}
|
|
268
|
-
className={
|
|
269
|
-
urlTouched && url
|
|
270
|
-
? urlValidation.valid
|
|
271
|
-
? 'pr-8 border-green-500'
|
|
272
|
-
: 'pr-8 border-destructive'
|
|
273
|
-
: ''
|
|
274
|
-
}
|
|
275
|
-
/>
|
|
276
|
-
{urlTouched && url && urlValidation.valid && (
|
|
277
|
-
<Check className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
|
|
278
|
-
)}
|
|
279
|
-
</div>
|
|
280
|
-
{showUrlExpansion && (
|
|
281
|
-
<div className="flex items-start gap-1.5 text-xs text-green-600 dark:text-green-400">
|
|
282
|
-
<Check className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
283
|
-
<span>Will use: {normalizedUrl}</span>
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
{urlTouched && url && urlValidation.message && !urlValidation.valid && (
|
|
287
|
-
<p className="text-xs text-destructive">{urlValidation.message}</p>
|
|
288
|
-
)}
|
|
289
|
-
</div>
|
|
290
|
-
|
|
291
|
-
<div className="space-y-2">
|
|
292
|
-
<Label htmlFor="anon-key">Anon Public Key</Label>
|
|
293
|
-
<div className="relative">
|
|
294
|
-
<Input
|
|
295
|
-
id="anon-key"
|
|
296
|
-
type="password"
|
|
297
|
-
placeholder="eyJ..."
|
|
298
|
-
value={anonKey}
|
|
299
|
-
onChange={(e) => {
|
|
300
|
-
setAnonKey(e.target.value);
|
|
301
|
-
setKeyTouched(true);
|
|
302
|
-
}}
|
|
303
|
-
onBlur={() => setKeyTouched(true)}
|
|
304
|
-
className={
|
|
305
|
-
keyTouched && anonKey
|
|
306
|
-
? keyValidation.valid
|
|
307
|
-
? 'pr-8 border-green-500'
|
|
308
|
-
: 'pr-8 border-destructive'
|
|
309
|
-
: ''
|
|
310
|
-
}
|
|
311
|
-
/>
|
|
312
|
-
{keyTouched && anonKey && keyValidation.valid && (
|
|
313
|
-
<Check className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
|
|
314
|
-
)}
|
|
315
|
-
</div>
|
|
316
|
-
{keyTouched && anonKey && keyValidation.message && (
|
|
317
|
-
<p
|
|
318
|
-
className={`text-xs ${keyValidation.valid ? 'text-green-600 dark:text-green-400' : 'text-destructive'}`}
|
|
319
|
-
>
|
|
320
|
-
{keyValidation.message}
|
|
321
|
-
</p>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
<div className="flex gap-2">
|
|
326
|
-
<Button
|
|
327
|
-
variant="outline"
|
|
328
|
-
onClick={() => setStep('welcome')}
|
|
329
|
-
className="flex-1"
|
|
330
|
-
>
|
|
331
|
-
Back
|
|
332
|
-
</Button>
|
|
333
|
-
{canClose && (
|
|
334
|
-
<Button
|
|
335
|
-
variant="outline"
|
|
336
|
-
onClick={onComplete}
|
|
337
|
-
className="flex-1"
|
|
338
|
-
>
|
|
339
|
-
Close
|
|
340
|
-
</Button>
|
|
341
|
-
)}
|
|
342
|
-
<Button
|
|
343
|
-
onClick={handleValidateAndSave}
|
|
344
|
-
disabled={!urlValidation.valid || !keyValidation.valid}
|
|
345
|
-
className="flex-1"
|
|
346
|
-
>
|
|
347
|
-
Connect
|
|
348
|
-
</Button>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
</>
|
|
352
|
-
)}
|
|
353
|
-
|
|
354
|
-
{step === 'validating' && (
|
|
355
|
-
<>
|
|
356
|
-
<DialogHeader>
|
|
357
|
-
<DialogTitle>Validating Connection</DialogTitle>
|
|
358
|
-
<DialogDescription>
|
|
359
|
-
Verifying your credentials with Supabase...
|
|
360
|
-
</DialogDescription>
|
|
361
|
-
</DialogHeader>
|
|
362
|
-
|
|
363
|
-
<div className="flex flex-col items-center justify-center py-8">
|
|
364
|
-
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
|
365
|
-
<p className="text-sm text-muted-foreground">Please wait...</p>
|
|
366
|
-
</div>
|
|
367
|
-
</>
|
|
368
|
-
)}
|
|
369
|
-
|
|
370
|
-
{step === 'success' && (
|
|
371
|
-
<>
|
|
372
|
-
<DialogHeader>
|
|
373
|
-
<DialogTitle>Success!</DialogTitle>
|
|
374
|
-
<DialogDescription>
|
|
375
|
-
Your database is connected.
|
|
376
|
-
</DialogDescription>
|
|
377
|
-
</DialogHeader>
|
|
378
|
-
|
|
379
|
-
<div className="flex flex-col items-center justify-center py-8">
|
|
380
|
-
<CheckCircle className="h-12 w-12 text-green-500 mb-4" />
|
|
381
|
-
<p className="text-sm text-muted-foreground">Restarting application...</p>
|
|
382
|
-
</div>
|
|
383
|
-
</>
|
|
384
|
-
)}
|
|
385
|
-
</DialogContent>
|
|
386
|
-
</Dialog>
|
|
387
|
-
);
|
|
388
|
-
}
|
package/src/components/Toast.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
|
|
3
|
-
import { cn } from '../lib/utils';
|
|
4
|
-
|
|
5
|
-
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
6
|
-
|
|
7
|
-
interface Toast {
|
|
8
|
-
id: string;
|
|
9
|
-
type: ToastType;
|
|
10
|
-
message: string;
|
|
11
|
-
duration?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ToastContextType {
|
|
15
|
-
toasts: Toast[];
|
|
16
|
-
addToast: (type: ToastType, message: string, duration?: number) => void;
|
|
17
|
-
removeToast: (id: string) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Simple toast store
|
|
21
|
-
let toasts: Toast[] = [];
|
|
22
|
-
let listeners: Set<() => void> = new Set();
|
|
23
|
-
|
|
24
|
-
function notify() {
|
|
25
|
-
listeners.forEach(listener => listener());
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const toast = {
|
|
29
|
-
success: (message: string, duration?: number) => addToast('success', message, duration),
|
|
30
|
-
error: (message: string, duration?: number) => addToast('error', message, duration),
|
|
31
|
-
info: (message: string, duration?: number) => addToast('info', message, duration),
|
|
32
|
-
warning: (message: string, duration?: number) => addToast('warning', message, duration),
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function addToast(type: ToastType, message: string, duration = 5000) {
|
|
36
|
-
const id = Math.random().toString(36).substr(2, 9);
|
|
37
|
-
toasts = [...toasts, { id, type, message, duration }];
|
|
38
|
-
notify();
|
|
39
|
-
|
|
40
|
-
if (duration > 0) {
|
|
41
|
-
setTimeout(() => removeToast(id), duration);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function removeToast(id: string) {
|
|
46
|
-
toasts = toasts.filter(t => t.id !== id);
|
|
47
|
-
notify();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function useToasts() {
|
|
51
|
-
const [, setTick] = useState(0);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
const listener = () => setTick(t => t + 1);
|
|
55
|
-
listeners.add(listener);
|
|
56
|
-
return () => { listeners.delete(listener); };
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
return toasts;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const icons = {
|
|
63
|
-
success: CheckCircle,
|
|
64
|
-
error: AlertCircle,
|
|
65
|
-
info: Info,
|
|
66
|
-
warning: AlertTriangle,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const styles = {
|
|
70
|
-
success: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400',
|
|
71
|
-
error: 'bg-destructive/10 border-destructive/20 text-destructive',
|
|
72
|
-
info: 'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400',
|
|
73
|
-
warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-600 dark:text-yellow-400',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
function ToastItem({ toast: t }: { toast: Toast }) {
|
|
77
|
-
const Icon = icons[t.type];
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div className={cn(
|
|
81
|
-
'flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm',
|
|
82
|
-
'animate-in slide-in-from-right-full duration-300',
|
|
83
|
-
styles[t.type]
|
|
84
|
-
)}>
|
|
85
|
-
<Icon className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
|
86
|
-
<p className="text-sm flex-1">{t.message}</p>
|
|
87
|
-
<button
|
|
88
|
-
onClick={() => removeToast(t.id)}
|
|
89
|
-
className="p-1 hover:bg-black/10 rounded transition-colors"
|
|
90
|
-
>
|
|
91
|
-
<X className="w-4 h-4" />
|
|
92
|
-
</button>
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function ToastContainer() {
|
|
98
|
-
const toastList = useToasts();
|
|
99
|
-
|
|
100
|
-
if (toastList.length === 0) return null;
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full">
|
|
104
|
-
{toastList.map(t => (
|
|
105
|
-
<ToastItem key={t.id} toast={t} />
|
|
106
|
-
))}
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MigrationBanner Component
|
|
3
|
-
*
|
|
4
|
-
* Displays a compact floating notification in top-right corner when migration is required.
|
|
5
|
-
* Non-blocking: allows users to continue using the app while showing the reminder.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useState } from "react";
|
|
9
|
-
import { AlertTriangle, X } from "lucide-react";
|
|
10
|
-
import { Button } from "../ui/button";
|
|
11
|
-
import { toast } from "../Toast";
|
|
12
|
-
import {
|
|
13
|
-
dismissMigrationReminder,
|
|
14
|
-
type MigrationStatus,
|
|
15
|
-
} from "../../lib/migration-check";
|
|
16
|
-
|
|
17
|
-
interface MigrationBannerProps {
|
|
18
|
-
/** Migration status from checkMigrationStatus */
|
|
19
|
-
status: MigrationStatus;
|
|
20
|
-
/** Callback when banner is dismissed */
|
|
21
|
-
onDismiss?: () => void;
|
|
22
|
-
/** Callback when user clicks to open modal */
|
|
23
|
-
onLearnMore?: () => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function MigrationBanner({
|
|
27
|
-
status,
|
|
28
|
-
onDismiss,
|
|
29
|
-
onLearnMore,
|
|
30
|
-
}: MigrationBannerProps) {
|
|
31
|
-
const [isVisible, setIsVisible] = useState(true);
|
|
32
|
-
|
|
33
|
-
if (!isVisible) return null;
|
|
34
|
-
|
|
35
|
-
const handleDismiss = () => {
|
|
36
|
-
dismissMigrationReminder();
|
|
37
|
-
setIsVisible(false);
|
|
38
|
-
onDismiss?.();
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const handleClick = () => {
|
|
42
|
-
if (onLearnMore) {
|
|
43
|
-
onLearnMore();
|
|
44
|
-
} else {
|
|
45
|
-
// Fallback: copy command
|
|
46
|
-
navigator.clipboard.writeText("npx email-automator migrate");
|
|
47
|
-
toast.success("Command copied to clipboard");
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div className="fixed right-4 top-16 z-50 max-w-sm animate-in slide-in-from-top-5">
|
|
53
|
-
<div className="rounded-lg border border-yellow-500 bg-yellow-50 p-4 shadow-lg dark:bg-yellow-950/90 dark:border-yellow-600">
|
|
54
|
-
<div className="flex items-start gap-3">
|
|
55
|
-
<AlertTriangle className="h-5 w-5 flex-shrink-0 text-yellow-600 dark:text-yellow-500 mt-0.5" />
|
|
56
|
-
<div className="flex-1 space-y-2">
|
|
57
|
-
<div className="flex items-start justify-between gap-2">
|
|
58
|
-
<p className="text-sm font-medium text-yellow-900 dark:text-yellow-100">
|
|
59
|
-
Database Update Required
|
|
60
|
-
</p>
|
|
61
|
-
<Button
|
|
62
|
-
size="icon"
|
|
63
|
-
variant="ghost"
|
|
64
|
-
className="h-5 w-5 -mr-1 -mt-1 text-yellow-900 hover:bg-yellow-100 dark:text-yellow-100 dark:hover:bg-yellow-900/30"
|
|
65
|
-
onClick={handleDismiss}
|
|
66
|
-
>
|
|
67
|
-
<X className="h-3 w-3" />
|
|
68
|
-
<span className="sr-only">Dismiss</span>
|
|
69
|
-
</Button>
|
|
70
|
-
</div>
|
|
71
|
-
<p className="text-xs text-yellow-800 dark:text-yellow-200">
|
|
72
|
-
Your app version ({status.appVersion}) requires a database schema update.
|
|
73
|
-
</p>
|
|
74
|
-
<div className="flex gap-2">
|
|
75
|
-
<Button
|
|
76
|
-
size="sm"
|
|
77
|
-
variant="default"
|
|
78
|
-
className="h-7 text-xs bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-500 dark:hover:bg-yellow-600"
|
|
79
|
-
onClick={handleClick}
|
|
80
|
-
>
|
|
81
|
-
Update Now
|
|
82
|
-
</Button>
|
|
83
|
-
<Button
|
|
84
|
-
size="sm"
|
|
85
|
-
variant="ghost"
|
|
86
|
-
className="h-7 text-xs text-yellow-900 hover:bg-yellow-100 dark:text-yellow-100 dark:hover:bg-yellow-900/30"
|
|
87
|
-
onClick={handleDismiss}
|
|
88
|
-
>
|
|
89
|
-
Remind Later
|
|
90
|
-
</Button>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|