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 inputRef = useRef<HTMLInputElement>(null) 20 21 useEffect(() => { 22 if (showForm) { 23 setTimeout(() => inputRef.current?.focus(), 500) 24 } 25 }, [showForm]) 26 27 return ( 28 <div className="min-h-screen"> 29 {/* Header */} 30 <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> 31 <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 32 <div className="flex items-center gap-2"> 33 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"> 34 <Globe className="w-5 h-5 text-primary-foreground" /> 35 </div> 36 <span className="text-xl font-semibold text-foreground"> 37 wisp.place 38 </span> 39 </div> 40 <div className="flex items-center gap-3"> 41 <Button 42 variant="ghost" 43 size="sm" 44 onClick={() => setShowForm(true)} 45 > 46 Sign In 47 </Button> 48 <Button 49 size="sm" 50 className="bg-accent text-accent-foreground hover:bg-accent/90" 51 > 52 Get Started 53 </Button> 54 </div> 55 </div> 56 </header> 57 58 {/* Hero Section */} 59 <section className="container mx-auto px-4 py-20 md:py-32"> 60 <div className="max-w-4xl mx-auto text-center"> 61 <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8"> 62 <span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span> 63 <span className="text-sm text-accent-foreground"> 64 Built on AT Protocol 65 </span> 66 </div> 67 68 <h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight"> 69 Host your sites on the{' '} 70 <span className="text-primary">decentralized</span> web 71 </h1> 72 73 <p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto"> 74 Deploy static sites to a truly open network. Your 75 content, your control, your identity. No platform 76 lock-in, ever. 77 </p> 78 79 <div className="max-w-md mx-auto relative"> 80 <div 81 className={`transition-all duration-500 ease-in-out ${ 82 showForm 83 ? 'opacity-0 -translate-y-5 pointer-events-none' 84 : 'opacity-100 translate-y-0' 85 }`} 86 > 87 <Button 88 size="lg" 89 className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full" 90 onClick={() => setShowForm(true)} 91 > 92 Log in with AT Proto 93 <ArrowRight className="ml-2 w-5 h-5" /> 94 </Button> 95 </div> 96 97 <div 98 className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 99 showForm 100 ? 'opacity-100 translate-y-0' 101 : 'opacity-0 translate-y-5 pointer-events-none' 102 }`} 103 > 104 <form 105 onSubmit={async (e) => { 106 e.preventDefault() 107 try { 108 const handle = inputRef.current?.value 109 const res = await fetch( 110 '/api/auth/signin', 111 { 112 method: 'POST', 113 headers: { 114 'Content-Type': 115 'application/json' 116 }, 117 body: JSON.stringify({ handle }) 118 } 119 ) 120 if (!res.ok) 121 throw new Error('Request failed') 122 const data = await res.json() 123 if (data.url) { 124 window.location.href = data.url 125 } else { 126 alert('Unexpected response') 127 } 128 } catch (error) { 129 console.error('Login failed:', error) 130 alert('Authentication failed') 131 } 132 }} 133 className="space-y-3" 134 > 135 <input 136 ref={inputRef} 137 type="text" 138 name="handle" 139 placeholder="Enter your handle (e.g., alice.bsky.social)" 140 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" 141 /> 142 <button 143 type="submit" 144 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" 145 > 146 Continue 147 <ArrowRight className="ml-2 w-5 h-5" /> 148 </button> 149 </form> 150 </div> 151 </div> 152 </div> 153 </section> 154 155 {/* Stats Section */} 156 <section className="container mx-auto px-4 py-16"> 157 <div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-5xl mx-auto"> 158 {[ 159 { value: '100%', label: 'Decentralized' }, 160 { value: '0ms', label: 'Cold Start' }, 161 { value: '∞', label: 'Scalability' }, 162 { value: 'You', label: 'Own Your Data' } 163 ].map((stat, i) => ( 164 <div key={i} className="text-center"> 165 <div className="text-4xl md:text-5xl font-bold text-primary mb-2"> 166 {stat.value} 167 </div> 168 <div className="text-sm text-muted-foreground"> 169 {stat.label} 170 </div> 171 </div> 172 ))} 173 </div> 174 </section> 175 176 {/* Features Grid */} 177 <section id="features" className="container mx-auto px-4 py-20"> 178 <div className="text-center mb-16"> 179 <h2 className="text-4xl md:text-5xl font-bold mb-4 text-balance"> 180 Built for the open web 181 </h2> 182 <p className="text-xl text-muted-foreground text-balance max-w-2xl mx-auto"> 183 Everything you need to deploy and manage static sites on 184 a decentralized network 185 </p> 186 </div> 187 188 <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto"> 189 {[ 190 { 191 icon: Shield, 192 title: 'True Ownership', 193 description: 194 'Your content lives on the AT Protocol network. No single company can take it down or lock you out.' 195 }, 196 { 197 icon: Zap, 198 title: 'Lightning Fast', 199 description: 200 'Distributed edge network ensures your sites load instantly from anywhere in the world.' 201 }, 202 { 203 icon: Lock, 204 title: 'Cryptographic Security', 205 description: 206 'Content-addressed storage and cryptographic verification ensure integrity and authenticity.' 207 }, 208 { 209 icon: Code, 210 title: 'Developer Friendly', 211 description: 212 'Simple CLI, Git integration, and familiar workflows. Deploy with a single command.' 213 }, 214 { 215 icon: Server, 216 title: 'Zero Vendor Lock-in', 217 description: 218 'Built on open protocols. Migrate your sites anywhere, anytime. Your data is portable.' 219 }, 220 { 221 icon: Globe, 222 title: 'Global Network', 223 description: 224 'Leverage the power of decentralized infrastructure for unmatched reliability and uptime.' 225 } 226 ].map((feature, i) => ( 227 <Card 228 key={i} 229 className="p-6 hover:shadow-lg transition-shadow border-2 bg-card" 230 > 231 <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4"> 232 <feature.icon className="w-6 h-6 text-accent" /> 233 </div> 234 <h3 className="text-xl font-semibold mb-2 text-card-foreground"> 235 {feature.title} 236 </h3> 237 <p className="text-muted-foreground leading-relaxed"> 238 {feature.description} 239 </p> 240 </Card> 241 ))} 242 </div> 243 </section> 244 245 {/* How It Works */} 246 <section 247 id="how-it-works" 248 className="container mx-auto px-4 py-20 bg-muted/30" 249 > 250 <div className="max-w-4xl mx-auto"> 251 <h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance"> 252 Deploy in three steps 253 </h2> 254 255 <div className="space-y-12"> 256 {[ 257 { 258 step: '01', 259 title: 'Upload your site', 260 description: 261 'Link your Git repository or upload a folder containing your static site directly.' 262 }, 263 { 264 step: '02', 265 title: 'Name and set domain', 266 description: 267 'Name your site and set domain routing to it. You can bring your own domain too.' 268 }, 269 { 270 step: '03', 271 title: 'Deploy to AT Protocol', 272 description: 273 'Your site is published to the decentralized network with a permanent, verifiable identity.' 274 } 275 ].map((step, i) => ( 276 <div key={i} className="flex gap-6 items-start"> 277 <div className="text-6xl font-bold text-accent/20 min-w-[80px]"> 278 {step.step} 279 </div> 280 <div className="flex-1 pt-2"> 281 <h3 className="text-2xl font-semibold mb-3"> 282 {step.title} 283 </h3> 284 <p className="text-lg text-muted-foreground leading-relaxed"> 285 {step.description} 286 </p> 287 </div> 288 </div> 289 ))} 290 </div> 291 </div> 292 </section> 293 294 {/* Footer */} 295 <footer className="border-t border-border/40 bg-muted/20"> 296 <div className="container mx-auto px-4 py-8"> 297 <div className="text-center text-sm text-muted-foreground"> 298 <p> 299 Built by{' '} 300 <a 301 href="https://bsky.app/profile/nekomimi.pet" 302 target="_blank" 303 rel="noopener noreferrer" 304 className="text-accent hover:text-accent/80 transition-colors font-medium" 305 > 306 @nekomimi.pet 307 </a> 308 </p> 309 </div> 310 </div> 311 </footer> 312 </div> 313 ) 314} 315 316const root = createRoot(document.getElementById('elysia')!) 317root.render( 318 <Layout className="gap-6"> 319 <App /> 320 </Layout> 321)