Something went wrong. Try again.
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
Something went wrong. Try again.
12 kB · 415 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416import { useState, useEffect } from 'react'import { createRoot } from 'react-dom/client'import { Button } from '@public/components/ui/button'import { Tabs, TabsContent, TabsList, TabsTrigger} from '@public/components/ui/tabs'import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter} from '@public/components/ui/dialog'import { Checkbox } from '@public/components/ui/checkbox'import { Label } from '@public/components/ui/label'import { Badge } from '@public/components/ui/badge'import { Globe, Loader2, Trash2} from 'lucide-react'import Layout from '@public/layouts'import { useUserInfo } from './hooks/useUserInfo'import { useSiteData, type SiteWithDomains } from './hooks/useSiteData'import { useDomainData } from './hooks/useDomainData'import { SitesTab } from './tabs/SitesTab'import { DomainsTab } from './tabs/DomainsTab'import { UploadTab } from './tabs/UploadTab'import { CLITab } from './tabs/CLITab'
function Dashboard() { // Use custom hooks const { userInfo, loading, fetchUserInfo } = useUserInfo() const { sites, sitesLoading, isSyncing, fetchSites, syncSites, deleteSite } = useSiteData() const { wispDomain, customDomains, domainsLoading, verificationStatus, fetchDomains, addCustomDomain, verifyDomain, deleteCustomDomain, mapWispDomain, mapCustomDomain, claimWispDomain, checkWispAvailability } = useDomainData()
// Site configuration modal state (shared across components) const [configuringSite, setConfiguringSite] = useState<SiteWithDomains | null>(null) const [selectedDomains, setSelectedDomains] = useState<Set<string>>(new Set()) const [isSavingConfig, setIsSavingConfig] = useState(false) const [isDeletingSite, setIsDeletingSite] = useState(false)
// Fetch initial data on mount useEffect(() => { fetchUserInfo() fetchSites() fetchDomains() }, [])
// Handle site configuration modal const handleConfigureSite = (site: SiteWithDomains) => { setConfiguringSite(site)
// Build set of currently mapped domains const mappedDomains = new Set<string>()
if (site.domains) { site.domains.forEach(domainInfo => { if (domainInfo.type === 'wisp') { mappedDomains.add('wisp') } else if (domainInfo.id) { mappedDomains.add(domainInfo.id) } }) }
setSelectedDomains(mappedDomains) }
const handleSaveSiteConfig = async () => { if (!configuringSite) return
setIsSavingConfig(true) try { // Determine which domains should be mapped/unmapped const shouldMapWisp = selectedDomains.has('wisp') const isCurrentlyMappedToWisp = wispDomain && wispDomain.rkey === configuringSite.rkey
// Handle wisp domain mapping if (shouldMapWisp && !isCurrentlyMappedToWisp) { await mapWispDomain(configuringSite.rkey) } else if (!shouldMapWisp && isCurrentlyMappedToWisp) { await mapWispDomain(null) }
// Handle custom domain mappings const selectedCustomDomainIds = Array.from(selectedDomains).filter(id => id !== 'wisp') const currentlyMappedCustomDomains = customDomains.filter( d => d.rkey === configuringSite.rkey )
// Unmap domains that are no longer selected for (const domain of currentlyMappedCustomDomains) { if (!selectedCustomDomainIds.includes(domain.id)) { await mapCustomDomain(domain.id, null) } }
// Map newly selected domains for (const domainId of selectedCustomDomainIds) { const isAlreadyMapped = currentlyMappedCustomDomains.some(d => d.id === domainId) if (!isAlreadyMapped) { await mapCustomDomain(domainId, configuringSite.rkey) } }
// Refresh both domains and sites to get updated mappings await fetchDomains() await fetchSites() setConfiguringSite(null) } catch (err) { console.error('Save config error:', err) alert( `Failed to save configuration: ${err instanceof Error ? err.message : 'Unknown error'}` ) } finally { setIsSavingConfig(false) } }
const handleDeleteSite = async () => { if (!configuringSite) return
if (!confirm(`Are you sure you want to delete "${configuringSite.display_name || configuringSite.rkey}"? This action cannot be undone.`)) { return }
setIsDeletingSite(true) const success = await deleteSite(configuringSite.rkey) if (success) { // Refresh domains in case this site was mapped await fetchDomains() setConfiguringSite(null) } setIsDeletingSite(false) }
const handleUploadComplete = async () => { await fetchSites() }
if (loading) { return ( <div className="w-full min-h-screen bg-background flex items-center justify-center"> <Loader2 className="w-8 h-8 animate-spin text-primary" /> </div> ) }
return ( <div className="w-full min-h-screen bg-background"> {/* Header */} <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> <div className="container mx-auto px-4 py-4 flex items-center justify-between"> <div className="flex items-center gap-2"> <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> <Globe className="w-5 h-5 text-primary-foreground" /> </div> <span className="text-xl font-semibold text-foreground"> wisp.place </span> </div> <div className="flex items-center gap-3"> <span className="text-sm text-muted-foreground"> {userInfo?.handle || 'Loading...'} </span> </div> </div> </header>
<div className="container mx-auto px-4 py-8 max-w-6xl w-full"> <div className="mb-8"> <h1 className="text-3xl font-bold mb-2">Dashboard</h1> <p className="text-muted-foreground"> Manage your sites and domains </p> </div>
<Tabs defaultValue="sites" className="space-y-6 w-full"> <TabsList className="grid w-full grid-cols-4"> <TabsTrigger value="sites">Sites</TabsTrigger> <TabsTrigger value="domains">Domains</TabsTrigger> <TabsTrigger value="upload">Upload</TabsTrigger> <TabsTrigger value="cli">CLI</TabsTrigger> </TabsList>
{/* Sites Tab */} <TabsContent value="sites"> <SitesTab sites={sites} sitesLoading={sitesLoading} isSyncing={isSyncing} userInfo={userInfo} onSyncSites={syncSites} onConfigureSite={handleConfigureSite} /> </TabsContent>
{/* Domains Tab */} <TabsContent value="domains"> <DomainsTab wispDomain={wispDomain} customDomains={customDomains} domainsLoading={domainsLoading} verificationStatus={verificationStatus} userInfo={userInfo} onAddCustomDomain={addCustomDomain} onVerifyDomain={verifyDomain} onDeleteCustomDomain={deleteCustomDomain} onClaimWispDomain={claimWispDomain} onCheckWispAvailability={checkWispAvailability} /> </TabsContent>
{/* Upload Tab */} <TabsContent value="upload"> <UploadTab sites={sites} sitesLoading={sitesLoading} onUploadComplete={handleUploadComplete} /> </TabsContent>
{/* CLI Tab */} <TabsContent value="cli"> <CLITab /> </TabsContent> </Tabs> </div>
{/* Site Configuration Modal */} <Dialog open={configuringSite !== null} onOpenChange={(open) => !open && setConfiguringSite(null)} > <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle>Configure Site Domains</DialogTitle> <DialogDescription> Select which domains should be mapped to this site. You can select multiple domains. </DialogDescription> </DialogHeader> {configuringSite && ( <div className="space-y-4 py-4"> <div className="p-3 bg-muted/30 rounded-lg"> <p className="text-sm font-medium mb-1">Site:</p> <p className="font-mono text-sm"> {configuringSite.display_name || configuringSite.rkey} </p> </div>
<div className="space-y-3"> <p className="text-sm font-medium">Available Domains:</p>
{wispDomain && ( <div className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-muted/30"> <Checkbox id="wisp" checked={selectedDomains.has('wisp')} onCheckedChange={(checked) => { const newSelected = new Set(selectedDomains) if (checked) { newSelected.add('wisp') } else { newSelected.delete('wisp') } setSelectedDomains(newSelected) }} /> <Label htmlFor="wisp" className="flex-1 cursor-pointer" > <div className="flex items-center justify-between"> <span className="font-mono text-sm"> {wispDomain.domain} </span> <Badge variant="secondary" className="text-xs ml-2"> Wisp </Badge> </div> </Label> </div> )}
{customDomains .filter((d) => d.verified) .map((domain) => ( <div key={domain.id} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-muted/30" > <Checkbox id={domain.id} checked={selectedDomains.has(domain.id)} onCheckedChange={(checked) => { const newSelected = new Set(selectedDomains) if (checked) { newSelected.add(domain.id) } else { newSelected.delete(domain.id) } setSelectedDomains(newSelected) }} /> <Label htmlFor={domain.id} className="flex-1 cursor-pointer" > <div className="flex items-center justify-between"> <span className="font-mono text-sm"> {domain.domain} </span> <Badge variant="outline" className="text-xs ml-2" > Custom </Badge> </div> </Label> </div> ))}
{customDomains.filter(d => d.verified).length === 0 && !wispDomain && ( <p className="text-sm text-muted-foreground py-4 text-center"> No domains available. Add a custom domain or claim your wisp.place subdomain. </p> )} </div>
<div className="p-3 bg-muted/20 rounded-lg border-l-4 border-blue-500/50"> <p className="text-xs text-muted-foreground"> <strong>Note:</strong> If no domains are selected, the site will be accessible at:{' '} <span className="font-mono"> sites.wisp.place/{userInfo?.handle || '...'}/{configuringSite.rkey} </span> </p> </div> </div> )} <DialogFooter className="flex flex-col sm:flex-row sm:justify-between gap-2"> <Button variant="destructive" onClick={handleDeleteSite} disabled={isSavingConfig || isDeletingSite} className="sm:mr-auto" > {isDeletingSite ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> Deleting... </> ) : ( <> <Trash2 className="w-4 h-4 mr-2" /> Delete Site </> )} </Button> <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto"> <Button variant="outline" onClick={() => setConfiguringSite(null)} disabled={isSavingConfig || isDeletingSite} className="w-full sm:w-auto" > Cancel </Button> <Button onClick={handleSaveSiteConfig} disabled={isSavingConfig || isDeletingSite} className="w-full sm:w-auto" > {isSavingConfig ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> Saving... </> ) : ( 'Save' )} </Button> </div> </DialogFooter> </DialogContent> </Dialog> </div> )}
const root = createRoot(document.getElementById('elysia')!)root.render( <Layout className="gap-6"> <Dashboard /> </Layout>)