Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at ui-refactor 256 lines 7.8 kB view raw
1import { useState, useEffect } from "react"; 2import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react"; 3import { BlackskyIcon, NorthskyIcon, BlueskyIcon, TopphieIcon } from "./Icons"; 4import { startSignup } from "../api/client"; 5import logo from "../assets/logo.svg"; 6 7const RECOMMENDED_PROVIDER = { 8 id: "margin", 9 name: "Margin", 10 service: "https://pds.margin.at", 11 Icon: null, 12 description: "Hosted by Margin, the easiest way to get started", 13 isMargin: true, 14}; 15 16const OTHER_PROVIDERS = [ 17 { 18 id: "bluesky", 19 name: "Bluesky", 20 service: "https://bsky.social", 21 Icon: BlueskyIcon, 22 description: "The main network", 23 }, 24 { 25 id: "blacksky", 26 name: "Blacksky", 27 service: "https://blacksky.app", 28 Icon: BlackskyIcon, 29 description: "For the Culture. A safe space for Black users and allies", 30 }, 31 { 32 id: "northsky", 33 name: "Northsky", 34 service: "https://northsky.social", 35 Icon: NorthskyIcon, 36 description: "A Canadian-based worker-owned cooperative", 37 }, 38 { 39 id: "topphie", 40 name: "Topphie", 41 service: "https://tophhie.social", 42 Icon: TopphieIcon, 43 description: "A welcoming and friendly community", 44 }, 45 { 46 id: "altq", 47 name: "AltQ", 48 service: "https://altq.net", 49 Icon: null, 50 description: "An independent, self-hosted PDS instance", 51 }, 52 { 53 id: "custom", 54 name: "Custom", 55 service: "", 56 custom: true, 57 Icon: null, 58 description: "Connect to your own or another custom PDS", 59 }, 60]; 61 62export default function SignUpModal({ onClose }) { 63 const [showOtherProviders, setShowOtherProviders] = useState(false); 64 const [showCustomInput, setShowCustomInput] = useState(false); 65 const [customService, setCustomService] = useState(""); 66 const [loading, setLoading] = useState(false); 67 const [error, setError] = useState(null); 68 69 useEffect(() => { 70 document.body.style.overflow = "hidden"; 71 return () => { 72 document.body.style.overflow = "unset"; 73 }; 74 }, []); 75 76 const handleProviderSelect = async (provider) => { 77 if (provider.custom) { 78 setShowCustomInput(true); 79 return; 80 } 81 82 setLoading(true); 83 setError(null); 84 85 try { 86 const result = await startSignup(provider.service); 87 if (result.authorizationUrl) { 88 window.location.href = result.authorizationUrl; 89 } 90 } catch (err) { 91 console.error(err); 92 setError("Could not connect to this provider. Please try again."); 93 setLoading(false); 94 } 95 }; 96 97 const handleCustomSubmit = async (e) => { 98 e.preventDefault(); 99 if (!customService.trim()) return; 100 101 setLoading(true); 102 setError(null); 103 104 let serviceUrl = customService.trim(); 105 if (!serviceUrl.startsWith("http")) { 106 serviceUrl = `https://${serviceUrl}`; 107 } 108 109 try { 110 const result = await startSignup(serviceUrl); 111 if (result.authorizationUrl) { 112 window.location.href = result.authorizationUrl; 113 } 114 } catch (err) { 115 console.error(err); 116 setError("Could not connect to this PDS. Please check the URL."); 117 setLoading(false); 118 } 119 }; 120 121 return ( 122 <div className="modal-overlay"> 123 <div className="modal-content signup-modal"> 124 <button className="modal-close" onClick={onClose}> 125 <X size={20} /> 126 </button> 127 128 {loading ? ( 129 <div className="signup-step" style={{ textAlign: "center" }}> 130 <Loader2 size={32} className="spinner" /> 131 <p style={{ marginTop: "1rem", color: "var(--text-secondary)" }}> 132 Connecting to provider... 133 </p> 134 </div> 135 ) : showCustomInput ? ( 136 <div className="signup-step"> 137 <h2>Custom Provider</h2> 138 <form onSubmit={handleCustomSubmit}> 139 <div className="form-group"> 140 <label>PDS address (e.g. pds.example.com)</label> 141 <input 142 type="text" 143 value={customService} 144 onChange={(e) => setCustomService(e.target.value)} 145 placeholder="pds.example.com" 146 autoFocus 147 /> 148 </div> 149 150 {error && ( 151 <div className="error-message"> 152 <AlertCircle size={16} /> 153 {error} 154 </div> 155 )} 156 157 <div className="modal-actions"> 158 <button 159 type="button" 160 className="btn-secondary" 161 onClick={() => { 162 setShowCustomInput(false); 163 setError(null); 164 }} 165 > 166 Back 167 </button> 168 <button 169 type="submit" 170 className="btn-primary" 171 disabled={!customService.trim()} 172 > 173 Continue 174 </button> 175 </div> 176 </form> 177 </div> 178 ) : ( 179 <div className="signup-step"> 180 <h2>Create your account</h2> 181 <p className="signup-subtitle"> 182 Margin uses the AT Protocol the same decentralized network that 183 powers Bluesky. Your account will be hosted on a server of your 184 choice. 185 </p> 186 187 {error && ( 188 <div className="error-message" style={{ marginBottom: "1rem" }}> 189 <AlertCircle size={16} /> 190 {error} 191 </div> 192 )} 193 194 <div className="signup-recommended"> 195 <div className="signup-recommended-badge">Recommended</div> 196 <button 197 className="provider-card provider-card-featured" 198 onClick={() => handleProviderSelect(RECOMMENDED_PROVIDER)} 199 > 200 <div className="provider-icon"> 201 <img 202 src={logo} 203 alt="Margin" 204 style={{ width: 24, height: 24 }} 205 /> 206 </div> 207 <div className="provider-info"> 208 <h3>{RECOMMENDED_PROVIDER.name}</h3> 209 <span>{RECOMMENDED_PROVIDER.description}</span> 210 </div> 211 <ChevronRight size={16} className="provider-arrow" /> 212 </button> 213 </div> 214 215 <button 216 type="button" 217 className="signup-toggle-others" 218 onClick={() => setShowOtherProviders(!showOtherProviders)} 219 > 220 {showOtherProviders ? "Hide other options" : "More options"} 221 <ChevronRight 222 size={14} 223 className={`toggle-chevron ${showOtherProviders ? "open" : ""}`} 224 /> 225 </button> 226 227 {showOtherProviders && ( 228 <div className="provider-grid"> 229 {OTHER_PROVIDERS.map((p) => ( 230 <button 231 key={p.id} 232 className="provider-card" 233 onClick={() => handleProviderSelect(p)} 234 > 235 <div className={`provider-icon ${p.wide ? "wide" : ""}`}> 236 {p.Icon ? ( 237 <p.Icon size={32} /> 238 ) : ( 239 <span className="provider-initial">{p.name[0]}</span> 240 )} 241 </div> 242 <div className="provider-info"> 243 <h3>{p.name}</h3> 244 <span>{p.description}</span> 245 </div> 246 <ChevronRight size={16} className="provider-arrow" /> 247 </button> 248 ))} 249 </div> 250 )} 251 </div> 252 )} 253 </div> 254 </div> 255 ); 256}