this repo has no description

Refactor main page around whether user is logged in

modamo-gh f708d2fa 3ed0e82b

+184 -128
app/library/page.tsx app/[handle]/page.tsx
+136
app/login/page.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useRef, useState } from "react"; 4 + import { Saira_Semi_Condensed } from "next/font/google"; 5 + import Image from "next/image"; 6 + 7 + const saira = Saira_Semi_Condensed({ 8 + weight: ["900"] 9 + }); 10 + 11 + const Login = () => { 12 + const isSelectingRef = useRef(false); 13 + 14 + const [handle, setHandle] = useState(""); 15 + const [handleSuggestions, setHandleSuggestions] = useState([]); 16 + 17 + useEffect(() => { 18 + if (isSelectingRef.current) { 19 + isSelectingRef.current = false; 20 + return; 21 + } 22 + 23 + if (handle.length < 3) { 24 + setHandleSuggestions([]); 25 + 26 + return; 27 + } 28 + 29 + const timer = setTimeout(async () => { 30 + const response = await fetch( 31 + `/api/search-handles?handle=${handle}` 32 + ); 33 + const data = await response.json(); 34 + 35 + setHandleSuggestions(data.actors || []); 36 + }, 400); 37 + 38 + return () => clearTimeout(timer); 39 + }, [handle]); 40 + 41 + return ( 42 + <div className="bg-gray-800 grid grid-rows-10 h-screen p-4 w-screen"> 43 + <main className="flex flex-col items-center justify-center row-span-9"> 44 + <h1 className={`${saira.className} text-orange-400 text-8xl`}> 45 + MOOTPOOL 46 + </h1> 47 + <div className="flex flex-col gap-2 w-1/4"> 48 + <label htmlFor="handle">Your ATProto Handle</label> 49 + <div> 50 + <input 51 + className={`border border-gray-600 focus:border-orange-400 h-12 focus:outline-none p-2 ${ 52 + handleSuggestions.length 53 + ? "rounded-t-lg" 54 + : "rounded-lg" 55 + } w-full`} 56 + id="" 57 + name="handle" 58 + placeholder="Enter your ATProto handle (e.g. example.blacksky.app)" 59 + onChange={(e) => setHandle(e.target.value)} 60 + type="text" 61 + value={handle} 62 + /> 63 + {handleSuggestions.length ? ( 64 + <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"> 65 + {handleSuggestions.map((suggestion, index) => ( 66 + <div 67 + className="hover:bg-gray-700 flex gap-2 h-12 p-1 rounded-lg" 68 + key={index} 69 + onClick={() => { 70 + isSelectingRef.current = true; 71 + 72 + setHandleSuggestions([]); 73 + setHandle(suggestion.handle); 74 + }} 75 + > 76 + <Image 77 + alt={""} 78 + className="aspect-square rounded-full" 79 + height={40} 80 + src={suggestion.avatar} 81 + width={40} 82 + /> 83 + <div className="flex flex-col justify-center"> 84 + <p className="text-sm"> 85 + {suggestion.displayName} 86 + </p> 87 + <p className="text-xs text-gray-400"> 88 + {suggestion.handle} 89 + </p> 90 + </div> 91 + </div> 92 + ))} 93 + </div> 94 + ) : null} 95 + </div> 96 + <button 97 + className={`${saira.className} bg-orange-400 hover:bg-orange-300 hover:cursor-pointer h-12 rounded-lg`} 98 + onClick={async () => { 99 + try { 100 + const response = await fetch("/oauth/login", { 101 + body: JSON.stringify({ handle }), 102 + headers: { 103 + "Content-Type": "application/json" 104 + }, 105 + method: "POST" 106 + }); 107 + const data = await response.json(); 108 + 109 + console.log(data); 110 + 111 + if (response.ok && data.redirectURL) { 112 + window.location.href = data.redirectURL; 113 + } else { 114 + console.error( 115 + "Login initiation failed:", 116 + data.error 117 + ); 118 + } 119 + } catch (error) { 120 + console.error( 121 + "Error initiating ATProto OAuth:", 122 + error 123 + ); 124 + } 125 + }} 126 + > 127 + Continue 128 + </button> 129 + </div> 130 + </main> 131 + <footer className="row-span-1" /> 132 + </div> 133 + ); 134 + }; 135 + 136 + export default Login;
+6 -1
app/oauth/callback/route.ts
··· 1 + import { getHandle } from "@/lib/atproto/profile"; 1 2 import { getOAuthClient } from "@/lib/auth/client"; 3 + import { Agent } from "@atproto/api"; 2 4 import { NextRequest, NextResponse } from "next/server"; 3 5 4 6 const PUBLIC_URL = process.env.PUBLIC_URL || "http://127.0.0.1:3000"; ··· 8 10 const params = request.nextUrl.searchParams; 9 11 const client = await getOAuthClient(); 10 12 const { session } = await client.callback(params); 11 - const response = NextResponse.redirect(new URL("/library", PUBLIC_URL)); 13 + const handle = await getHandle(session); 14 + const response = NextResponse.redirect( 15 + new URL(`/${handle}`, PUBLIC_URL) 16 + ); 12 17 13 18 response.cookies.set("did", session.did, { 14 19 httpOnly: true,
+11 -127
app/page.tsx
··· 1 - "use client"; 2 - 3 - import { Saira_Semi_Condensed } from "next/font/google"; 4 - import Image from "next/image"; 5 - import { useEffect, useRef, useState } from "react"; 6 - 7 - const saira = Saira_Semi_Condensed({ 8 - weight: ["900"] 9 - }); 10 - 11 - const Home = () => { 12 - const isSelectingRef = useRef(false); 13 - 14 - const [handle, setHandle] = useState(""); 15 - const [handleSuggestions, setHandleSuggestions] = useState([]); 1 + import { getHandle } from "@/lib/atproto/profile"; 2 + import { getSession } from "@/lib/auth/session"; 3 + import { redirect } from "next/navigation"; 16 4 17 - useEffect(() => { 18 - if (isSelectingRef.current) { 19 - isSelectingRef.current = false; 20 - return; 21 - } 22 - 23 - if (handle.length < 3) { 24 - setHandleSuggestions([]); 25 - 26 - return; 27 - } 28 - 29 - const timer = setTimeout(async () => { 30 - const response = await fetch( 31 - `/api/search-handles?handle=${handle}` 32 - ); 33 - const data = await response.json(); 34 - 35 - setHandleSuggestions(data.actors || []); 36 - }, 400); 37 - 38 - return () => clearTimeout(timer); 39 - }, [handle]); 40 - 41 - return ( 42 - <main className="bg-gray-800 flex flex-col h-screen items-center justify-center w-screen"> 43 - <h1 className={`${saira.className} text-orange-400 text-8xl`}> 44 - MOOTPOOL 45 - </h1> 46 - <div className="flex flex-col gap-2 w-1/4"> 47 - <label htmlFor="handle">Your ATProto Handle</label> 48 - <div> 49 - <input 50 - className={`border border-gray-600 focus:border-orange-400 h-12 focus:outline-none p-2 ${ 51 - handleSuggestions.length 52 - ? "rounded-t-lg" 53 - : "rounded-lg" 54 - } w-full`} 55 - id="" 56 - name="handle" 57 - placeholder="Enter your ATProto handle (e.g. example.blacksky.app)" 58 - onChange={(e) => setHandle(e.target.value)} 59 - type="text" 60 - value={handle} 61 - /> 62 - {handleSuggestions.length ? ( 63 - <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"> 64 - {handleSuggestions.map((suggestion, index) => ( 65 - <div 66 - className="hover:bg-gray-700 flex gap-2 h-12 p-1 rounded-lg" 67 - key={index} 68 - onClick={() => { 69 - isSelectingRef.current = true; 5 + const App = async () => { 6 + const session = await getSession(); 70 7 71 - setHandleSuggestions([]); 72 - setHandle(suggestion.handle); 73 - }} 74 - > 75 - <Image 76 - alt={""} 77 - className="aspect-square rounded-full" 78 - height={40} 79 - src={suggestion.avatar} 80 - width={40} 81 - /> 82 - <div className="flex flex-col justify-center"> 83 - <p className="text-sm"> 84 - {suggestion.displayName} 85 - </p> 86 - <p className="text-xs text-gray-400"> 87 - {suggestion.handle} 88 - </p> 89 - </div> 90 - </div> 91 - ))} 92 - </div> 93 - ) : null} 94 - </div> 95 - <button 96 - className={`${saira.className} bg-orange-400 hover:bg-orange-300 hover:cursor-pointer h-12 rounded-lg`} 97 - onClick={async () => { 98 - try { 99 - const response = await fetch("/oauth/login", { 100 - body: JSON.stringify({ handle }), 101 - headers: { 102 - "Content-Type": "application/json" 103 - }, 104 - method: "POST" 105 - }); 106 - const data = await response.json(); 8 + if (!session) { 9 + redirect("/login"); 10 + } 107 11 108 - console.log(data); 12 + const handle = await getHandle(session); 109 13 110 - if (response.ok && data.redirectURL) { 111 - window.location.href = data.redirectURL; 112 - } else { 113 - console.error( 114 - "Login initiation failed:", 115 - data.error 116 - ); 117 - } 118 - } catch (error) { 119 - console.error( 120 - "Error initiating ATProto OAuth:", 121 - error 122 - ); 123 - } 124 - }} 125 - > 126 - Continue 127 - </button> 128 - </div> 129 - </main> 130 - ); 14 + redirect(`/${handle}`); 131 15 }; 132 16 133 - export default Home; 17 + export default App;
+20
components/Header.tsx
··· 1 + import { Saira_Semi_Condensed } from "next/font/google"; 2 + import BookSearch from "./BookSearch"; 3 + 4 + const saira = Saira_Semi_Condensed({ 5 + weight: ["900"] 6 + }); 7 + 8 + const Header = () => { 9 + return ( 10 + <header className="flex gap-4 items-center row-span-1"> 11 + <h1 className={`${saira.className} text-orange-400 text-4xl`}> 12 + MOOTPOOL 13 + </h1> 14 + <BookSearch /> 15 + <div className="aspect-square bg-gray-700 border border-gray-600 h-12 w-12 rounded-full" /> 16 + </header> 17 + ); 18 + }; 19 + 20 + export default Header;
+11
lib/atproto/profile.ts
··· 1 + import { Agent } from "@atproto/api"; 2 + import { OAuthSession } from "@atproto/oauth-client-node"; 3 + 4 + export const getHandle = async (session: OAuthSession) => { 5 + const agent = new Agent(session); 6 + const profile = await agent.app.bsky.actor.getProfile({ 7 + actor: session.did 8 + }); 9 + 10 + return profile.data.handle; 11 + };