this repo has no description
0
fork

Configure Feed

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

Add login input and handle autocomplete suggestions

modamo-gh b67cebe9 ee75d5da

+316 -4
+22
app/api/search-handles/route.ts
··· 1 + import { NextRequest, NextResponse } from "next/server"; 2 + 3 + export const GET = async (request: NextRequest) => { 4 + const { searchParams } = new URL(request.url); 5 + const handle = searchParams.get("handle"); 6 + 7 + if (handle) { 8 + const url = new URL( 9 + "https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead" 10 + ); 11 + 12 + url.searchParams.set("q", handle); 13 + url.searchParams.set("limit", "3"); 14 + 15 + const response = await fetch(url); 16 + const json = await response.json(); 17 + 18 + return NextResponse.json({ actors: json.actors }); 19 + } 20 + 21 + return NextResponse.json({ actors: [] }); 22 + };
+119
app/login/components/HandleInput.tsx
··· 1 + "use client"; 2 + 3 + import Image from "next/image"; 4 + import { useEffect, useRef, useState } from "react"; 5 + import { AppBskyActorDefs } from "@atproto/api"; 6 + 7 + const HandleInput = () => { 8 + const isSelectingRef = useRef(false); 9 + 10 + const [handle, setHandle] = useState(""); 11 + const [handleSuggestions, setHandleSuggestions] = useState< 12 + AppBskyActorDefs.ProfileViewBasic[] 13 + >([]); 14 + 15 + useEffect(() => { 16 + if (isSelectingRef.current) { 17 + isSelectingRef.current = false; 18 + 19 + return; 20 + } 21 + 22 + if (handle.length < 3) { 23 + setHandleSuggestions([]); 24 + 25 + return; 26 + } 27 + 28 + const timer = setTimeout(async () => { 29 + const response = await fetch( 30 + `/api/search-handles?handle=${handle}` 31 + ); 32 + const data = await response.json(); 33 + 34 + setHandleSuggestions(data.actors || []); 35 + }, 400); 36 + 37 + return () => clearTimeout(timer); 38 + }, [handle]); 39 + 40 + return ( 41 + <div className="flex flex-col gap-2 w-1/4"> 42 + <label htmlFor="handle">Your ATProto Handle</label> 43 + <div> 44 + <input 45 + className={`border border-gray-600 focus:border-[#FF6A00] h-12 focus:outline-none p-2 ${ 46 + handleSuggestions.length ? "rounded-t-lg" : "rounded-lg" 47 + } w-full`} 48 + name="handle" 49 + placeholder="Enter your ATProto handle (e.g. example.blacksky.app)" 50 + onChange={(e) => setHandle(e.target.value)} 51 + type="text" 52 + value={handle} 53 + /> 54 + {handleSuggestions.length ? ( 55 + <div className="absolute bg-gray-800 border border-t-0 border-gray-600 hover:cursor-pointer flex flex-col gap-2 p-2 rounded-b-lg w-1/4"> 56 + {handleSuggestions.map((suggestion, index) => ( 57 + <div 58 + className="hover:bg-gray-700 flex gap-2 h-12 p-1 rounded-lg" 59 + key={index} 60 + onClick={() => { 61 + isSelectingRef.current = true; 62 + 63 + setHandleSuggestions([]); 64 + setHandle(suggestion.handle); 65 + }} 66 + > 67 + <Image 68 + alt={""} 69 + className="aspect-square rounded-full" 70 + height={40} 71 + src={suggestion.avatar} 72 + width={40} 73 + /> 74 + <div className="flex flex-col justify-center"> 75 + <p className="text-sm"> 76 + {suggestion.displayName} 77 + </p> 78 + <p className="text-xs text-gray-400"> 79 + {suggestion.handle} 80 + </p> 81 + </div> 82 + </div> 83 + ))} 84 + </div> 85 + ) : null} 86 + </div> 87 + <button 88 + className="bg-[#FF6A00] hover:bg-[#FF8840] hover:cursor-pointer h-12 rounded-lg" 89 + onClick={async () => { 90 + try { 91 + const response = await fetch("/oauth/login", { 92 + body: JSON.stringify({ handle }), 93 + headers: { 94 + "Content-Type": "application/json" 95 + }, 96 + method: "POST" 97 + }); 98 + const data = await response.json(); 99 + 100 + if (response.ok && data.redirectURL) { 101 + window.location.href = data.redirectURL; 102 + } else { 103 + console.error( 104 + "Login initiation failed:", 105 + data.error 106 + ); 107 + } 108 + } catch (error) { 109 + console.error("Error initiating ATProto OAuth:", error); 110 + } 111 + }} 112 + > 113 + Continue 114 + </button> 115 + </div> 116 + ); 117 + }; 118 + 119 + export default HandleInput;
+9 -3
app/login/page.tsx
··· 1 + import HandleInput from "./components/HandleInput"; 2 + 1 3 const Login = () => { 2 - return <></> 3 - } 4 + return ( 5 + <div className="bg-[#1C1A29] flex h-screen items-center justify-center"> 6 + <HandleInput /> 7 + </div> 8 + ); 9 + }; 4 10 5 - export default Login; 11 + export default Login;
+4 -1
next.config.ts
··· 2 2 3 3 const nextConfig: NextConfig = { 4 4 images: { 5 - remotePatterns: [new URL("https://openweathermap.org/img/**")] 5 + remotePatterns: [ 6 + new URL("https://openweathermap.org/img/**"), 7 + new URL("https://cdn.bsky.app/img/avatar/plain/**") 8 + ] 6 9 } 7 10 }; 8 11
+161
package-lock.json
··· 8 8 "name": "bmore_today", 9 9 "version": "0.1.0", 10 10 "dependencies": { 11 + "@atproto/api": "^0.19.0", 11 12 "luxon": "^3.7.2", 12 13 "next": "16.1.6", 13 14 "react": "19.2.3", ··· 39 40 "url": "https://github.com/sponsors/sindresorhus" 40 41 } 41 42 }, 43 + "node_modules/@atproto/api": { 44 + "version": "0.19.0", 45 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.19.0.tgz", 46 + "integrity": "sha512-7u/EGgkIj4bbslGer2RMQPtMWCPvREcpH0mVagaf5om+NcPzUIZeIacWKANVv95BdMJ7jlcHS7xrkEMPmg2dFw==", 47 + "license": "MIT", 48 + "dependencies": { 49 + "@atproto/common-web": "^0.4.17", 50 + "@atproto/lexicon": "^0.6.1", 51 + "@atproto/syntax": "^0.4.3", 52 + "@atproto/xrpc": "^0.7.7", 53 + "await-lock": "^2.2.2", 54 + "multiformats": "^9.9.0", 55 + "tlds": "^1.234.0", 56 + "zod": "^3.23.8" 57 + } 58 + }, 59 + "node_modules/@atproto/api/node_modules/zod": { 60 + "version": "3.25.76", 61 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 62 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 63 + "license": "MIT", 64 + "funding": { 65 + "url": "https://github.com/sponsors/colinhacks" 66 + } 67 + }, 68 + "node_modules/@atproto/common-web": { 69 + "version": "0.4.17", 70 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.17.tgz", 71 + "integrity": "sha512-sfxD8NGxyoxhxmM9EUshEFbWcJ3+JHEOZF4Quk6HsCh1UxpHBmLabT/vEsAkDWl+C/8U0ine0+c/gHyE/OZiQQ==", 72 + "license": "MIT", 73 + "dependencies": { 74 + "@atproto/lex-data": "^0.0.12", 75 + "@atproto/lex-json": "^0.0.12", 76 + "@atproto/syntax": "^0.4.3", 77 + "zod": "^3.23.8" 78 + } 79 + }, 80 + "node_modules/@atproto/common-web/node_modules/zod": { 81 + "version": "3.25.76", 82 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 83 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 84 + "license": "MIT", 85 + "funding": { 86 + "url": "https://github.com/sponsors/colinhacks" 87 + } 88 + }, 89 + "node_modules/@atproto/lex-data": { 90 + "version": "0.0.12", 91 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.12.tgz", 92 + "integrity": "sha512-aekJudcK1p6sbTqUv2bJMJBAGZaOJS0mgDclpK3U6VuBREK/au4B6ffunBFWgrDfg0Vwj2JGyEA7E51WZkJcRw==", 93 + "license": "MIT", 94 + "dependencies": { 95 + "multiformats": "^9.9.0", 96 + "tslib": "^2.8.1", 97 + "uint8arrays": "3.0.0", 98 + "unicode-segmenter": "^0.14.0" 99 + } 100 + }, 101 + "node_modules/@atproto/lex-json": { 102 + "version": "0.0.12", 103 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.12.tgz", 104 + "integrity": "sha512-XlEpnWWZdDJ5BIgG25GyH+6iBfyrFL18BI5JSE6rUfMObbFMrQRaCuRLQfryRXNysVz3L3U+Qb9y8KcXbE8AcA==", 105 + "license": "MIT", 106 + "dependencies": { 107 + "@atproto/lex-data": "^0.0.12", 108 + "tslib": "^2.8.1" 109 + } 110 + }, 111 + "node_modules/@atproto/lexicon": { 112 + "version": "0.6.1", 113 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", 114 + "integrity": "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw==", 115 + "license": "MIT", 116 + "dependencies": { 117 + "@atproto/common-web": "^0.4.13", 118 + "@atproto/syntax": "^0.4.3", 119 + "iso-datestring-validator": "^2.2.2", 120 + "multiformats": "^9.9.0", 121 + "zod": "^3.23.8" 122 + } 123 + }, 124 + "node_modules/@atproto/lexicon/node_modules/zod": { 125 + "version": "3.25.76", 126 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 127 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 128 + "license": "MIT", 129 + "funding": { 130 + "url": "https://github.com/sponsors/colinhacks" 131 + } 132 + }, 133 + "node_modules/@atproto/syntax": { 134 + "version": "0.4.3", 135 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 136 + "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 137 + "license": "MIT", 138 + "dependencies": { 139 + "tslib": "^2.8.1" 140 + } 141 + }, 142 + "node_modules/@atproto/xrpc": { 143 + "version": "0.7.7", 144 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", 145 + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", 146 + "license": "MIT", 147 + "dependencies": { 148 + "@atproto/lexicon": "^0.6.0", 149 + "zod": "^3.23.8" 150 + } 151 + }, 152 + "node_modules/@atproto/xrpc/node_modules/zod": { 153 + "version": "3.25.76", 154 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 155 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 156 + "license": "MIT", 157 + "funding": { 158 + "url": "https://github.com/sponsors/colinhacks" 159 + } 160 + }, 42 161 "node_modules/@babel/code-frame": { 43 162 "version": "7.29.0", 44 163 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", ··· 2415 2534 "url": "https://github.com/sponsors/ljharb" 2416 2535 } 2417 2536 }, 2537 + "node_modules/await-lock": { 2538 + "version": "2.2.2", 2539 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 2540 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 2541 + "license": "MIT" 2542 + }, 2418 2543 "node_modules/axe-core": { 2419 2544 "version": "4.11.1", 2420 2545 "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", ··· 4416 4541 "dev": true, 4417 4542 "license": "ISC" 4418 4543 }, 4544 + "node_modules/iso-datestring-validator": { 4545 + "version": "2.2.2", 4546 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 4547 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 4548 + "license": "MIT" 4549 + }, 4419 4550 "node_modules/iterator.prototype": { 4420 4551 "version": "1.1.5", 4421 4552 "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", ··· 4960 5091 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 4961 5092 "dev": true, 4962 5093 "license": "MIT" 5094 + }, 5095 + "node_modules/multiformats": { 5096 + "version": "9.9.0", 5097 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 5098 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 5099 + "license": "(Apache-2.0 AND MIT)" 4963 5100 }, 4964 5101 "node_modules/nanoid": { 4965 5102 "version": "3.3.11", ··· 6169 6306 "url": "https://github.com/sponsors/jonschlinkert" 6170 6307 } 6171 6308 }, 6309 + "node_modules/tlds": { 6310 + "version": "1.261.0", 6311 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", 6312 + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", 6313 + "license": "MIT", 6314 + "bin": { 6315 + "tlds": "bin.js" 6316 + } 6317 + }, 6172 6318 "node_modules/to-regex-range": { 6173 6319 "version": "5.0.1", 6174 6320 "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", ··· 6356 6502 "typescript": ">=4.8.4 <6.0.0" 6357 6503 } 6358 6504 }, 6505 + "node_modules/uint8arrays": { 6506 + "version": "3.0.0", 6507 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 6508 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 6509 + "license": "MIT", 6510 + "dependencies": { 6511 + "multiformats": "^9.4.2" 6512 + } 6513 + }, 6359 6514 "node_modules/unbox-primitive": { 6360 6515 "version": "1.1.0", 6361 6516 "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", ··· 6380 6535 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 6381 6536 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 6382 6537 "dev": true, 6538 + "license": "MIT" 6539 + }, 6540 + "node_modules/unicode-segmenter": { 6541 + "version": "0.14.5", 6542 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 6543 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 6383 6544 "license": "MIT" 6384 6545 }, 6385 6546 "node_modules/unrs-resolver": {
+1
package.json
··· 9 9 "lint": "eslint" 10 10 }, 11 11 "dependencies": { 12 + "@atproto/api": "^0.19.0", 12 13 "luxon": "^3.7.2", 13 14 "next": "16.1.6", 14 15 "react": "19.2.3",