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}