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

not found page, some improvements to login page and trending

+93 -55
+10 -16
backend/internal/db/tags.go
··· 11 11 var query string 12 12 if db.driver == "postgres" { 13 13 query = ` 14 - SELECT tag, SUM(cnt) as count FROM ( 15 - SELECT value as tag, COUNT(*) as cnt 14 + SELECT tag, COUNT(*) as count FROM ( 15 + SELECT value as tag, author_did 16 16 FROM annotations, json_array_elements_text(tags_json::json) as value 17 17 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 18 18 AND created_at > NOW() - INTERVAL '14 days' 19 - GROUP BY tag 20 19 UNION ALL 21 - SELECT value as tag, COUNT(*) as cnt 20 + SELECT value as tag, author_did 22 21 FROM highlights, json_array_elements_text(tags_json::json) as value 23 22 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 24 23 AND created_at > NOW() - INTERVAL '14 days' 25 - GROUP BY tag 26 24 UNION ALL 27 - SELECT value as tag, COUNT(*) as cnt 25 + SELECT value as tag, author_did 28 26 FROM bookmarks, json_array_elements_text(tags_json::json) as value 29 27 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 30 28 AND created_at > NOW() - INTERVAL '14 days' 31 - GROUP BY tag 32 29 ) combined 33 30 GROUP BY tag 34 - HAVING SUM(cnt) >= 2 31 + HAVING COUNT(DISTINCT author_did) >= 3 35 32 ORDER BY count DESC 36 33 LIMIT $1 37 34 ` 38 35 } else { 39 36 query = ` 40 - SELECT tag, SUM(cnt) as count FROM ( 41 - SELECT json_each.value as tag, COUNT(*) as cnt 37 + SELECT tag, COUNT(*) as count FROM ( 38 + SELECT json_each.value as tag, author_did 42 39 FROM annotations, json_each(annotations.tags_json) 43 40 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 44 41 AND created_at > datetime('now', '-14 days') 45 - GROUP BY tag 46 42 UNION ALL 47 - SELECT json_each.value as tag, COUNT(*) as cnt 43 + SELECT json_each.value as tag, author_did 48 44 FROM highlights, json_each(highlights.tags_json) 49 45 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 50 46 AND created_at > datetime('now', '-14 days') 51 - GROUP BY tag 52 47 UNION ALL 53 - SELECT json_each.value as tag, COUNT(*) as cnt 48 + SELECT json_each.value as tag, author_did 54 49 FROM bookmarks, json_each(bookmarks.tags_json) 55 50 WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]' 56 51 AND created_at > datetime('now', '-14 days') 57 - GROUP BY tag 58 52 ) combined 59 53 GROUP BY tag 60 - HAVING SUM(cnt) >= 2 54 + HAVING COUNT(DISTINCT author_did) >= 3 61 55 ORDER BY count DESC 62 56 LIMIT ? 63 57 `
+2 -1
web/src/App.tsx
··· 27 27 } from "./routes/wrappers"; 28 28 import About from "./views/About"; 29 29 import AdminModeration from "./views/core/AdminModeration"; 30 + import NotFound from "./views/NotFound"; 30 31 31 32 function UrlRedirect() { 32 33 const [searchParams] = useSearchParams(); ··· 241 242 } 242 243 /> 243 244 244 - <Route path="*" element={<Navigate to="/home" replace />} /> 245 + <Route path="*" element={<NotFound />} /> 245 246 </Routes> 246 247 </BrowserRouter> 247 248 );
-3
web/src/components/navigation/Sidebar.tsx
··· 87 87 className="px-3 hover:opacity-80 transition-opacity w-fit flex items-center gap-2.5" 88 88 > 89 89 <img src="/logo.svg" alt="Margin" className="w-8 h-8" /> 90 - <span className="font-display font-bold text-lg text-surface-900 dark:text-white tracking-tight hidden lg:inline"> 91 - Margin 92 - </span> 93 90 </Link> 94 91 95 92 <nav className="flex flex-col gap-0.5">
+35
web/src/views/NotFound.tsx
··· 1 + import React from "react"; 2 + import { Link } from "react-router-dom"; 3 + import { Home, AlertCircle } from "lucide-react"; 4 + import { useStore } from "@nanostores/react"; 5 + import { $theme } from "../store/theme"; 6 + 7 + export default function NotFound() { 8 + useStore($theme); 9 + 10 + return ( 11 + <div className="min-h-screen flex items-center justify-center bg-surface-100 dark:bg-surface-800 p-4"> 12 + <div className="w-full max-w-md bg-white dark:bg-surface-900 rounded-2xl border border-surface-200/60 dark:border-surface-800 p-8 shadow-sm dark:shadow-none text-center"> 13 + <div className="w-16 h-16 bg-surface-50 dark:bg-surface-800 rounded-2xl flex items-center justify-center mx-auto mb-6 text-surface-400 dark:text-surface-500"> 14 + <AlertCircle size={32} /> 15 + </div> 16 + 17 + <h1 className="text-3xl font-bold font-display text-surface-900 dark:text-white mb-3 tracking-tight"> 18 + Page not found 19 + </h1> 20 + 21 + <p className="text-surface-500 dark:text-surface-400 text-base mb-8 leading-relaxed max-w-xs mx-auto"> 22 + The page you are looking for doesn't exist or has been moved. 23 + </p> 24 + 25 + <Link 26 + to="/home" 27 + className="inline-flex items-center justify-center gap-2 px-6 py-3.5 w-full bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-xl font-bold text-sm transition-transform active:scale-[0.98] hover:bg-surface-800 dark:hover:bg-surface-50 shadow-lg shadow-surface-900/10 dark:shadow-none" 28 + > 29 + <Home size={18} /> 30 + Back to Home 31 + </Link> 32 + </div> 33 + </div> 34 + ); 35 + }
+46 -35
web/src/views/auth/Login.tsx
··· 1 1 import React, { useState, useEffect, useRef } from "react"; 2 2 import { Link, useSearchParams, Navigate } from "react-router-dom"; 3 3 import { AtSign } from "lucide-react"; 4 - import { BlueskyIcon, MarginIcon } from "../../components/common/Icons"; 5 4 import SignUpModal from "../../components/modals/SignUpModal"; 6 5 import { 7 6 searchActors, ··· 47 46 "altq.net", 48 47 ]; 49 48 49 + const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null); 50 + 50 51 useEffect(() => { 51 52 const cycleText = () => { 52 53 setMorphClass("opacity-0 translate-y-2 blur-sm"); ··· 70 71 if (!handle.includes(".")) { 71 72 const data = await searchActors(handle); 72 73 setSuggestions(data.actors || []); 74 + 75 + const exactMatch = data.actors?.find((s) => s.handle === handle); 76 + if (exactMatch) { 77 + setSelectedAvatar(exactMatch.avatar || null); 78 + } 79 + 73 80 setShowSuggestions(true); 74 81 setSelectedIndex(-1); 75 82 } ··· 116 123 const selectSuggestion = (actor: ActorSearchItem) => { 117 124 isSelectionRef.current = true; 118 125 setHandle(actor.handle); 126 + setSelectedAvatar(actor.avatar || null); 119 127 setSuggestions([]); 120 128 setShowSuggestions(false); 121 129 inputRef.current?.blur(); ··· 148 156 } 149 157 150 158 return ( 151 - <div className="min-h-screen flex items-center justify-center bg-surface-50 dark:bg-surface-950 p-4"> 152 - <div className="w-full max-w-[440px] flex flex-col items-center"> 153 - <div className="flex items-center justify-center gap-6 mb-12"> 154 - <MarginIcon size={60} /> 155 - <span className="text-3xl font-light text-surface-300 dark:text-surface-600 pb-1"> 156 - × 157 - </span> 158 - <div className="text-[#0285FF]"> 159 - <BlueskyIcon size={60} /> 160 - </div> 159 + <div className="min-h-screen flex items-center justify-center bg-surface-100 dark:bg-surface-800 p-4"> 160 + <div className="w-full max-w-[440px] bg-white dark:bg-surface-900 rounded-2xl border border-surface-200/60 dark:border-surface-800 p-8 shadow-sm dark:shadow-none"> 161 + <div className="flex flex-col items-center mb-8"> 162 + <h1 className="text-2xl font-bold font-display text-surface-900 dark:text-white text-center leading-snug"> 163 + Sign in with your <br /> 164 + <span 165 + className={`inline-block transition-all duration-400 ease-out text-transparent bg-clip-text bg-gradient-to-r from-[#027bff] to-[#0285FF] ${morphClass}`} 166 + > 167 + {providers[providerIndex]} 168 + </span>{" "} 169 + handle 170 + </h1> 161 171 </div> 162 172 163 - <h1 className="text-2xl font-bold font-display text-surface-900 dark:text-white mb-8 text-center leading-relaxed"> 164 - Sign in with your <br /> 165 - <span 166 - className={`inline-block transition-all duration-400 ease-out text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-indigo-600 ${morphClass}`} 167 - > 168 - {providers[providerIndex]} 169 - </span>{" "} 170 - handle 171 - </h1> 172 - 173 - <form onSubmit={handleSubmit} className="w-full flex flex-col gap-5"> 174 - <div className="relative"> 175 - <div className="absolute left-4 top-1/2 -translate-y-1/2 text-surface-400 dark:text-surface-500"> 176 - <AtSign size={20} className="stroke-[2.5]" /> 173 + <form onSubmit={handleSubmit} className="w-full flex flex-col gap-4"> 174 + <div className="relative group"> 175 + <div className="absolute left-4 top-1/2 -translate-y-1/2 text-surface-400 dark:text-surface-500 transition-colors pointer-events-none"> 176 + {selectedAvatar ? ( 177 + <Avatar 178 + src={selectedAvatar} 179 + size="xs" 180 + className="ring-2 ring-white dark:ring-surface-900 shadow-sm" 181 + /> 182 + ) : ( 183 + <AtSign 184 + size={20} 185 + className="stroke-[2.5] group-focus-within:text-[#027bff]" 186 + /> 187 + )} 177 188 </div> 178 189 <input 179 190 ref={inputRef} ··· 182 193 onChange={(e) => { 183 194 const val = e.target.value; 184 195 setHandle(val); 196 + if (selectedAvatar) setSelectedAvatar(null); 185 197 if (val.length < 3) { 186 198 setSuggestions([]); 187 199 setShowSuggestions(false); ··· 195 207 setShowSuggestions(true) 196 208 } 197 209 placeholder="handle.bsky.social" 198 - className="w-full pl-12 pr-4 py-3.5 bg-white dark:bg-surface-900 border border-surface-200 dark:border-surface-700 rounded-xl shadow-sm outline-none focus:border-primary-500 dark:focus:border-primary-400 focus:ring-4 focus:ring-primary-500/10 transition-all font-medium text-lg text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500" 210 + className="w-full pl-12 pr-4 py-3.5 bg-surface-50 dark:bg-surface-950 border border-surface-200 dark:border-surface-700 rounded-xl focus:border-[#027bff] dark:focus:border-[#027bff] outline-none focus:ring-4 focus:ring-[#027bff]/10 transition-all font-medium text-lg text-surface-900 dark:text-white placeholder:text-surface-400 dark:placeholder:text-surface-500" 199 211 autoCapitalize="none" 200 212 autoCorrect="off" 201 213 autoComplete="off" ··· 206 218 {showSuggestions && suggestions.length > 0 && ( 207 219 <div 208 220 ref={suggestionsRef} 209 - className="absolute top-[calc(100%+8px)] left-0 right-0 bg-white/90 dark:bg-surface-900/95 backdrop-blur-xl border border-surface-200 dark:border-surface-700 rounded-xl shadow-xl overflow-hidden z-50 animate-fade-in max-h-[300px] overflow-y-auto" 221 + className="absolute top-[calc(100%+8px)] left-0 right-0 bg-white dark:bg-surface-900 border border-surface-200 dark:border-surface-700 rounded-xl shadow-xl overflow-hidden z-50 animate-fade-in max-h-[300px] overflow-y-auto" 210 222 > 211 223 {suggestions.map((actor, index) => ( 212 224 <button ··· 231 243 </div> 232 244 233 245 {error && ( 234 - <div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-lg border border-red-100 dark:border-red-800 text-center font-medium"> 246 + <div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-lg border border-red-100 dark:border-red-800 text-center font-medium animate-fade-in"> 235 247 {error} 236 248 </div> 237 249 )} ··· 239 251 <button 240 252 type="submit" 241 253 disabled={loading || !handle} 242 - className="w-full py-3.5 bg-primary-600 dark:bg-primary-500 hover:bg-primary-700 dark:hover:bg-primary-400 text-white rounded-xl font-bold text-lg shadow-lg shadow-primary-600/20 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2" 254 + className="w-full py-3.5 bg-[#027bff] hover:bg-[#0269d9] active:scale-[0.98] text-white rounded-xl font-bold text-lg shadow-md shadow-[#027bff]/20 focus:outline-none focus:ring-4 focus:ring-[#027bff]/20 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2 mt-2" 243 255 > 244 256 {loading ? "Connecting..." : "Continue"} 245 257 </button> 246 258 247 - <p className="text-center text-sm text-surface-400 dark:text-surface-500 mt-2"> 259 + <p className="text-center text-sm text-surface-400 dark:text-surface-500 mt-2 leading-relaxed"> 248 260 By signing in, you agree to our{" "} 249 261 <Link 250 262 to="/terms" 251 - className="text-surface-900 dark:text-white hover:underline" 263 + className="text-surface-900 dark:text-white hover:underline font-medium hover:text-[#027bff] dark:hover:text-[#027bff] transition-colors" 252 264 > 253 265 Terms of Service 254 266 </Link>{" "} 255 267 and{" "} 256 268 <Link 257 269 to="/privacy" 258 - className="text-surface-900 dark:text-white hover:underline" 270 + className="text-surface-900 dark:text-white hover:underline font-medium hover:text-[#027bff] dark:hover:text-[#027bff] transition-colors" 259 271 > 260 272 Privacy Policy 261 273 </Link> 262 - . 263 274 </p> 264 275 265 - <div className="flex items-center gap-4 py-2"> 276 + <div className="flex items-center gap-4 py-2 opacity-50"> 266 277 <div className="h-px bg-surface-200 dark:bg-surface-700 flex-1" /> 267 278 <span className="text-xs font-bold text-surface-400 dark:text-surface-500 uppercase tracking-wider"> 268 279 or ··· 273 284 <button 274 285 type="button" 275 286 onClick={() => setShowSignUp(true)} 276 - className="w-full py-3.5 bg-transparent border-2 border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-500 hover:bg-surface-50 dark:hover:bg-surface-900 text-surface-700 dark:text-surface-300 rounded-xl font-bold transition-all" 287 + className="w-full py-3.5 bg-transparent border border-surface-200 dark:border-surface-700 hover:bg-surface-50 dark:hover:bg-surface-800 text-surface-600 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white rounded-xl font-bold transition-all text-sm" 277 288 > 278 289 Create New Account 279 290 </button>