(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}