Monorepo for Wisp.place. A static site hosting service built on top of the AT Protocol.
1import { useState, useRef, useEffect } from 'react' 2import { createRoot } from 'react-dom/client' 3 4import Layout from '@public/layouts' 5 6function App() { 7 const [showForm, setShowForm] = useState(false) 8 const inputRef = useRef<HTMLInputElement>(null) 9 10 useEffect(() => { 11 if (showForm) { 12 setTimeout(() => inputRef.current?.focus(), 500) 13 } 14 }, [showForm]) 15 16 return ( 17 <> 18 <section id="header" className="py-24 px-6"> 19 <div className="text-center space-y-8"> 20 <div className="space-y-4"> 21 <h1 className="text-6xl md:text-8xl font-bold text-balance leading-tight"> 22 The complete platform to{' '} 23 <span className="gradient-text"> 24 publish the web. 25 </span> 26 </h1> 27 <p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto text-balance"> 28 Your decentralized toolkit to stop configuring and 29 start publishing. Securely build, deploy, and own 30 your web presence with AT Protocol. 31 </p> 32 </div> 33 34 <div className="inline-flex items-center gap-2 bg-accent/10 border border-accent/20 rounded-full px-4 py-2"> 35 <svg 36 xmlns="http://www.w3.org/2000/svg" 37 className="h-5 w-5 text-accent" 38 viewBox="0 0 20 20" 39 fill="currentColor" 40 > 41 <path 42 fillRule="evenodd" 43 d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.414-1.414L11 9.586V6z" 44 clipRule="evenodd" 45 /> 46 </svg> 47 <span className="text-sm font-medium text-accent"> 48 Publish once, own forever 49 </span> 50 </div> 51 52 <div className="max-w-md mx-auto space-y-4 mt-8"> 53 <div className="relative h-16"> 54 <div 55 className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 56 showForm 57 ? 'opacity-0 -translate-y-5 pointer-events-none' 58 : 'opacity-100 translate-y-0' 59 }`} 60 > 61 <button 62 onClick={() => setShowForm(true)} 63 className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors" 64 > 65 Log in with AT Proto 66 <svg 67 xmlns="http://www.w3.org/2000/svg" 68 className="ml-2 w-5 h-5" 69 viewBox="0 0 24 24" 70 fill="none" 71 stroke="currentColor" 72 strokeWidth="2" 73 strokeLinecap="round" 74 strokeLinejoin="round" 75 > 76 <path d="M5 12h14M12 5l7 7-7 7" /> 77 </svg> 78 </button> 79 </div> 80 81 <div 82 className={`transition-all duration-500 ease-in-out absolute inset-0 ${ 83 showForm 84 ? 'opacity-100 translate-y-0' 85 : 'opacity-0 translate-y-5 pointer-events-none' 86 }`} 87 > 88 <form 89 onSubmit={async (e) => { 90 e.preventDefault() 91 try { 92 const handle = 93 inputRef.current?.value 94 const res = await fetch( 95 '/api/auth/signin', 96 { 97 method: 'POST', 98 headers: { 99 'Content-Type': 100 'application/json' 101 }, 102 body: JSON.stringify({ 103 handle 104 }) 105 } 106 ) 107 if (!res.ok) 108 throw new Error( 109 'Request failed' 110 ) 111 const data = await res.json() 112 if (data.url) { 113 window.location.href = data.url 114 } else { 115 alert('Unexpected response') 116 } 117 } catch (error) { 118 console.error( 119 'Login failed:', 120 error 121 ) 122 alert('Authentication failed') 123 } 124 }} 125 className="space-y-3" 126 > 127 <input 128 ref={inputRef} 129 type="text" 130 name="handle" 131 placeholder="Enter your handle (e.g., alice.bsky.social)" 132 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" 133 /> 134 <button 135 type="submit" 136 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" 137 > 138 Continue 139 <svg 140 xmlns="http://www.w3.org/2000/svg" 141 className="ml-2 w-5 h-5" 142 viewBox="0 0 24 24" 143 fill="none" 144 stroke="currentColor" 145 strokeWidth="2" 146 strokeLinecap="round" 147 strokeLinejoin="round" 148 > 149 <path d="M5 12h14M12 5l7 7-7 7" /> 150 </svg> 151 </button> 152 </form> 153 </div> 154 </div> 155 </div> 156 </div> 157 </section> 158 </> 159 ) 160} 161 162const root = createRoot(document.getElementById('elysia')!) 163root.render( 164 <Layout className="gap-6"> 165 <App /> 166 </Layout> 167)