Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { useState, useRef, useEffect } from 'react' 2import { createRoot } from 'react-dom/client' 3import { 4 ArrowRight, 5 Shield, 6 Zap, 7 Globe, 8 Lock, 9 Code, 10 Server 11} from 'lucide-react' 12 13import Layout from '@public/layouts' 14import { Button } from '@public/components/ui/button' 15import { Card } from '@public/components/ui/card' 16 17function App() { 18 const [showForm, setShowForm] = useState(false) 19 const [checkingAuth, setCheckingAuth] = useState(true) 20 const inputRef = useRef<HTMLInputElement>(null) 21 22 useEffect(() => { 23 // Check authentication status on mount 24 const checkAuth = async () => { 25 try { 26 const response = await fetch('/api/auth/status', { 27 credentials: 'include' 28 }) 29 const data = await response.json() 30 if (data.authenticated) { 31 // User is already authenticated, redirect to editor 32 window.location.href = '/editor' 33 return 34 } 35 } catch (error) { 36 console.error('Auth check failed:', error) 37 } finally { 38 setCheckingAuth(false) 39 } 40 } 41 42 checkAuth() 43 }, []) 44 45 useEffect(() => { 46 if (showForm) { 47 setTimeout(() => inputRef.current?.focus(), 500) 48 } 49 }, [showForm]) 50 51 if (checkingAuth) { 52 return ( 53 <div className="min-h-screen bg-background flex items-center justify-center"> 54 <div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin"></div> 55 </div> 56 ) 57 } 58 59 return ( 60 <> 61 <div className="min-h-screen"> 62 {/* Header */} 63 <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 64 <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 65 <div className="flex items-center gap-2"> 66 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 67 <Globe className="w-5 h-5 text-primary-foreground" /> 68 </div> 69 <span className="text-xl font-semibold text-foreground"> 70 wisp.place 71 </span> 72 </div> 73 <div className="flex items-center gap-3"> 74 <Button 75 variant="ghost" 76 size="sm" 77 onClick={() => setShowForm(true)} 78 > 79 Sign In 80 </Button> 81 <Button 82 size="sm" 83 className="bg-accent text-accent-foreground hover:bg-accent/90" 84 > 85 Get Started 86 </Button> 87 </div> 88 </div> 89 </header> 90 91 {/* Hero Section */} 92 <section className="container mx-auto px-4 py-20 md:py-32"> 93 <div className="max-w-4xl mx-auto text-center"> 94 <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8"> 95 <span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span> 96 <span className="text-sm text-foreground"> 97 Built on AT Protocol 98 </span> 99 </div> 100 101 <h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight"> 102 Your Website.Your Control. Lightning Fast. 103 </h1> 104 105 <p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto"> 106 Host static sites in your AT Protocol account. You 107 keep ownership and control. We just serve them fast 108 through our CDN. 109 </p> 110 111 <div className="max-w-md mx-auto relative"> 112 <div 113 className={`transition-all duration-500 ease-in-out ${ 114 showForm 115 ? 'opacity-0 -translate-y-5 pointer-events-none' 116 : 'opacity-100 translate-y-0' 117 }`} 118 > 119 <Button 120 size="lg" 121 className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full" 122 onClick={() => setShowForm(true)} 123 > 124 Log in with AT Proto 125 <ArrowRight className="ml-2 w-5 h-5" /> 126 </Button> 127 </div> 128 129 <div 130 className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 131 showForm 132 ? 'opacity-100 translate-y-0' 133 : 'opacity-0 translate-y-5 pointer-events-none' 134 }`} 135 > 136 <form 137 onSubmit={async (e) => { 138 e.preventDefault() 139 try { 140 const handle = 141 inputRef.current?.value 142 const res = await fetch( 143 '/api/auth/signin', 144 { 145 method: 'POST', 146 headers: { 147 'Content-Type': 148 'application/json' 149 }, 150 body: JSON.stringify({ 151 handle 152 }) 153 } 154 ) 155 if (!res.ok) 156 throw new Error( 157 'Request failed' 158 ) 159 const data = await res.json() 160 if (data.url) { 161 window.location.href = data.url 162 } else { 163 alert('Unexpected response') 164 } 165 } catch (error) { 166 console.error( 167 'Login failed:', 168 error 169 ) 170 alert('Authentication failed') 171 } 172 }} 173 className="space-y-3" 174 > 175 <input 176 ref={inputRef} 177 type="text" 178 name="handle" 179 placeholder="Enter your handle (e.g., alice.bsky.social)" 180 className="w-full py-4 px-4 text-lg bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent" 181 /> 182 <button 183 type="submit" 184 className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 185 > 186 Continue 187 <ArrowRight className="ml-2 w-5 h-5" /> 188 </button> 189 </form> 190 </div> 191 </div> 192 </div> 193 </section> 194 195 {/* How It Works */} 196 <section className="container mx-auto px-4 py-16 bg-muted/30"> 197 <div className="max-w-3xl mx-auto text-center"> 198 <h2 className="text-3xl md:text-4xl font-bold mb-8"> 199 How it works 200 </h2> 201 <div className="space-y-6 text-left"> 202 <div className="flex gap-4 items-start"> 203 <div className="text-4xl font-bold text-accent/40 min-w-[60px]"> 204 01 205 </div> 206 <div> 207 <h3 className="text-xl font-semibold mb-2"> 208 Upload your static site 209 </h3> 210 <p className="text-muted-foreground"> 211 Your HTML, CSS, and JavaScript files are 212 stored in your AT Protocol account as 213 gzipped blobs and a manifest record. 214 </p> 215 </div> 216 </div> 217 <div className="flex gap-4 items-start"> 218 <div className="text-4xl font-bold text-accent/40 min-w-[60px]"> 219 02 220 </div> 221 <div> 222 <h3 className="text-xl font-semibold mb-2"> 223 We serve it globally 224 </h3> 225 <p className="text-muted-foreground"> 226 Wisp.place reads your site from your 227 account and delivers it through our CDN 228 for fast loading anywhere. 229 </p> 230 </div> 231 </div> 232 <div className="flex gap-4 items-start"> 233 <div className="text-4xl font-bold text-accent/40 min-w-[60px]"> 234 03 235 </div> 236 <div> 237 <h3 className="text-xl font-semibold mb-2"> 238 You stay in control 239 </h3> 240 <p className="text-muted-foreground"> 241 Update or remove your site anytime 242 through your AT Protocol account. No 243 lock-in, no middleman ownership. 244 </p> 245 </div> 246 </div> 247 </div> 248 </div> 249 </section> 250 251 {/* Features Grid */} 252 <section id="features" className="container mx-auto px-4 py-20"> 253 <div className="text-center mb-16"> 254 <h2 className="text-4xl md:text-5xl font-bold mb-4 text-balance"> 255 Why Wisp.place? 256 </h2> 257 <p className="text-xl text-muted-foreground text-balance max-w-2xl mx-auto"> 258 Static site hosting that respects your ownership 259 </p> 260 </div> 261 262 <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"> 263 {[ 264 { 265 icon: Shield, 266 title: 'You Own Your Content', 267 description: 268 'Your site lives in your AT Protocol account. Move it to another service anytime, or take it offline yourself.' 269 }, 270 { 271 icon: Zap, 272 title: 'CDN Performance', 273 description: 274 'We cache and serve your site from edge locations worldwide for fast load times.' 275 }, 276 { 277 icon: Lock, 278 title: 'No Vendor Lock-in', 279 description: 280 'Your data stays in your account. Switch providers or self-host whenever you want.' 281 }, 282 { 283 icon: Code, 284 title: 'Simple Deployment', 285 description: 286 'Upload your static files and we handle the rest. No complex configuration needed.' 287 }, 288 { 289 icon: Server, 290 title: 'AT Protocol Native', 291 description: 292 'Built for the decentralized web. Your site has a verifiable identity on the network.' 293 }, 294 { 295 icon: Globe, 296 title: 'Custom Domains', 297 description: 298 'Use your own domain name or a wisp.place subdomain. Your choice, either way.' 299 } 300 ].map((feature, i) => ( 301 <Card 302 key={i} 303 className="p-6 hover:shadow-lg transition-shadow border-2 bg-card" 304 > 305 <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4"> 306 <feature.icon className="w-6 h-6 text-accent" /> 307 </div> 308 <h3 className="text-xl font-semibold mb-2 text-card-foreground"> 309 {feature.title} 310 </h3> 311 <p className="text-muted-foreground leading-relaxed"> 312 {feature.description} 313 </p> 314 </Card> 315 ))} 316 </div> 317 </section> 318 319 {/* CTA Section */} 320 <section className="container mx-auto px-4 py-20"> 321 <div className="max-w-3xl mx-auto text-center bg-accent/5 border border-accent/20 rounded-2xl p-12"> 322 <h2 className="text-3xl md:text-4xl font-bold mb-4"> 323 Ready to deploy? 324 </h2> 325 <p className="text-xl text-muted-foreground mb-8"> 326 Host your static site on your own AT Protocol 327 account today 328 </p> 329 <Button 330 size="lg" 331 className="bg-accent text-accent-foreground hover:bg-accent/90 text-lg px-8 py-6" 332 onClick={() => setShowForm(true)} 333 > 334 Get Started 335 <ArrowRight className="ml-2 w-5 h-5" /> 336 </Button> 337 </div> 338 </section> 339 340 {/* Footer */} 341 <footer className="border-t border-border/40 bg-muted/20"> 342 <div className="container mx-auto px-4 py-8"> 343 <div className="text-center text-sm text-muted-foreground"> 344 <p> 345 Built by{' '} 346 <a 347 href="https://bsky.app/profile/nekomimi.pet" 348 target="_blank" 349 rel="noopener noreferrer" 350 className="text-accent hover:text-accent/80 transition-colors font-medium" 351 > 352 @nekomimi.pet 353 </a> 354 </p> 355 </div> 356 </div> 357 </footer> 358 </div> 359 </> 360 ) 361} 362 363const root = createRoot(document.getElementById('elysia')!) 364root.render( 365 <Layout className="gap-6"> 366 <App /> 367 </Layout> 368)