import React, { useState, useRef, useEffect } from 'react' import { createRoot } from 'react-dom/client' import { ArrowRight, Shield, Zap, Globe, Lock, Code, Server } from 'lucide-react' import Layout from '@public/layouts' import { Button } from '@public/components/ui/button' import { Card } from '@public/components/ui/card' import { BlueskyPostList, BlueskyProfile, BlueskyPost, AtProtoProvider, useLatestRecord, type AtProtoStyles, type FeedPostRecord } from 'atproto-ui' //Credit to https://tangled.org/@jakelazaroff.com/actor-typeahead interface Actor { handle: string avatar?: string displayName?: string } interface ActorTypeaheadProps { children: React.ReactElement> host?: string rows?: number onSelect?: (handle: string) => void autoSubmit?: boolean } const ActorTypeahead: React.FC = ({ children, host = 'https://public.api.bsky.app', rows = 5, onSelect, autoSubmit = false }) => { const [actors, setActors] = useState([]) const [index, setIndex] = useState(-1) const [pressed, setPressed] = useState(false) const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) const inputRef = useRef(null) const lastQueryRef = useRef('') const previousValueRef = useRef('') const preserveIndexRef = useRef(false) const handleInput = async (e: React.FormEvent) => { const query = e.currentTarget.value // Check if the value actually changed (filter out arrow key events) if (query === previousValueRef.current) { return } previousValueRef.current = query if (!query) { setActors([]) setIndex(-1) setIsOpen(false) lastQueryRef.current = '' return } // Store the query for this request const currentQuery = query lastQueryRef.current = currentQuery try { const url = new URL('xrpc/app.bsky.actor.searchActorsTypeahead', host) url.searchParams.set('q', query) url.searchParams.set('limit', `${rows}`) const res = await fetch(url) const json = await res.json() // Only update if this is still the latest query if (lastQueryRef.current === currentQuery) { setActors(json.actors || []) // Only reset index if we're not preserving it if (!preserveIndexRef.current) { setIndex(-1) } preserveIndexRef.current = false setIsOpen(true) } } catch (error) { console.error('Failed to fetch actors:', error) if (lastQueryRef.current === currentQuery) { setActors([]) setIsOpen(false) } } } const handleKeyDown = (e: React.KeyboardEvent) => { const navigationKeys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Enter', 'Escape'] // Mark that we should preserve the index for navigation keys if (navigationKeys.includes(e.key)) { preserveIndexRef.current = true } if (!isOpen || actors.length === 0) return switch (e.key) { case 'ArrowDown': e.preventDefault() setIndex((prev) => { const newIndex = prev < 0 ? 0 : Math.min(prev + 1, actors.length - 1) return newIndex }) break case 'PageDown': e.preventDefault() setIndex(actors.length - 1) break case 'ArrowUp': e.preventDefault() setIndex((prev) => { const newIndex = prev < 0 ? 0 : Math.max(prev - 1, 0) return newIndex }) break case 'PageUp': e.preventDefault() setIndex(0) break case 'Escape': e.preventDefault() setActors([]) setIndex(-1) setIsOpen(false) break case 'Enter': if (index >= 0 && index < actors.length) { e.preventDefault() selectActor(actors[index].handle) } break } } const selectActor = (handle: string) => { if (inputRef.current) { inputRef.current.value = handle } setActors([]) setIndex(-1) setIsOpen(false) onSelect?.(handle) // Auto-submit the form if enabled if (autoSubmit && inputRef.current) { const form = inputRef.current.closest('form') if (form) { // Use setTimeout to ensure the value is set before submission setTimeout(() => { form.requestSubmit() }, 0) } } } const handleFocusOut = (e: React.FocusEvent) => { if (pressed) return setActors([]) setIndex(-1) setIsOpen(false) } // Clone the input element and add our event handlers const input = React.cloneElement(children, { ref: (el: HTMLInputElement) => { inputRef.current = el // Preserve the original ref if it exists const originalRef = (children as any).ref if (typeof originalRef === 'function') { originalRef(el) } else if (originalRef) { originalRef.current = el } }, onInput: (e: React.FormEvent) => { handleInput(e) children.props.onInput?.(e) }, onKeyDown: (e: React.KeyboardEvent) => { handleKeyDown(e) children.props.onKeyDown?.(e) }, onBlur: (e: React.FocusEvent) => { handleFocusOut(e) children.props.onBlur?.(e) }, autoComplete: 'off' } as any) return (
{input} {isOpen && actors.length > 0 && (
    setPressed(true)} onMouseUp={() => { setPressed(false) inputRef.current?.focus() }} > {actors.map((actor, i) => (
  • ))}
)}
) } const LatestPostWithPrefetch: React.FC<{ did: string }> = ({ did }) => { const { record, rkey, loading } = useLatestRecord( did, 'app.bsky.feed.post' ) if (loading) return Loading… if (!record || !rkey) return No posts yet. return } function App() { const [showForm, setShowForm] = useState(false) const [checkingAuth, setCheckingAuth] = useState(true) const inputRef = useRef(null) useEffect(() => { // Check authentication status on mount const checkAuth = async () => { try { const response = await fetch('/api/auth/status', { credentials: 'include' }) const data = await response.json() if (data.authenticated) { // User is already authenticated, redirect to editor window.location.href = '/editor' return } // If not authenticated, clear any stale cookies document.cookie = 'did=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax' } catch (error) { console.error('Auth check failed:', error) // Clear cookies on error as well document.cookie = 'did=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax' } finally { setCheckingAuth(false) } } checkAuth() }, []) useEffect(() => { if (showForm) { setTimeout(() => inputRef.current?.focus(), 500) } }, [showForm]) if (checkingAuth) { return (
) } return ( <>
{/* Header */}
wisp.place wisp.place
{/* Hero Section */}
Built on AT Protocol

Your Website.Your Control. Lightning Fast.

Host static sites in your AT Protocol account. You keep ownership and control. We just serve them fast through our CDN.

{ e.preventDefault() try { const handle = inputRef.current?.value const res = await fetch( '/api/auth/signin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ handle }) } ) if (!res.ok) throw new Error( 'Request failed' ) const data = await res.json() if (data.url) { window.location.href = data.url } else { alert('Unexpected response') } } catch (error) { console.error( 'Login failed:', error ) // Clear any invalid cookies document.cookie = 'did=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax' alert('Authentication failed') } }} className="space-y-3" > { if (inputRef.current) { inputRef.current.value = handle } }} >
{/* How It Works */}

How it works

01

Upload your static site

Your HTML, CSS, and JavaScript files are stored in your AT Protocol account as gzipped blobs and a manifest record.

02

We serve it globally

Wisp.place reads your site from your account and delivers it through our CDN for fast loading anywhere.

03

You stay in control

Update or remove your site anytime through your AT Protocol account. No lock-in, no middleman ownership.

{/* Features Grid */}

Why Wisp.place?

Static site hosting that respects your ownership

{[ { icon: Shield, title: 'You Own Your Content', description: 'Your site lives in your AT Protocol account. Move it to another service anytime, or take it offline yourself.' }, { icon: Zap, title: 'CDN Performance', description: 'We cache and serve your site from edge locations worldwide for fast load times.' }, { icon: Lock, title: 'No Vendor Lock-in', description: 'Your data stays in your account. Switch providers or self-host whenever you want.' }, { icon: Code, title: 'Simple Deployment', description: 'Upload your static files and we handle the rest. No complex configuration needed.' }, { icon: Server, title: 'AT Protocol Native', description: 'Built for the decentralized web. Your site has a verifiable identity on the network.' }, { icon: Globe, title: 'Custom Domains', description: 'Use your own domain name or a wisp.place subdomain. Your choice, either way.' } ].map((feature, i) => (

{feature.title}

{feature.description}

))}
{/* CTA Section */}

Follow on Bluesky for updates

{/* Ready to Deploy CTA */}

Ready to deploy?

Host your static site on your own AT Protocol account today

{/* Footer */}
) } const root = createRoot(document.getElementById('elysia')!) root.render( )