@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,71 +0,0 @@
|
|
|
1
|
-
import { Component, ReactNode } from 'react';
|
|
2
|
-
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
3
|
-
import { Button } from './ui/button';
|
|
4
|
-
import { Card } from './ui/card';
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
children: ReactNode;
|
|
8
|
-
fallback?: ReactNode;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface State {
|
|
12
|
-
hasError: boolean;
|
|
13
|
-
error: Error | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class ErrorBoundary extends Component<Props, State> {
|
|
17
|
-
constructor(props: Props) {
|
|
18
|
-
super(props);
|
|
19
|
-
this.state = { hasError: false, error: null };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static getDerivedStateFromError(error: Error): State {
|
|
23
|
-
return { hasError: true, error };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
27
|
-
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
handleReset = () => {
|
|
31
|
-
this.setState({ hasError: false, error: null });
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
render() {
|
|
35
|
-
if (this.state.hasError) {
|
|
36
|
-
if (this.props.fallback) {
|
|
37
|
-
return this.props.fallback;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div className="min-h-screen flex items-center justify-center p-8 bg-background">
|
|
42
|
-
<Card className="max-w-md w-full p-8 text-center">
|
|
43
|
-
<div className="w-16 h-16 bg-destructive/10 text-destructive rounded-full flex items-center justify-center mx-auto mb-6">
|
|
44
|
-
<AlertTriangle className="w-8 h-8" />
|
|
45
|
-
</div>
|
|
46
|
-
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
|
|
47
|
-
<p className="text-muted-foreground mb-6">
|
|
48
|
-
An unexpected error occurred. Please try refreshing the page.
|
|
49
|
-
</p>
|
|
50
|
-
{this.state.error && (
|
|
51
|
-
<pre className="text-xs text-left bg-secondary p-3 rounded-lg mb-6 overflow-auto max-h-32">
|
|
52
|
-
{this.state.error.message}
|
|
53
|
-
</pre>
|
|
54
|
-
)}
|
|
55
|
-
<div className="flex gap-3 justify-center">
|
|
56
|
-
<Button onClick={this.handleReset} variant="outline">
|
|
57
|
-
<RefreshCw className="w-4 h-4 mr-2" />
|
|
58
|
-
Try Again
|
|
59
|
-
</Button>
|
|
60
|
-
<Button onClick={() => window.location.reload()}>
|
|
61
|
-
Refresh Page
|
|
62
|
-
</Button>
|
|
63
|
-
</div>
|
|
64
|
-
</Card>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return this.props.children;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { supabase } from '../lib/supabase';
|
|
3
|
-
import { ProcessingEvent } from '../lib/types';
|
|
4
|
-
import {
|
|
5
|
-
Terminal,
|
|
6
|
-
Brain,
|
|
7
|
-
Zap,
|
|
8
|
-
Info,
|
|
9
|
-
AlertTriangle,
|
|
10
|
-
Activity,
|
|
11
|
-
Minimize2,
|
|
12
|
-
ChevronDown,
|
|
13
|
-
ChevronUp,
|
|
14
|
-
Code
|
|
15
|
-
} from 'lucide-react';
|
|
16
|
-
import { Card, CardHeader, CardTitle, CardContent } from './ui/card';
|
|
17
|
-
import { Button } from './ui/button';
|
|
18
|
-
import { cn } from '../lib/utils';
|
|
19
|
-
import { useTerminal } from '../context/TerminalContext';
|
|
20
|
-
|
|
21
|
-
export function LiveTerminal() {
|
|
22
|
-
const [events, setEvents] = useState<ProcessingEvent[]>([]);
|
|
23
|
-
const { isExpanded, setIsExpanded } = useTerminal();
|
|
24
|
-
const [expandedEvents, setExpandedEvents] = useState<Record<string, boolean>>({});
|
|
25
|
-
|
|
26
|
-
// Initial fetch of recent events
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
fetchRecentEvents();
|
|
29
|
-
|
|
30
|
-
// Subscribe to realtime changes
|
|
31
|
-
const channel = supabase
|
|
32
|
-
.channel('processing_events_feed')
|
|
33
|
-
.on(
|
|
34
|
-
'postgres_changes',
|
|
35
|
-
{
|
|
36
|
-
event: 'INSERT',
|
|
37
|
-
schema: 'public',
|
|
38
|
-
table: 'processing_events',
|
|
39
|
-
},
|
|
40
|
-
(payload) => {
|
|
41
|
-
const newEvent = payload.new as ProcessingEvent;
|
|
42
|
-
|
|
43
|
-
// Auto-expand errors
|
|
44
|
-
if (newEvent.event_type === 'error') {
|
|
45
|
-
setExpandedEvents(prev => ({ ...prev, [newEvent.id]: true }));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
setEvents((prev) => {
|
|
49
|
-
// Insert at the beginning (descending order)
|
|
50
|
-
const updated = [newEvent, ...prev];
|
|
51
|
-
if (updated.length > 100) return updated.slice(0, 100);
|
|
52
|
-
return updated;
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
.subscribe();
|
|
57
|
-
|
|
58
|
-
return () => {
|
|
59
|
-
supabase.removeChannel(channel);
|
|
60
|
-
};
|
|
61
|
-
}, []);
|
|
62
|
-
|
|
63
|
-
const fetchRecentEvents = async () => {
|
|
64
|
-
const { data } = await supabase
|
|
65
|
-
.from('processing_events')
|
|
66
|
-
.select('*')
|
|
67
|
-
.order('created_at', { ascending: false })
|
|
68
|
-
.limit(50);
|
|
69
|
-
|
|
70
|
-
if (data) {
|
|
71
|
-
setEvents(data);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const toggleExpand = (id: string) => {
|
|
76
|
-
setExpandedEvents(prev => ({ ...prev, [id]: !prev[id] }));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const getIcon = (type: string) => {
|
|
80
|
-
switch (type) {
|
|
81
|
-
case 'analysis': return <Brain className="w-3 h-3 text-purple-500" />;
|
|
82
|
-
case 'action': return <Zap className="w-3 h-3 text-emerald-500" />;
|
|
83
|
-
case 'error': return <AlertTriangle className="w-3 h-3 text-red-500" />;
|
|
84
|
-
default: return <Info className="w-3 h-3 text-blue-500" />;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const formatTime = (isoString: string) => {
|
|
89
|
-
return new Date(isoString).toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
if (!isExpanded) {
|
|
93
|
-
return (
|
|
94
|
-
<div className="fixed bottom-4 right-4 z-50">
|
|
95
|
-
<Button
|
|
96
|
-
onClick={() => setIsExpanded(true)}
|
|
97
|
-
className="shadow-lg bg-primary text-primary-foreground hover:opacity-90 border border-border"
|
|
98
|
-
>
|
|
99
|
-
<Terminal className="w-4 h-4 mr-2" />
|
|
100
|
-
Live Activity
|
|
101
|
-
{events.length > 0 && (
|
|
102
|
-
<span className="ml-2 flex h-2 w-2 relative">
|
|
103
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
104
|
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
105
|
-
</span>
|
|
106
|
-
)}
|
|
107
|
-
</Button>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<Card className="fixed bottom-4 right-4 z-50 w-[550px] h-[650px] flex flex-col shadow-2xl border-border bg-background/95 text-foreground backdrop-blur-md animate-in slide-in-from-bottom-10">
|
|
114
|
-
<CardHeader className="py-3 px-4 border-b border-border flex flex-row items-center justify-between sticky top-0 bg-background/95 z-20">
|
|
115
|
-
<div className="flex items-center gap-2">
|
|
116
|
-
<Terminal className="w-4 h-4 text-primary" />
|
|
117
|
-
<CardTitle className="text-sm font-mono font-bold">Agent Terminal</CardTitle>
|
|
118
|
-
<div className="flex items-center gap-1 text-[10px] text-green-600 dark:text-green-400 bg-green-500/10 px-2 py-0.5 rounded-full border border-green-500/20 font-bold">
|
|
119
|
-
<Activity className="w-3 h-3" />
|
|
120
|
-
LIVE
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
<div className="flex items-center gap-2">
|
|
124
|
-
<Button
|
|
125
|
-
variant="ghost"
|
|
126
|
-
size="sm"
|
|
127
|
-
className="h-7 text-[10px] font-mono hover:bg-secondary"
|
|
128
|
-
onClick={() => setEvents([])}
|
|
129
|
-
>
|
|
130
|
-
Clear
|
|
131
|
-
</Button>
|
|
132
|
-
<Button
|
|
133
|
-
variant="ghost"
|
|
134
|
-
size="sm"
|
|
135
|
-
className="h-7 w-7 p-0 hover:bg-secondary"
|
|
136
|
-
onClick={() => setIsExpanded(false)}
|
|
137
|
-
>
|
|
138
|
-
<Minimize2 className="w-4 h-4" />
|
|
139
|
-
</Button>
|
|
140
|
-
</div>
|
|
141
|
-
</CardHeader>
|
|
142
|
-
<CardContent className="flex-1 overflow-y-auto p-4 font-mono text-xs space-y-6 custom-scrollbar">
|
|
143
|
-
{events.length === 0 && (
|
|
144
|
-
<div className="text-center text-muted-foreground py-20 italic">
|
|
145
|
-
Waiting for agent activity...
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
|
|
149
|
-
{events.map((event, i) => (
|
|
150
|
-
<div key={event.id} className="relative pl-8 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
151
|
-
{/* Connecting Line */}
|
|
152
|
-
{i !== events.length - 1 && (
|
|
153
|
-
<div className="absolute left-[13px] top-7 bottom-[-24px] w-[1px] bg-border" />
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
|
-
{/* Icon Badge */}
|
|
157
|
-
<div className={cn(
|
|
158
|
-
"absolute left-0 top-0 w-7 h-7 rounded-full border border-border bg-card flex items-center justify-center z-10 shadow-sm",
|
|
159
|
-
event.event_type === 'error' && "border-red-500/50 bg-red-500/5",
|
|
160
|
-
event.event_type === 'analysis' && "border-purple-500/50 bg-purple-500/5",
|
|
161
|
-
event.event_type === 'action' && "border-emerald-500/50 bg-emerald-500/5"
|
|
162
|
-
)}>
|
|
163
|
-
{getIcon(event.event_type)}
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
<div className="flex flex-col gap-1.5">
|
|
167
|
-
<div className="flex items-center justify-between group">
|
|
168
|
-
<div className="flex items-center gap-2">
|
|
169
|
-
<span className={cn(
|
|
170
|
-
"font-bold uppercase tracking-tight text-[10px]",
|
|
171
|
-
event.event_type === 'info' && "text-muted-foreground",
|
|
172
|
-
event.event_type === 'analysis' && "text-purple-600 dark:text-purple-400",
|
|
173
|
-
event.event_type === 'action' && "text-emerald-600 dark:text-emerald-400",
|
|
174
|
-
event.event_type === 'error' && "text-red-600 dark:text-red-400",
|
|
175
|
-
)}>
|
|
176
|
-
{event.agent_state}
|
|
177
|
-
</span>
|
|
178
|
-
<span className="text-[10px] text-muted-foreground/60">{formatTime(event.created_at)}</span>
|
|
179
|
-
</div>
|
|
180
|
-
{(event.details?.system_prompt || event.details?._raw_response || event.details?.raw_response) && (
|
|
181
|
-
<Button
|
|
182
|
-
variant="ghost"
|
|
183
|
-
size="sm"
|
|
184
|
-
className="h-5 px-1.5 text-[9px] text-muted-foreground hover:text-primary opacity-0 group-hover:opacity-100 transition-opacity"
|
|
185
|
-
onClick={() => toggleExpand(event.id)}
|
|
186
|
-
>
|
|
187
|
-
{expandedEvents[event.id] ? <ChevronUp className="w-3 h-3 mr-1" /> : <ChevronDown className="w-3 h-3 mr-1" />}
|
|
188
|
-
Details
|
|
189
|
-
</Button>
|
|
190
|
-
)}
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{/* Detailed Content */}
|
|
194
|
-
{event.event_type === 'analysis' && event.details ? (
|
|
195
|
-
<div className="bg-purple-500/5 border border-purple-500/10 rounded-lg p-3 space-y-2">
|
|
196
|
-
<div className="flex gap-2">
|
|
197
|
-
<span className={cn(
|
|
198
|
-
"px-1.5 py-0.5 rounded text-[9px] font-bold border",
|
|
199
|
-
event.details.is_useless ? "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20" : "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20"
|
|
200
|
-
)}>
|
|
201
|
-
{event.details.is_useless ? 'USELESS' : 'RELEVANT'}
|
|
202
|
-
</span>
|
|
203
|
-
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold border bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20 uppercase">
|
|
204
|
-
{event.details.category}
|
|
205
|
-
</span>
|
|
206
|
-
</div>
|
|
207
|
-
<p className="text-foreground/90 italic leading-relaxed">
|
|
208
|
-
"{event.details.summary}"
|
|
209
|
-
</p>
|
|
210
|
-
{event.details.suggested_actions && event.details.suggested_actions.length > 0 && (
|
|
211
|
-
<div className="pt-1 flex items-center gap-2 flex-wrap">
|
|
212
|
-
{event.details.suggested_actions.map((a: string) => (
|
|
213
|
-
<div key={a} className="flex items-center gap-1 text-[9px] text-emerald-600 dark:text-emerald-400 font-bold bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20 uppercase">
|
|
214
|
-
<Zap className="w-2.5 h-2.5" />
|
|
215
|
-
{a}
|
|
216
|
-
</div>
|
|
217
|
-
))}
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
) : event.event_type === 'action' && event.details ? (
|
|
222
|
-
<div className="bg-emerald-500/5 border border-emerald-500/10 rounded-lg p-3">
|
|
223
|
-
<p className="text-emerald-600 dark:text-emerald-400 font-bold mb-1 uppercase text-[9px] tracking-widest">Execution Complete</p>
|
|
224
|
-
<p className="text-foreground font-medium">
|
|
225
|
-
{event.details.action === 'delete' && 'Moved to Trash'}
|
|
226
|
-
{event.details.action === 'archive' && 'Archived Email'}
|
|
227
|
-
{event.details.action === 'draft' && 'Drafted Reply'}
|
|
228
|
-
{event.details.action === 'read' && 'Marked as Read'}
|
|
229
|
-
{event.details.action === 'star' && 'Starred Email'}
|
|
230
|
-
{!['delete', 'archive', 'draft', 'read', 'star'].includes(event.details.action) && event.details.action}
|
|
231
|
-
</p>
|
|
232
|
-
{event.details.reason && (
|
|
233
|
-
<p className="text-[10px] text-muted-foreground mt-1.5 flex items-center gap-1">
|
|
234
|
-
<Info className="w-3 h-3" />
|
|
235
|
-
{event.details.reason}
|
|
236
|
-
</p>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
) : event.event_type === 'error' && event.details ? (
|
|
240
|
-
<div className="bg-red-500/5 border border-red-500/10 rounded-lg p-2.5 text-red-600 dark:text-red-400 font-medium">
|
|
241
|
-
{event.details.error}
|
|
242
|
-
</div>
|
|
243
|
-
) : (
|
|
244
|
-
<p className="text-muted-foreground leading-relaxed">
|
|
245
|
-
{event.details?.message || JSON.stringify(event.details)}
|
|
246
|
-
</p>
|
|
247
|
-
)}
|
|
248
|
-
|
|
249
|
-
{/* Collapsible Technical Details */}
|
|
250
|
-
{expandedEvents[event.id] && (
|
|
251
|
-
<div className="mt-2 space-y-3 animate-in fade-in zoom-in-95 duration-200">
|
|
252
|
-
{event.details?.system_prompt && (
|
|
253
|
-
<div className="space-y-1.5">
|
|
254
|
-
<div className="flex items-center gap-1.5 text-[9px] font-bold text-muted-foreground uppercase tracking-widest px-1">
|
|
255
|
-
<Code className="w-3 h-3" /> System Prompt
|
|
256
|
-
</div>
|
|
257
|
-
<div className="bg-secondary/50 rounded-md p-3 border border-border overflow-x-auto">
|
|
258
|
-
<pre className="whitespace-pre-wrap break-words text-[10px] leading-normal text-muted-foreground select-all">
|
|
259
|
-
{event.details.system_prompt}
|
|
260
|
-
</pre>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
{event.details?.content_preview && (
|
|
265
|
-
<div className="space-y-1.5">
|
|
266
|
-
<div className="flex items-center gap-1.5 text-[9px] font-bold text-muted-foreground uppercase tracking-widest px-1">
|
|
267
|
-
<Code className="w-3 h-3" /> Input Content (Cleaned)
|
|
268
|
-
</div>
|
|
269
|
-
<div className="bg-secondary/50 rounded-md p-3 border border-border">
|
|
270
|
-
<p className="whitespace-pre-wrap break-words text-[10px] text-muted-foreground">
|
|
271
|
-
{event.details.content_preview}
|
|
272
|
-
</p>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
)}
|
|
276
|
-
{event.details?._raw_response && (
|
|
277
|
-
<div className="space-y-1.5">
|
|
278
|
-
<div className="flex items-center gap-1.5 text-[9px] font-bold text-muted-foreground uppercase tracking-widest px-1">
|
|
279
|
-
<Code className="w-3 h-3" /> Raw LLM JSON Output
|
|
280
|
-
</div>
|
|
281
|
-
<div className="bg-secondary/50 rounded-md p-3 border border-border overflow-x-auto">
|
|
282
|
-
<pre className="text-[10px] text-muted-foreground select-all">
|
|
283
|
-
{JSON.stringify(JSON.parse(event.details._raw_response), null, 2)}
|
|
284
|
-
</pre>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
{event.details?.raw_response && (
|
|
289
|
-
<div className="space-y-1.5">
|
|
290
|
-
<div className="flex items-center gap-1.5 text-[9px] font-bold text-muted-foreground uppercase tracking-widest px-1">
|
|
291
|
-
<Code className="w-3 h-3" /> Raw Response (from Error)
|
|
292
|
-
</div>
|
|
293
|
-
<div className="bg-secondary/50 rounded-md p-3 border border-border overflow-x-auto">
|
|
294
|
-
<pre className="text-[10px] text-muted-foreground select-all whitespace-pre-wrap">
|
|
295
|
-
{event.details.raw_response}
|
|
296
|
-
</pre>
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
)}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
))}
|
|
305
|
-
</CardContent>
|
|
306
|
-
</Card>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Loader2 } from 'lucide-react';
|
|
2
|
-
import { cn } from '../lib/utils';
|
|
3
|
-
|
|
4
|
-
interface LoadingSpinnerProps {
|
|
5
|
-
size?: 'sm' | 'md' | 'lg';
|
|
6
|
-
className?: string;
|
|
7
|
-
text?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function LoadingSpinner({ size = 'md', className, text }: LoadingSpinnerProps) {
|
|
11
|
-
const sizeClasses = {
|
|
12
|
-
sm: 'w-4 h-4',
|
|
13
|
-
md: 'w-8 h-8',
|
|
14
|
-
lg: 'w-12 h-12',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className={cn('flex flex-col items-center justify-center gap-3', className)}>
|
|
19
|
-
<Loader2 className={cn('animate-spin text-primary', sizeClasses[size])} />
|
|
20
|
-
{text && <p className="text-sm text-muted-foreground">{text}</p>}
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function PageLoader({ text = 'Loading...' }: { text?: string }) {
|
|
26
|
-
return (
|
|
27
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
28
|
-
<LoadingSpinner size="lg" text={text} />
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function CardLoader() {
|
|
34
|
-
return (
|
|
35
|
-
<div className="p-12 flex items-center justify-center">
|
|
36
|
-
<LoadingSpinner size="md" />
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|