bloby-bot 0.39.1 → 0.40.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/package.json +1 -1
- package/workspace/client/index.html +0 -3
- package/workspace/client/src/App.tsx +9 -20
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +113 -117
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +34 -32
- package/workspace/client/src/components/Layout/MobileNav.tsx +6 -103
- package/workspace/client/src/components/Layout/Sidebar.tsx +10 -11
- package/workspace/client/src/styles/globals.css +4 -89
- package/workspace/client/public/.well-known/assetlinks.json +0 -8
- package/workspace/client/public/bloby-cyberpunk.png +0 -0
- package/workspace/client/public/brand/blackrock.svg +0 -8
- package/workspace/client/public/kid-breakfast.png +0 -0
- package/workspace/client/public/wallpapers/bg.jpg +0 -0
- package/workspace/client/public/wallpapers/crypto_bg.png +0 -0
- package/workspace/client/public/wallpapers/wp-dusk.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-mountain.jpg +0 -0
- package/workspace/client/public/wallpapers/wp-ocean.jpg +0 -0
- package/workspace/client/src/components/Dashboard/AiChatPage.tsx +0 -145
- package/workspace/client/src/components/Dashboard/CryptoPage.tsx +0 -470
- package/workspace/client/src/components/Dashboard/WishlistPage.tsx +0 -464
- package/workspace/client/src/components/Layout/MiniSidebar.tsx +0 -64
- package/workspace/client/src/components/Lock/PinInput.tsx +0 -107
- package/workspace/client/src/components/Lock/WorkspaceLock.tsx +0 -484
- package/workspace/client/src/components/StickyNotes/StickyNotesOverlay.tsx +0 -396
- package/workspace/client/src/components/StickyNotes/StickyNotesSettingsPage.tsx +0 -427
- package/workspace/client/src/components/Wallpaper/WallpaperBackground.tsx +0 -12
- package/workspace/client/src/components/Wallpaper/WallpaperContext.tsx +0 -160
- package/workspace/client/src/components/Wallpaper/WallpaperPicker.tsx +0 -67
|
@@ -1,484 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { Shield, KeyRound, Lock, ArrowLeft, Check, Loader2, Eye, EyeOff } from 'lucide-react';
|
|
3
|
-
import { cn } from '@/lib/utils';
|
|
4
|
-
import { PinInput } from './PinInput';
|
|
5
|
-
|
|
6
|
-
// ── Types ─────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
type LockState =
|
|
9
|
-
| 'loading'
|
|
10
|
-
| 'setup-choose'
|
|
11
|
-
| 'setup-create'
|
|
12
|
-
| 'setup-confirm'
|
|
13
|
-
| 'locked'
|
|
14
|
-
| 'unlocked';
|
|
15
|
-
|
|
16
|
-
type LockType = 'pin' | 'password';
|
|
17
|
-
|
|
18
|
-
const SESSION_KEY = 'workspace_lock_session';
|
|
19
|
-
|
|
20
|
-
// ── IMPORTANT ─────────────────────────────────────────────────────────
|
|
21
|
-
// The API_BASE below must match how your workspace proxies requests to
|
|
22
|
-
// the backend. Default Bloby workspaces use "/app/api" because the
|
|
23
|
-
// supervisor proxies /app/api/* → backend. If your workspace serves the
|
|
24
|
-
// backend directly (no proxy), change this to "/api".
|
|
25
|
-
const API_BASE = '/app/api';
|
|
26
|
-
|
|
27
|
-
// ── Component ─────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function WorkspaceLock({ children }: { children: React.ReactNode }) {
|
|
30
|
-
const [state, setState] = useState<LockState>('loading');
|
|
31
|
-
const [lockType, setLockType] = useState<LockType | null>(null);
|
|
32
|
-
const [selectedType, setSelectedType] = useState<LockType | null>(null);
|
|
33
|
-
const [value, setValue] = useState('');
|
|
34
|
-
const [error, setError] = useState('');
|
|
35
|
-
const [shaking, setShaking] = useState(false);
|
|
36
|
-
const [submitting, setSubmitting] = useState(false);
|
|
37
|
-
const [showPassword, setShowPassword] = useState(false);
|
|
38
|
-
const [fadeKey, setFadeKey] = useState(0);
|
|
39
|
-
const firstEntry = useRef('');
|
|
40
|
-
|
|
41
|
-
const transitionTo = useCallback((next: LockState) => {
|
|
42
|
-
setFadeKey((k) => k + 1);
|
|
43
|
-
setState(next);
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
const triggerShake = useCallback(() => {
|
|
47
|
-
setShaking(true);
|
|
48
|
-
setTimeout(() => setShaking(false), 500);
|
|
49
|
-
}, []);
|
|
50
|
-
|
|
51
|
-
// ── Check lock status on mount ──────────────────────────────────────
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
(async () => {
|
|
54
|
-
try {
|
|
55
|
-
const res = await fetch(`${API_BASE}/lock/status`, { signal: AbortSignal.timeout(3000) });
|
|
56
|
-
const data = await res.json();
|
|
57
|
-
|
|
58
|
-
if (!data.configured) {
|
|
59
|
-
transitionTo('setup-choose');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
setLockType(data.type);
|
|
64
|
-
|
|
65
|
-
const token = localStorage.getItem(SESSION_KEY);
|
|
66
|
-
if (token) {
|
|
67
|
-
const vRes = await fetch(`${API_BASE}/lock/verify`, {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json' },
|
|
70
|
-
body: JSON.stringify({ token }),
|
|
71
|
-
});
|
|
72
|
-
const vData = await vRes.json();
|
|
73
|
-
if (vData.valid) {
|
|
74
|
-
setState('unlocked');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
localStorage.removeItem(SESSION_KEY);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
transitionTo('locked');
|
|
81
|
-
} catch {
|
|
82
|
-
// Fail-open if backend is unreachable
|
|
83
|
-
setState('unlocked');
|
|
84
|
-
}
|
|
85
|
-
})();
|
|
86
|
-
}, [transitionTo]);
|
|
87
|
-
|
|
88
|
-
// ── Actions ─────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
const doSetup = async (type: LockType, val: string) => {
|
|
91
|
-
setSubmitting(true);
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetch(`${API_BASE}/lock/setup`, {
|
|
94
|
-
method: 'POST',
|
|
95
|
-
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body: JSON.stringify({ type, value: val }),
|
|
97
|
-
});
|
|
98
|
-
const data = await res.json();
|
|
99
|
-
if (data.token) localStorage.setItem(SESSION_KEY, data.token);
|
|
100
|
-
setState('unlocked');
|
|
101
|
-
} catch {
|
|
102
|
-
setError('Failed to save. Try again.');
|
|
103
|
-
triggerShake();
|
|
104
|
-
} finally {
|
|
105
|
-
setSubmitting(false);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const doVerify = async (val: string) => {
|
|
110
|
-
setSubmitting(true);
|
|
111
|
-
try {
|
|
112
|
-
const res = await fetch(`${API_BASE}/lock/verify`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
headers: { 'Content-Type': 'application/json' },
|
|
115
|
-
body: JSON.stringify({ value: val }),
|
|
116
|
-
});
|
|
117
|
-
const data = await res.json();
|
|
118
|
-
|
|
119
|
-
if (data.valid) {
|
|
120
|
-
if (data.token) localStorage.setItem(SESSION_KEY, data.token);
|
|
121
|
-
setState('unlocked');
|
|
122
|
-
} else {
|
|
123
|
-
setError(lockType === 'pin' ? 'Wrong PIN' : 'Wrong password');
|
|
124
|
-
setValue('');
|
|
125
|
-
triggerShake();
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
setError('Connection failed');
|
|
129
|
-
} finally {
|
|
130
|
-
setSubmitting(false);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// ── PIN complete handler (auto-advances) ────────────────────────────
|
|
135
|
-
const handlePinComplete = useCallback(
|
|
136
|
-
(completed: string) => {
|
|
137
|
-
if (state === 'setup-create') {
|
|
138
|
-
firstEntry.current = completed;
|
|
139
|
-
setValue('');
|
|
140
|
-
setError('');
|
|
141
|
-
transitionTo('setup-confirm');
|
|
142
|
-
} else if (state === 'setup-confirm') {
|
|
143
|
-
if (firstEntry.current === completed) {
|
|
144
|
-
doSetup(selectedType!, completed);
|
|
145
|
-
} else {
|
|
146
|
-
setError("PINs don't match. Try again.");
|
|
147
|
-
setValue('');
|
|
148
|
-
triggerShake();
|
|
149
|
-
}
|
|
150
|
-
} else if (state === 'locked') {
|
|
151
|
-
doVerify(completed);
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
[state, selectedType, transitionTo, triggerShake],
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// ── Password submit handler ─────────────────────────────────────────
|
|
158
|
-
const handlePasswordSubmit = () => {
|
|
159
|
-
if (submitting || !value) return;
|
|
160
|
-
|
|
161
|
-
if (state === 'setup-create') {
|
|
162
|
-
if (value.length < 4) {
|
|
163
|
-
setError('At least 4 characters');
|
|
164
|
-
triggerShake();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
firstEntry.current = value;
|
|
168
|
-
setValue('');
|
|
169
|
-
setError('');
|
|
170
|
-
setShowPassword(false);
|
|
171
|
-
transitionTo('setup-confirm');
|
|
172
|
-
} else if (state === 'setup-confirm') {
|
|
173
|
-
if (firstEntry.current === value) {
|
|
174
|
-
doSetup(selectedType!, value);
|
|
175
|
-
} else {
|
|
176
|
-
setError("Passwords don't match. Try again.");
|
|
177
|
-
setValue('');
|
|
178
|
-
triggerShake();
|
|
179
|
-
}
|
|
180
|
-
} else if (state === 'locked') {
|
|
181
|
-
doVerify(value);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// ── Navigation ──────────────────────────────────────────────────────
|
|
186
|
-
const goBack = () => {
|
|
187
|
-
if (state === 'setup-confirm') {
|
|
188
|
-
transitionTo('setup-create');
|
|
189
|
-
} else {
|
|
190
|
-
transitionTo('setup-choose');
|
|
191
|
-
}
|
|
192
|
-
setValue('');
|
|
193
|
-
setError('');
|
|
194
|
-
setShowPassword(false);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const chooseType = (type: LockType) => {
|
|
198
|
-
setSelectedType(type);
|
|
199
|
-
setValue('');
|
|
200
|
-
setError('');
|
|
201
|
-
setShowPassword(false);
|
|
202
|
-
transitionTo('setup-create');
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// ── Render ──────────────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
if (state === 'loading') {
|
|
208
|
-
return (
|
|
209
|
-
<div
|
|
210
|
-
className="fixed inset-0 z-[300] flex items-center justify-center"
|
|
211
|
-
style={{
|
|
212
|
-
backgroundColor: '#0A0A0A',
|
|
213
|
-
backgroundImage: 'radial-gradient(circle, #1f1f1f 1.2px, transparent 1.2px)',
|
|
214
|
-
backgroundSize: '20px 20px',
|
|
215
|
-
}}
|
|
216
|
-
>
|
|
217
|
-
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (state === 'unlocked') return <>{children}</>;
|
|
223
|
-
|
|
224
|
-
const isSetup = state === 'setup-choose' || state === 'setup-create' || state === 'setup-confirm';
|
|
225
|
-
const currentType = isSetup ? selectedType : lockType;
|
|
226
|
-
const isPin = currentType === 'pin';
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<div
|
|
230
|
-
className="fixed inset-0 z-[300] flex items-center justify-center overflow-auto"
|
|
231
|
-
style={{
|
|
232
|
-
backgroundColor: '#0A0A0A',
|
|
233
|
-
backgroundImage: 'radial-gradient(circle, #1f1f1f 1.2px, transparent 1.2px)',
|
|
234
|
-
backgroundSize: '20px 20px',
|
|
235
|
-
}}
|
|
236
|
-
>
|
|
237
|
-
<div className="w-full max-w-sm px-4 sm:px-5 py-8">
|
|
238
|
-
{/* ── Logo + badge ── */}
|
|
239
|
-
<div className="flex flex-col items-center mb-8">
|
|
240
|
-
<img
|
|
241
|
-
src="/bloby.png"
|
|
242
|
-
alt=""
|
|
243
|
-
className="h-14 mb-4 object-contain"
|
|
244
|
-
draggable={false}
|
|
245
|
-
/>
|
|
246
|
-
<div className="flex items-center gap-1.5">
|
|
247
|
-
<Shield className="h-3.5 w-3.5 text-muted-foreground" />
|
|
248
|
-
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-widest">
|
|
249
|
-
Workspace Lock
|
|
250
|
-
</span>
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
{/* ── Main card ── */}
|
|
255
|
-
<div className="animated-border animated-border-slow rounded-2xl p-px">
|
|
256
|
-
<div
|
|
257
|
-
key={fadeKey}
|
|
258
|
-
className="rounded-2xl bg-[#111111] p-5 sm:p-7 animate-[fadeIn_0.2s_ease-out]"
|
|
259
|
-
style={{ animationFillMode: 'backwards' }}
|
|
260
|
-
>
|
|
261
|
-
{/* ════════════════ Choose screen ════════════════ */}
|
|
262
|
-
{state === 'setup-choose' && (
|
|
263
|
-
<div className="space-y-6">
|
|
264
|
-
<div className="text-center">
|
|
265
|
-
<h1
|
|
266
|
-
className="text-lg font-semibold text-foreground"
|
|
267
|
-
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
|
268
|
-
>
|
|
269
|
-
Secure your workspace
|
|
270
|
-
</h1>
|
|
271
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
272
|
-
Choose your preferred lock method
|
|
273
|
-
</p>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
<div className="grid grid-cols-2 gap-3">
|
|
277
|
-
<button
|
|
278
|
-
onClick={() => chooseType('pin')}
|
|
279
|
-
className="group rounded-xl border-2 border-[#252525] bg-[#161616] p-5 transition-all duration-200 hover:border-white/[0.12] hover:bg-[#1A1A1A] focus:outline-none focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1)]"
|
|
280
|
-
>
|
|
281
|
-
<div className="flex flex-col items-center gap-3">
|
|
282
|
-
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/[0.03] group-hover:bg-white/[0.06] transition-colors">
|
|
283
|
-
<KeyRound className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
284
|
-
</div>
|
|
285
|
-
<div className="text-center">
|
|
286
|
-
<div className="text-sm font-medium text-foreground">PIN Code</div>
|
|
287
|
-
<div className="text-[11px] text-muted-foreground mt-0.5">6 digits, quick access</div>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
</button>
|
|
291
|
-
|
|
292
|
-
<button
|
|
293
|
-
onClick={() => chooseType('password')}
|
|
294
|
-
className="group rounded-xl border-2 border-[#252525] bg-[#161616] p-5 transition-all duration-200 hover:border-white/[0.12] hover:bg-[#1A1A1A] focus:outline-none focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1)]"
|
|
295
|
-
>
|
|
296
|
-
<div className="flex flex-col items-center gap-3">
|
|
297
|
-
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/[0.03] group-hover:bg-white/[0.06] transition-colors">
|
|
298
|
-
<Lock className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
299
|
-
</div>
|
|
300
|
-
<div className="text-center">
|
|
301
|
-
<div className="text-sm font-medium text-foreground">Password</div>
|
|
302
|
-
<div className="text-[11px] text-muted-foreground mt-0.5">Custom passphrase</div>
|
|
303
|
-
</div>
|
|
304
|
-
</div>
|
|
305
|
-
</button>
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
309
|
-
|
|
310
|
-
{/* ════════════════ Setup: create / confirm ════════════════ */}
|
|
311
|
-
{(state === 'setup-create' || state === 'setup-confirm') && selectedType && (
|
|
312
|
-
<div className="space-y-5">
|
|
313
|
-
<div className="flex items-center gap-3">
|
|
314
|
-
<button
|
|
315
|
-
onClick={goBack}
|
|
316
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-white/[0.04] hover:bg-white/[0.08] transition-colors"
|
|
317
|
-
>
|
|
318
|
-
<ArrowLeft className="h-3.5 w-3.5 text-muted-foreground" />
|
|
319
|
-
</button>
|
|
320
|
-
<div className="min-w-0">
|
|
321
|
-
<h1
|
|
322
|
-
className="text-base font-semibold text-foreground truncate"
|
|
323
|
-
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
|
324
|
-
>
|
|
325
|
-
{state === 'setup-create'
|
|
326
|
-
? isPin ? 'Set your PIN' : 'Set your password'
|
|
327
|
-
: isPin ? 'Confirm your PIN' : 'Confirm your password'}
|
|
328
|
-
</h1>
|
|
329
|
-
<p className="text-xs text-muted-foreground">
|
|
330
|
-
{state === 'setup-create'
|
|
331
|
-
? isPin ? 'Choose a 6-digit PIN' : 'Minimum 4 characters'
|
|
332
|
-
: 'Enter it once more to confirm'}
|
|
333
|
-
</p>
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<div className="flex flex-col items-center gap-3">
|
|
338
|
-
{isPin ? (
|
|
339
|
-
<PinInput
|
|
340
|
-
key={state}
|
|
341
|
-
value={value}
|
|
342
|
-
onChange={(v) => { setValue(v); setError(''); }}
|
|
343
|
-
onComplete={handlePinComplete}
|
|
344
|
-
error={shaking}
|
|
345
|
-
disabled={submitting}
|
|
346
|
-
/>
|
|
347
|
-
) : (
|
|
348
|
-
<>
|
|
349
|
-
<PasswordField
|
|
350
|
-
key={state}
|
|
351
|
-
value={value}
|
|
352
|
-
onChange={(v) => { setValue(v); setError(''); }}
|
|
353
|
-
onSubmit={handlePasswordSubmit}
|
|
354
|
-
placeholder={state === 'setup-confirm' ? 'Re-enter password' : 'Enter password'}
|
|
355
|
-
show={showPassword}
|
|
356
|
-
onToggleShow={() => setShowPassword(!showPassword)}
|
|
357
|
-
error={shaking}
|
|
358
|
-
disabled={submitting}
|
|
359
|
-
/>
|
|
360
|
-
<button
|
|
361
|
-
onClick={handlePasswordSubmit}
|
|
362
|
-
disabled={submitting || value.length < (state === 'setup-create' ? 4 : 1)}
|
|
363
|
-
className="w-full h-10 rounded-xl bg-primary text-primary-foreground text-sm font-medium transition-all hover:bg-primary/90 disabled:opacity-25 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
364
|
-
>
|
|
365
|
-
{submitting ? (
|
|
366
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
367
|
-
) : state === 'setup-confirm' ? (
|
|
368
|
-
<><Check className="h-4 w-4" />Confirm</>
|
|
369
|
-
) : (
|
|
370
|
-
'Continue'
|
|
371
|
-
)}
|
|
372
|
-
</button>
|
|
373
|
-
</>
|
|
374
|
-
)}
|
|
375
|
-
{error && <p className="text-xs text-destructive font-medium">{error}</p>}
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<div className="flex items-center justify-center gap-1.5 pt-1">
|
|
379
|
-
<div className="w-1.5 h-1.5 rounded-full bg-foreground" />
|
|
380
|
-
<div className={cn('w-1.5 h-1.5 rounded-full transition-colors duration-300', state === 'setup-confirm' ? 'bg-foreground' : 'bg-white/15')} />
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
)}
|
|
384
|
-
|
|
385
|
-
{/* ════════════════ Locked ════════════════ */}
|
|
386
|
-
{state === 'locked' && lockType && (
|
|
387
|
-
<div className="space-y-6">
|
|
388
|
-
<div className="text-center">
|
|
389
|
-
<h1
|
|
390
|
-
className="text-lg font-semibold text-foreground"
|
|
391
|
-
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
|
392
|
-
>
|
|
393
|
-
Welcome back
|
|
394
|
-
</h1>
|
|
395
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
396
|
-
{lockType === 'pin' ? 'Enter your PIN to continue' : 'Enter your password to continue'}
|
|
397
|
-
</p>
|
|
398
|
-
</div>
|
|
399
|
-
|
|
400
|
-
<div className="flex flex-col items-center gap-3">
|
|
401
|
-
{lockType === 'pin' ? (
|
|
402
|
-
<PinInput
|
|
403
|
-
value={value}
|
|
404
|
-
onChange={(v) => { setValue(v); setError(''); }}
|
|
405
|
-
onComplete={handlePinComplete}
|
|
406
|
-
error={shaking}
|
|
407
|
-
disabled={submitting}
|
|
408
|
-
/>
|
|
409
|
-
) : (
|
|
410
|
-
<>
|
|
411
|
-
<PasswordField
|
|
412
|
-
value={value}
|
|
413
|
-
onChange={(v) => { setValue(v); setError(''); }}
|
|
414
|
-
onSubmit={handlePasswordSubmit}
|
|
415
|
-
placeholder="Enter password"
|
|
416
|
-
show={showPassword}
|
|
417
|
-
onToggleShow={() => setShowPassword(!showPassword)}
|
|
418
|
-
error={shaking}
|
|
419
|
-
disabled={submitting}
|
|
420
|
-
/>
|
|
421
|
-
<button
|
|
422
|
-
onClick={handlePasswordSubmit}
|
|
423
|
-
disabled={submitting || !value}
|
|
424
|
-
className="w-full h-10 rounded-xl bg-primary text-primary-foreground text-sm font-medium transition-all hover:bg-primary/90 disabled:opacity-25 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
425
|
-
>
|
|
426
|
-
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Unlock'}
|
|
427
|
-
</button>
|
|
428
|
-
</>
|
|
429
|
-
)}
|
|
430
|
-
{error && <p className="text-xs text-destructive font-medium">{error}</p>}
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
)}
|
|
434
|
-
</div>
|
|
435
|
-
</div>
|
|
436
|
-
|
|
437
|
-
<p className="text-center text-[11px] text-muted-foreground/40 mt-6">
|
|
438
|
-
Ask your agent to reset if you forget your{' '}
|
|
439
|
-
{lockType === 'pin' || selectedType === 'pin' ? 'PIN' : 'password'}
|
|
440
|
-
</p>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ── Password field sub-component ──────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
function PasswordField({
|
|
449
|
-
value, onChange, onSubmit, placeholder, show, onToggleShow, error, disabled,
|
|
450
|
-
}: {
|
|
451
|
-
value: string; onChange: (v: string) => void; onSubmit: () => void;
|
|
452
|
-
placeholder: string; show: boolean; onToggleShow: () => void;
|
|
453
|
-
error: boolean; disabled: boolean;
|
|
454
|
-
}) {
|
|
455
|
-
return (
|
|
456
|
-
<div className={cn('relative w-full', error && 'animate-shake')}>
|
|
457
|
-
<input
|
|
458
|
-
type={show ? 'text' : 'password'}
|
|
459
|
-
value={value}
|
|
460
|
-
onChange={(e) => onChange(e.target.value)}
|
|
461
|
-
onKeyDown={(e) => e.key === 'Enter' && onSubmit()}
|
|
462
|
-
placeholder={placeholder}
|
|
463
|
-
disabled={disabled}
|
|
464
|
-
autoFocus
|
|
465
|
-
autoComplete="off"
|
|
466
|
-
className={cn(
|
|
467
|
-
'w-full h-12 rounded-xl border-2 bg-[#161616] px-4 pr-11 text-sm text-foreground placeholder:text-muted-foreground/50 outline-none transition-all duration-200',
|
|
468
|
-
'border-[#252525] focus:border-[rgba(175,39,227,0.45)] focus:shadow-[0_0_0_3px_rgba(175,39,227,0.1),0_0_24px_-6px_rgba(175,39,227,0.18)]',
|
|
469
|
-
error && 'border-destructive/40',
|
|
470
|
-
disabled && 'opacity-40',
|
|
471
|
-
)}
|
|
472
|
-
style={{ fontSize: '16px' }}
|
|
473
|
-
/>
|
|
474
|
-
<button
|
|
475
|
-
type="button"
|
|
476
|
-
onClick={onToggleShow}
|
|
477
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground/50 hover:text-foreground transition-colors p-1"
|
|
478
|
-
tabIndex={-1}
|
|
479
|
-
>
|
|
480
|
-
{show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
481
|
-
</button>
|
|
482
|
-
</div>
|
|
483
|
-
);
|
|
484
|
-
}
|