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 <>
29 <div className="min-h-screen">
30 {/* Header */}
31 <header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
32 <div className="container mx-auto px-4 py-4 flex items-center justify-between">
33 <div className="flex items-center gap-2">
34 <div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
35 <Globe className="w-5 h-5 text-primary-foreground" />
36 </div>
37 <span className="text-xl font-semibold text-foreground">
38 wisp.place
39 </span>
40 </div>
41 <div className="flex items-center gap-3">
42 <Button
43 variant="ghost"
44 size="sm"
45 onClick={() => setShowForm(true)}
46 >
47 Sign In
48 </Button>
49 <Button
50 size="sm"
51 className="bg-accent text-accent-foreground hover:bg-accent/90"
52 >
53 Get Started
54 </Button>
55 </div>
56 </div>
57 </header>
58
59 {/* Hero Section */}
60 <section className="container mx-auto px-4 py-20 md:py-32">
61 <div className="max-w-4xl mx-auto text-center">
62 <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8">
63 <span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span>
64 <span className="text-sm text-accent-foreground">
65 Built on AT Protocol
66 </span>
67 </div>
68
69 <h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight">
70 Your Website.Your Control. Lightning Fast.
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 Host static sites in your AT Protocol account. You
75 keep ownership and control. We just serve them fast
76 through our CDN.
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 =
109 inputRef.current?.value
110 const res = await fetch(
111 '/api/auth/signin',
112 {
113 method: 'POST',
114 headers: {
115 'Content-Type':
116 'application/json'
117 },
118 body: JSON.stringify({
119 handle
120 })
121 }
122 )
123 if (!res.ok)
124 throw new Error(
125 'Request failed'
126 )
127 const data = await res.json()
128 if (data.url) {
129 window.location.href = data.url
130 } else {
131 alert('Unexpected response')
132 }
133 } catch (error) {
134 console.error(
135 'Login failed:',
136 error
137 )
138 alert('Authentication failed')
139 }
140 }}
141 className="space-y-3"
142 >
143 <input
144 ref={inputRef}
145 type="text"
146 name="handle"
147 placeholder="Enter your handle (e.g., alice.bsky.social)"
148 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"
149 />
150 <button
151 type="submit"
152 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"
153 >
154 Continue
155 <ArrowRight className="ml-2 w-5 h-5" />
156 </button>
157 </form>
158 </div>
159 </div>
160 </div>
161 </section>
162
163 {/* How It Works */}
164 <section className="container mx-auto px-4 py-16 bg-muted/30">
165 <div className="max-w-3xl mx-auto text-center">
166 <h2 className="text-3xl md:text-4xl font-bold mb-8">
167 How it works
168 </h2>
169 <div className="space-y-6 text-left">
170 <div className="flex gap-4 items-start">
171 <div className="text-4xl font-bold text-accent/40 min-w-[60px]">
172 01
173 </div>
174 <div>
175 <h3 className="text-xl font-semibold mb-2">
176 Upload your static site
177 </h3>
178 <p className="text-muted-foreground">
179 Your HTML, CSS, and JavaScript files are
180 stored in your AT Protocol account as
181 gzipped blobs and a manifest record.
182 </p>
183 </div>
184 </div>
185 <div className="flex gap-4 items-start">
186 <div className="text-4xl font-bold text-accent/40 min-w-[60px]">
187 02
188 </div>
189 <div>
190 <h3 className="text-xl font-semibold mb-2">
191 We serve it globally
192 </h3>
193 <p className="text-muted-foreground">
194 Wisp.place reads your site from your
195 account and delivers it through our CDN
196 for fast loading anywhere.
197 </p>
198 </div>
199 </div>
200 <div className="flex gap-4 items-start">
201 <div className="text-4xl font-bold text-accent/40 min-w-[60px]">
202 03
203 </div>
204 <div>
205 <h3 className="text-xl font-semibold mb-2">
206 You stay in control
207 </h3>
208 <p className="text-muted-foreground">
209 Update or remove your site anytime
210 through your AT Protocol account. No
211 lock-in, no middleman ownership.
212 </p>
213 </div>
214 </div>
215 </div>
216 </div>
217 </section>
218
219 {/* Features Grid */}
220 <section id="features" className="container mx-auto px-4 py-20">
221 <div className="text-center mb-16">
222 <h2 className="text-4xl md:text-5xl font-bold mb-4 text-balance">
223 Why Wisp.place?
224 </h2>
225 <p className="text-xl text-muted-foreground text-balance max-w-2xl mx-auto">
226 Static site hosting that respects your ownership
227 </p>
228 </div>
229
230 <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
231 {[
232 {
233 icon: Shield,
234 title: 'You Own Your Content',
235 description:
236 'Your site lives in your AT Protocol account. Move it to another service anytime, or take it offline yourself.'
237 },
238 {
239 icon: Zap,
240 title: 'CDN Performance',
241 description:
242 'We cache and serve your site from edge locations worldwide for fast load times.'
243 },
244 {
245 icon: Lock,
246 title: 'No Vendor Lock-in',
247 description:
248 'Your data stays in your account. Switch providers or self-host whenever you want.'
249 },
250 {
251 icon: Code,
252 title: 'Simple Deployment',
253 description:
254 'Upload your static files and we handle the rest. No complex configuration needed.'
255 },
256 {
257 icon: Server,
258 title: 'AT Protocol Native',
259 description:
260 'Built for the decentralized web. Your site has a verifiable identity on the network.'
261 },
262 {
263 icon: Globe,
264 title: 'Custom Domains',
265 description:
266 'Use your own domain name or a wisp.place subdomain. Your choice, either way.'
267 }
268 ].map((feature, i) => (
269 <Card
270 key={i}
271 className="p-6 hover:shadow-lg transition-shadow border-2 bg-card"
272 >
273 <div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4">
274 <feature.icon className="w-6 h-6 text-accent" />
275 </div>
276 <h3 className="text-xl font-semibold mb-2 text-card-foreground">
277 {feature.title}
278 </h3>
279 <p className="text-muted-foreground leading-relaxed">
280 {feature.description}
281 </p>
282 </Card>
283 ))}
284 </div>
285 </section>
286
287 {/* CTA Section */}
288 <section className="container mx-auto px-4 py-20">
289 <div className="max-w-3xl mx-auto text-center bg-accent/5 border border-accent/20 rounded-2xl p-12">
290 <h2 className="text-3xl md:text-4xl font-bold mb-4">
291 Ready to deploy?
292 </h2>
293 <p className="text-xl text-muted-foreground mb-8">
294 Host your static site on your own AT Protocol
295 account today
296 </p>
297 <Button
298 size="lg"
299 className="bg-accent text-accent-foreground hover:bg-accent/90 text-lg px-8 py-6"
300 onClick={() => setShowForm(true)}
301 >
302 Get Started
303 <ArrowRight className="ml-2 w-5 h-5" />
304 </Button>
305 </div>
306 </section>
307
308 {/* Footer */}
309 <footer className="border-t border-border/40 bg-muted/20">
310 <div className="container mx-auto px-4 py-8">
311 <div className="text-center text-sm text-muted-foreground">
312 <p>
313 Built by{' '}
314 <a
315 href="https://bsky.app/profile/nekomimi.pet"
316 target="_blank"
317 rel="noopener noreferrer"
318 className="text-accent hover:text-accent/80 transition-colors font-medium"
319 >
320 @nekomimi.pet
321 </a>
322 </p>
323 </div>
324 </div>
325 </footer>
326 </div>
327 </>
328 )
329}
330
331const root = createRoot(document.getElementById('elysia')!)
332root.render(
333 <Layout className="gap-6">
334 <App />
335 </Layout>
336)