From 48b743b2c692b2f41a485cd3093aba68815d5746 Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Mon, 8 Jun 2026 19:10:33 -0400 Subject: [PATCH] Add vault creation UI --- src/components/VaultManager.tsx | 216 ++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/components/VaultManager.tsx diff --git a/src/components/VaultManager.tsx b/src/components/VaultManager.tsx new file mode 100644 index 0000000..dffc961 --- /dev/null +++ b/src/components/VaultManager.tsx @@ -0,0 +1,216 @@ +import { useEffect, useState } from 'react'; +import { Modal } from './Modal'; +import { CloudIcon, LaptopIcon, InfoIcon } from '../icons'; +import { useHabitatAuth } from '../auth/habitatAuth'; + +type VaultManagerProps = { + open: boolean; + onClose: () => void; + onOpenVault: (path: string, type: 'local' | 'synced', did: string | null) => void; + onOpenSettings?: () => void; +}; + +export function VaultManager({ open, onClose, onOpenVault, onOpenSettings }: VaultManagerProps) { + const [path, setPath] = useState(''); + const [error, setError] = useState(null); + const [avatarUrl, setAvatarUrl] = useState(null); + const { authInfo } = useHabitatAuth(); + const isAuthenticated = !!authInfo; + const [vaultType, setVaultType] = useState<'local' | 'synced'>('synced'); + + // Clear error whenever the modal opens so previous errors don't linger. + useEffect(() => { + if (open) { + setError(null); + } + }, [open]); + + // If user is not authenticated, force local vault type. + useEffect(() => { + if (!isAuthenticated) { + setVaultType('local'); + } + }, [isAuthenticated]); + + // Fetch avatar from public Bluesky API when authenticated. + useEffect(() => { + if (!isAuthenticated || !authInfo) { + setAvatarUrl(null); + return; + } + let cancelled = false; + async function fetchAvatar() { + try { + const res = await fetch(`https://api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(authInfo!.handle)}`); + if (!res.ok) return; + const data = await res.json(); + if (!cancelled && data.avatar) { + setAvatarUrl(data.avatar); + } + } catch { + // Ignore fetch errors; fallback to initial letter. + } + } + fetchAvatar(); + return () => { cancelled = true; }; + }, [isAuthenticated, authInfo]); + + const handleOpen = () => { + if (!path.trim()) { + setError('Please select a directory'); + return; + } + setError(null); + onOpenVault(path.trim(), vaultType, isAuthenticated ? authInfo.did : null); + onClose(); + }; + + const handleSelectDirectory = async () => { + try { + const result = await window.textile?.vault?.showOpenDialog(); + if (result && !result.canceled && result.filePaths.length > 0) { + setPath(result.filePaths[0]!); + setError(null); + } + } catch { + // Dialog may not be available in browser dev environment + } + }; + + return ( + +
+
+ + Directory + +
+ + {path || 'No directory selected'} + + +
+ {error &&

{error}

} +
+ +
+
+ + Sync to your Personal Data Server (PDS) + + +
+
+ + +
+ {isAuthenticated && ( +
+ {avatarUrl ? ( + + ) : ( +
+ {authInfo.handle.charAt(0).toUpperCase()} +
+ )} + + {authInfo.handle} + + +
+ )} + {!isAuthenticated && ( +

+ {' '} + to enable syncing. +

+ )} +
+
+ +
+ + +
+
+ ); +} -- 2.54.0