Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

refactor login page

+192 -176
-3
web/src/components/RightSidebar.jsx
··· 124 124 <Link to="/url" className="right-link"> 125 125 Browse by URL 126 126 </Link> 127 - <Link to="/highlights" className="right-link"> 128 - Public Highlights 129 - </Link> 130 127 </nav> 131 128 </div> 132 129 )}
+116 -125
web/src/css/login.css
··· 3 3 flex-direction: column; 4 4 align-items: center; 5 5 justify-content: center; 6 - min-height: 70vh; 7 - padding: 60px 20px; 6 + min-height: 80vh; 7 + padding: 40px 20px; 8 8 width: 100%; 9 - max-width: 500px; 10 - margin: 0 auto; 11 9 } 12 10 13 - @media (max-width: 600px) { 14 - .login-page { 15 - padding: 40px 16px; 16 - } 11 + .login-header-group { 12 + display: flex; 13 + flex-direction: row; 14 + align-items: center; 15 + justify-content: center; 16 + gap: 24px; 17 + margin-bottom: 48px; 18 + width: auto; 19 + } 17 20 18 - .login-at-logo { 19 - font-size: 4rem; 20 - } 21 - 22 - .login-brand-name { 23 - font-size: 1.25rem; 24 - } 25 - 26 - .login-brand-icon { 27 - width: 40px; 28 - height: 40px; 29 - font-size: 1.5rem; 30 - } 21 + .login-logo-img { 22 + width: 60px; 23 + height: 60px; 24 + object-fit: contain; 25 + display: block; 31 26 } 32 27 33 - .login-at-logo { 34 - font-size: 5rem; 35 - font-weight: 800; 36 - color: var(--accent); 37 - margin-bottom: 24px; 28 + .login-x { 29 + font-size: 2rem; 30 + color: var(--text-tertiary); 31 + font-weight: 300; 38 32 line-height: 1; 33 + padding-bottom: 4px; 39 34 } 40 35 41 - .login-logo-img { 42 - width: 80px; 43 - height: 80px; 44 - margin-bottom: 24px; 45 - object-fit: contain; 36 + .login-atproto-icon { 37 + color: #3b83f6 !important; 38 + display: flex; 39 + align-items: center; 40 + justify-content: center; 46 41 } 47 42 48 43 .login-heading { 49 44 font-size: 1.5rem; 50 - font-weight: 600; 45 + font-weight: 700; 51 46 margin-bottom: 32px; 52 47 display: flex; 53 48 align-items: center; 54 - gap: 10px; 49 + justify-content: center; 50 + gap: 8px; 55 51 text-align: center; 56 - line-height: 1.4; 52 + line-height: 1.3; 53 + color: var(--text-primary); 57 54 } 58 55 59 56 .login-help-btn { ··· 73 70 } 74 71 75 72 .login-help-text { 76 - background: var(--bg-elevated); 73 + background: var(--bg-tertiary); 77 74 border: 1px solid var(--border); 78 75 border-radius: var(--radius-md); 79 - padding: 16px 20px; 76 + padding: 16px; 80 77 margin-bottom: 24px; 81 - font-size: 0.95rem; 78 + font-size: 0.9rem; 82 79 color: var(--text-secondary); 83 - line-height: 1.6; 80 + line-height: 1.5; 84 81 text-align: center; 82 + width: 100%; 85 83 } 86 84 87 85 .login-help-text code { 88 - background: var(--bg-tertiary); 89 - padding: 2px 8px; 86 + background: rgba(255, 255, 255, 0.05); 87 + padding: 2px 6px; 90 88 border-radius: var(--radius-sm); 91 - font-size: 0.9rem; 89 + font-size: 0.85rem; 90 + font-family: var(--font-mono); 92 91 } 93 92 94 93 .login-form { 95 94 display: flex; 96 95 flex-direction: column; 97 - gap: 16px; 96 + gap: 20px; 98 97 width: 100%; 99 98 } 100 99 ··· 105 104 .login-input { 106 105 width: 100%; 107 106 padding: 14px 16px; 108 - background: var(--bg-elevated); 107 + background: var(--bg-secondary); 109 108 border: 1px solid var(--border); 110 109 border-radius: var(--radius-md); 111 110 color: var(--text-primary); 112 111 font-size: 1rem; 113 - transition: 114 - border-color 0.15s, 115 - box-shadow 0.15s; 112 + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 113 + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 116 114 } 117 115 118 116 .login-input:focus { 119 117 outline: none; 120 118 border-color: var(--accent); 121 - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); 119 + box-shadow: 0 0 0 4px var(--accent-subtle); 120 + background: var(--bg-primary); 122 121 } 123 122 124 123 .login-input::placeholder { ··· 127 126 128 127 .login-suggestions { 129 128 position: absolute; 130 - top: calc(100% + 4px); 129 + top: calc(100% + 8px); 131 130 left: 0; 132 131 right: 0; 133 - background: var(--bg-card); 132 + background: var(--bg-elevated); 134 133 border: 1px solid var(--border); 135 134 border-radius: var(--radius-md); 136 135 box-shadow: var(--shadow-lg); 137 136 overflow: hidden; 138 137 z-index: 100; 138 + max-height: 300px; 139 + overflow-y: auto; 139 140 } 140 141 141 142 .login-suggestion { ··· 149 150 cursor: pointer; 150 151 text-align: left; 151 152 transition: background 0.1s; 153 + border-bottom: 1px solid var(--border); 154 + } 155 + 156 + .login-suggestion:last-child { 157 + border-bottom: none; 152 158 } 153 159 154 160 .login-suggestion:hover, 155 161 .login-suggestion.selected { 156 - background: var(--bg-elevated); 162 + background: var(--bg-tertiary); 157 163 } 158 164 159 165 .login-suggestion-avatar { 160 - width: 40px; 161 - height: 40px; 166 + width: 36px; 167 + height: 36px; 162 168 border-radius: var(--radius-full); 163 169 background: linear-gradient(135deg, var(--accent), #a855f7); 164 170 display: flex; ··· 166 172 justify-content: center; 167 173 flex-shrink: 0; 168 174 overflow: hidden; 169 - font-size: 0.875rem; 175 + font-size: 0.8rem; 170 176 font-weight: 600; 171 177 color: white; 172 178 } ··· 181 187 display: flex; 182 188 flex-direction: column; 183 189 min-width: 0; 190 + gap: 2px; 184 191 } 185 192 186 193 .login-suggestion-name { 187 194 font-weight: 600; 195 + font-size: 0.95rem; 188 196 color: var(--text-primary); 189 197 white-space: nowrap; 190 198 overflow: hidden; ··· 192 200 } 193 201 194 202 .login-suggestion-handle { 195 - font-size: 0.875rem; 203 + font-size: 0.85rem; 196 204 color: var(--text-secondary); 197 205 white-space: nowrap; 198 206 overflow: hidden; ··· 202 210 .login-error { 203 211 padding: 12px 16px; 204 212 background: rgba(239, 68, 68, 0.1); 205 - border: 1px solid rgba(239, 68, 68, 0.3); 213 + border: 1px solid rgba(239, 68, 68, 0.2); 206 214 border-radius: var(--radius-md); 207 - color: #ef4444; 215 + color: var(--error); 208 216 font-size: 0.875rem; 217 + text-align: center; 209 218 } 210 219 211 - .login-legal { 212 - font-size: 0.75rem; 213 - color: var(--text-tertiary); 214 - line-height: 1.5; 215 - margin-top: 16px; 216 - } 217 - 218 - .login-brand { 219 - display: flex; 220 - align-items: center; 221 - justify-content: center; 222 - gap: 12px; 223 - margin-bottom: 24px; 224 - } 225 - 226 - .login-brand-icon { 227 - width: 48px; 228 - height: 48px; 229 - background: linear-gradient(135deg, var(--accent), #a855f7); 230 - border-radius: var(--radius-lg); 231 - display: flex; 232 - align-items: center; 233 - justify-content: center; 234 - font-size: 1.75rem; 235 - font-weight: 800; 236 - color: white; 237 - } 238 - 239 - .login-brand-name { 240 - font-size: 1.75rem; 241 - font-weight: 700; 242 - } 243 - 244 - .login-avatar { 245 - width: 72px; 246 - height: 72px; 247 - border-radius: var(--radius-full); 248 - background: linear-gradient(135deg, var(--accent), #a855f7); 249 - display: flex; 250 - align-items: center; 251 - justify-content: center; 252 - margin: 0 auto 16px; 253 - font-weight: 700; 254 - font-size: 1.5rem; 255 - color: white; 256 - overflow: hidden; 257 - } 258 - 259 - .login-avatar img { 220 + .login-submit { 221 + padding: 14px 24px; 222 + font-size: 1rem; 223 + font-weight: 600; 260 224 width: 100%; 261 - height: 100%; 262 - object-fit: cover; 225 + justify-content: center; 263 226 } 264 227 265 228 .login-avatar-large { 266 - width: 100px; 267 - height: 100px; 229 + width: 80px; 230 + height: 80px; 268 231 border-radius: var(--radius-full); 269 232 background: linear-gradient(135deg, var(--accent), #a855f7); 270 233 display: flex; ··· 275 238 font-size: 2rem; 276 239 color: white; 277 240 overflow: hidden; 241 + box-shadow: var(--shadow-md); 278 242 } 279 243 280 244 .login-avatar-large img { ··· 284 248 } 285 249 286 250 .login-welcome { 287 - font-size: 1.5rem; 251 + font-size: 1.25rem; 288 252 font-weight: 600; 289 253 margin-bottom: 32px; 290 254 text-align: center; 291 - } 292 - 293 - .login-welcome-name { 294 - font-size: 1.25rem; 295 - font-weight: 600; 296 - margin-bottom: 24px; 255 + color: var(--text-primary); 297 256 } 298 257 299 258 .login-actions { ··· 303 262 width: 100%; 304 263 } 305 264 306 - .login-btn { 307 - width: 100%; 308 - padding: 14px 24px; 309 - font-size: 1rem; 310 - font-weight: 600; 265 + .morph-container { 266 + display: inline-block; 267 + color: var(--text-primary); 268 + font-weight: 700; 269 + transition: 270 + opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), 271 + transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), 272 + filter 0.4s cubic-bezier(0.4, 0, 0.2, 1); 273 + white-space: nowrap; 274 + vertical-align: bottom; 311 275 } 312 276 313 - .login-submit { 314 - padding: 18px 32px; 315 - font-size: 1.1rem; 316 - font-weight: 600; 277 + .morph-out { 278 + opacity: 0; 279 + transform: translateY(8px) scale(0.95); 280 + filter: blur(4px); 281 + } 282 + 283 + .morph-in { 284 + opacity: 1; 285 + transform: translateY(0) scale(1); 286 + filter: blur(0); 287 + } 288 + 289 + .login-legal { 290 + margin-top: 24px; 291 + font-size: 0.85rem; 292 + color: var(--text-tertiary); 293 + text-align: center; 294 + line-height: 1.5; 295 + } 296 + 297 + .login-legal a { 298 + color: var(--accent); 299 + text-decoration: underline; 300 + text-decoration-color: var(--accent); 301 + text-underline-offset: 4px; 302 + font-weight: 500; 303 + } 304 + 305 + .login-legal a:hover { 306 + text-decoration-thickness: 2px; 307 + opacity: 0.8; 317 308 }
+76 -48
web/src/pages/Login.jsx
··· 2 2 import { Link } from "react-router-dom"; 3 3 import { useAuth } from "../context/AuthContext"; 4 4 import { searchActors, startLogin } from "../api/client"; 5 - import { HelpCircle } from "lucide-react"; 5 + import { AtSign } from "lucide-react"; 6 6 import logo from "../assets/logo.svg"; 7 7 8 8 export default function Login() { ··· 12 12 const [showInviteInput, setShowInviteInput] = useState(false); 13 13 const [suggestions, setSuggestions] = useState([]); 14 14 const [showSuggestions, setShowSuggestions] = useState(false); 15 - const [showHelp, setShowHelp] = useState(false); 16 15 const [loading, setLoading] = useState(false); 17 16 const [error, setError] = useState(null); 18 17 const [selectedIndex, setSelectedIndex] = useState(-1); 19 18 const inputRef = useRef(null); 20 19 const inviteRef = useRef(null); 21 20 const suggestionsRef = useRef(null); 21 + 22 + const [providerIndex, setProviderIndex] = useState(0); 23 + const [morphClass, setMorphClass] = useState("morph-in"); 24 + const providers = [ 25 + "AT Protocol", 26 + "Bluesky", 27 + "Blacksky", 28 + "Tangled", 29 + "selfhosted.social", 30 + "Northsky", 31 + "witchcraft.systems", 32 + "topphie.social", 33 + "altq.net", 34 + ]; 35 + 36 + useEffect(() => { 37 + const cycleText = () => { 38 + setMorphClass("morph-out"); 39 + 40 + setTimeout(() => { 41 + setProviderIndex((prev) => (prev + 1) % providers.length); 42 + setMorphClass("morph-in"); 43 + }, 400); 44 + }; 45 + 46 + const interval = setInterval(cycleText, 3000); 47 + return () => clearInterval(interval); 48 + }, [providers.length]); 22 49 23 50 const isSelectionRef = useRef(false); 24 51 ··· 58 85 return () => document.removeEventListener("mousedown", handleClickOutside); 59 86 }, []); 60 87 88 + if (isAuthenticated) { 89 + return ( 90 + <div className="login-page"> 91 + <div className="login-avatar-large"> 92 + {user?.avatar ? ( 93 + <img src={user.avatar} alt={user.displayName || user.handle} /> 94 + ) : ( 95 + <span> 96 + {(user?.displayName || user?.handle || "??") 97 + .substring(0, 2) 98 + .toUpperCase()} 99 + </span> 100 + )} 101 + </div> 102 + <h1 className="login-welcome"> 103 + Welcome back, {user?.displayName || user?.handle} 104 + </h1> 105 + <div className="login-actions"> 106 + <Link to={`/profile/${user?.did}`} className="btn btn-primary"> 107 + View Profile 108 + </Link> 109 + <button onClick={logout} className="btn btn-ghost"> 110 + Sign out 111 + </button> 112 + </div> 113 + </div> 114 + ); 115 + } 116 + 61 117 const handleKeyDown = (e) => { 62 118 if (!showSuggestions || suggestions.length === 0) return; 63 119 ··· 113 169 } 114 170 }; 115 171 116 - if (isAuthenticated) { 117 - return ( 118 - <div className="login-page"> 119 - <div className="login-avatar-large"> 120 - {user?.avatar ? ( 121 - <img src={user.avatar} alt={user.displayName || user.handle} /> 122 - ) : ( 123 - <span> 124 - {(user?.displayName || user?.handle || "??") 125 - .substring(0, 2) 126 - .toUpperCase()} 127 - </span> 128 - )} 129 - </div> 130 - <h1 className="login-welcome"> 131 - Welcome back, {user?.displayName || user?.handle} 132 - </h1> 133 - <div className="login-actions"> 134 - <Link to={`/profile/${user?.did}`} className="btn btn-primary"> 135 - View Profile 136 - </Link> 137 - <button onClick={logout} className="btn btn-ghost"> 138 - Sign out 139 - </button> 140 - </div> 141 - </div> 142 - ); 143 - } 144 - 145 172 return ( 146 173 <div className="login-page"> 147 - <img src={logo} alt="Margin Logo" className="login-logo-img" /> 174 + <div className="login-header-group"> 175 + <img src={logo} alt="Margin Logo" className="login-logo-img" /> 176 + <span className="login-x">X</span> 177 + <div className="login-atproto-icon"> 178 + <AtSign size={64} strokeWidth={2.4} /> 179 + </div> 180 + </div> 148 181 149 182 <h1 className="login-heading"> 150 - Use the AT Protocol to login to Margin 151 - <button 152 - className="login-help-btn" 153 - onClick={() => setShowHelp(!showHelp)} 154 - type="button" 155 - > 156 - <HelpCircle size={20} /> 157 - </button> 183 + Sign in with your{" "} 184 + <span className={`morph-container ${morphClass}`}> 185 + {providers[providerIndex]} 186 + </span>{" "} 187 + handle 158 188 </h1> 159 - 160 - {showHelp && ( 161 - <p className="login-help-text"> 162 - The AT Protocol is an open, decentralized network for social apps. 163 - Your handle looks like <code>name.bsky.social</code> or your own 164 - domain. 165 - </p> 166 - )} 167 189 168 190 <form onSubmit={handleSubmit} className="login-form"> 169 191 <div className="login-input-wrapper"> ··· 263 285 ? "Submit Code" 264 286 : "Continue"} 265 287 </button> 288 + 289 + <p className="login-legal"> 290 + By signing in, you agree to our{" "} 291 + <Link to="/terms">Terms of Service</Link> and{" "} 292 + <Link to="/privacy">Privacy Policy</Link>. 293 + </p> 266 294 </form> 267 295 </div> 268 296 );