Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
8.0 kB · 231 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232import { 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<string | null>(null); const [avatarUrl, setAvatarUrl] = useState<string | null>(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 ( <Modal open={open} onClose={onClose} ariaLabel="Create Vault dialog" title="Create Vault" panelClassName="w-full max-w-md" bodyClassName="flex min-h-0 flex-1 flex-col gap-4 p-5" > <div className="space-y-5"> <div> <span className="text-foreground/80 mb-1 block text-xs font-medium"> Directory </span> <div className="flex items-center gap-2"> <span className="text-foreground/60 flex-1 truncate text-xs"> {path || 'No directory selected'} </span> <button type="button" onClick={handleSelectDirectory} className="bg-primary/10 text-primary hover:bg-primary/20 border-primary/30 shrink-0 cursor-pointer rounded-md border px-2.5 py-1.5 text-xs transition-colors" > Browse… </button> </div> {error && <p className="text-destructive mt-1 text-xs">{error}</p>} </div>
<div> <div className="mb-1.5 flex items-center gap-1"> <span className="text-foreground/80 text-xs font-medium"> Sync to your Personal Data Server (PDS) </span> <button type="button" className="text-foreground/40 hover:text-foreground/70 inline-block cursor-help border-0 bg-transparent p-0" title="Syncing your files to your Personal Data Server (PDS) enables sharing files to other devices and is needed for live collaboration. Keeping your files local will ensure Textile never sends your files off your device." aria-label="Info about PDS sync" onClick={(e) => e.preventDefault()} > <InfoIcon className="size-3.5" /> </button> </div> <div className="border-border flex overflow-hidden rounded-md border"> <button type="button" onClick={() => isAuthenticated && setVaultType('synced')} disabled={!isAuthenticated} className={`flex flex-1 cursor-pointer items-center justify-center gap-1.5 py-1.5 text-xs ${ vaultType === 'synced' ? 'bg-primary text-primary-foreground font-medium' : 'bg-background text-foreground/60 hover:bg-border/20' } ${!isAuthenticated ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`} > <CloudIcon className="size-3.5" /> Sync to PDS </button> <button type="button" onClick={() => setVaultType('local')} className={`border-border flex flex-1 cursor-pointer items-center justify-center gap-1.5 border-l py-1.5 text-xs ${ vaultType === 'local' ? 'bg-primary text-primary-foreground font-medium' : 'bg-background text-foreground/60 hover:bg-border/20' }`} > <LaptopIcon className="size-3.5" /> Keep my files local </button> </div> {isAuthenticated && ( <div className={`mt-1.5 flex items-center gap-1.5 ${vaultType === 'synced' ? '' : 'invisible'}`} > {avatarUrl ? ( <img src={avatarUrl} alt="" className="size-5 rounded-full object-cover" aria-hidden /> ) : ( <div className="bg-primary text-primary-foreground flex size-5 items-center justify-center rounded-full text-[10px] font-semibold"> {authInfo.handle.charAt(0).toUpperCase()} </div> )} <span className="text-foreground/80 text-xs"> {authInfo.handle} </span> <button type="button" onClick={() => { onClose(); onOpenSettings?.(); }} className="text-foreground/40 hover:text-foreground/70 cursor-pointer border-0 bg-transparent p-0 text-[10px]" > Switch Account </button> </div> )} {!isAuthenticated && ( <p className="text-foreground/60 mt-1 text-xs"> <button type="button" onClick={() => { onClose(); onOpenSettings?.(); }} className="text-primary cursor-pointer border-0 bg-transparent p-0 text-xs hover:underline" > Sign in to your Atmosphere Account </button>{' '} to enable syncing. </p> )} </div> </div>
<div className="mt-auto flex justify-end gap-2"> <button type="button" onClick={onClose} className="border-border bg-background text-foreground hover:bg-border/30 cursor-pointer rounded-md border px-3 py-1.5 text-xs" > Cancel </button> <button type="button" onClick={handleOpen} className="bg-primary text-primary-foreground cursor-pointer rounded-md px-3 py-1.5 text-xs hover:opacity-90" > Create Vault </button> </div> </Modal> );}