Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { 2 Card, 3 CardContent, 4 CardDescription, 5 CardHeader, 6 CardTitle 7} from '@public/components/ui/card' 8import { Button } from '@public/components/ui/button' 9import { Badge } from '@public/components/ui/badge' 10import { 11 Globe, 12 ExternalLink, 13 CheckCircle2, 14 AlertCircle, 15 Loader2, 16 RefreshCw, 17 Settings 18} from 'lucide-react' 19import type { SiteWithDomains } from '../hooks/useSiteData' 20import type { UserInfo } from '../hooks/useUserInfo' 21 22interface SitesTabProps { 23 sites: SiteWithDomains[] 24 sitesLoading: boolean 25 isSyncing: boolean 26 userInfo: UserInfo | null 27 onSyncSites: () => Promise<void> 28 onConfigureSite: (site: SiteWithDomains) => void 29} 30 31export function SitesTab({ 32 sites, 33 sitesLoading, 34 isSyncing, 35 userInfo, 36 onSyncSites, 37 onConfigureSite 38}: SitesTabProps) { 39 const getSiteUrl = (site: SiteWithDomains) => { 40 // Use the first mapped domain if available 41 if (site.domains && site.domains.length > 0) { 42 return `https://${site.domains[0].domain}` 43 } 44 45 // Default fallback URL - use handle instead of DID 46 if (!userInfo) return '#' 47 return `https://sites.wisp.place/${userInfo.handle}/${site.rkey}` 48 } 49 50 const getSiteDomainName = (site: SiteWithDomains) => { 51 // Return the first domain if available 52 if (site.domains && site.domains.length > 0) { 53 return site.domains[0].domain 54 } 55 56 // Use handle instead of DID for display 57 if (!userInfo) return `sites.wisp.place/.../${site.rkey}` 58 return `sites.wisp.place/${userInfo.handle}/${site.rkey}` 59 } 60 61 return ( 62 <div className="space-y-4 min-h-[400px]"> 63 <Card> 64 <CardHeader> 65 <div className="flex items-center justify-between"> 66 <div> 67 <CardTitle>Your Sites</CardTitle> 68 <CardDescription> 69 View and manage all your deployed sites 70 </CardDescription> 71 </div> 72 <Button 73 variant="outline" 74 size="sm" 75 onClick={onSyncSites} 76 disabled={isSyncing || sitesLoading} 77 > 78 <RefreshCw 79 className={`w-4 h-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} 80 /> 81 Sync from PDS 82 </Button> 83 </div> 84 </CardHeader> 85 <CardContent className="space-y-4"> 86 {sitesLoading ? ( 87 <div className="flex items-center justify-center py-8"> 88 <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" /> 89 </div> 90 ) : sites.length === 0 ? ( 91 <div className="text-center py-8 text-muted-foreground"> 92 <p>No sites yet. Upload your first site!</p> 93 </div> 94 ) : ( 95 sites.map((site) => ( 96 <div 97 key={`${site.did}-${site.rkey}`} 98 className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors" 99 > 100 <div className="flex-1"> 101 <div className="flex items-center gap-3 mb-2"> 102 <h3 className="font-semibold text-lg"> 103 {site.display_name || site.rkey} 104 </h3> 105 <Badge 106 variant="secondary" 107 className="text-xs" 108 > 109 active 110 </Badge> 111 </div> 112 113 {/* Display all mapped domains */} 114 {site.domains && site.domains.length > 0 ? ( 115 <div className="space-y-1"> 116 {site.domains.map((domainInfo, idx) => ( 117 <div key={`${domainInfo.domain}-${idx}`} className="flex items-center gap-2"> 118 <a 119 href={`https://${domainInfo.domain}`} 120 target="_blank" 121 rel="noopener noreferrer" 122 className="text-sm text-accent hover:text-accent/80 flex items-center gap-1" 123 > 124 <Globe className="w-3 h-3" /> 125 {domainInfo.domain} 126 <ExternalLink className="w-3 h-3" /> 127 </a> 128 <Badge 129 variant={domainInfo.type === 'wisp' ? 'default' : 'outline'} 130 className="text-xs" 131 > 132 {domainInfo.type} 133 </Badge> 134 {domainInfo.type === 'custom' && ( 135 <Badge 136 variant={domainInfo.verified ? 'default' : 'secondary'} 137 className="text-xs" 138 > 139 {domainInfo.verified ? ( 140 <> 141 <CheckCircle2 className="w-3 h-3 mr-1" /> 142 verified 143 </> 144 ) : ( 145 <> 146 <AlertCircle className="w-3 h-3 mr-1" /> 147 pending 148 </> 149 )} 150 </Badge> 151 )} 152 </div> 153 ))} 154 </div> 155 ) : ( 156 <a 157 href={getSiteUrl(site)} 158 target="_blank" 159 rel="noopener noreferrer" 160 className="text-sm text-muted-foreground hover:text-accent flex items-center gap-1" 161 > 162 {getSiteDomainName(site)} 163 <ExternalLink className="w-3 h-3" /> 164 </a> 165 )} 166 </div> 167 <Button 168 variant="outline" 169 size="sm" 170 onClick={() => onConfigureSite(site)} 171 > 172 <Settings className="w-4 h-4 mr-2" /> 173 Configure 174 </Button> 175 </div> 176 )) 177 )} 178 </CardContent> 179 </Card> 180 181 <div className="p-4 bg-muted/30 rounded-lg border-l-4 border-yellow-500/50"> 182 <div className="flex items-start gap-2"> 183 <AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0" /> 184 <div className="flex-1 space-y-1"> 185 <p className="text-xs font-semibold text-yellow-600 dark:text-yellow-400"> 186 Note about sites.wisp.place URLs 187 </p> 188 <p className="text-xs text-muted-foreground"> 189 Complex sites hosted on <code className="px-1 py-0.5 bg-background rounded text-xs">sites.wisp.place</code> may have broken assets if they use absolute paths (e.g., <code className="px-1 py-0.5 bg-background rounded text-xs">/folder/script.js</code>) in CSS or JavaScript files. While HTML paths are automatically rewritten, CSS and JS files are served as-is. For best results, use a wisp.place subdomain or custom domain, or ensure your site uses relative paths. 190 </p> 191 </div> 192 </div> 193 </div> 194 </div> 195 ) 196}