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