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)