alternative tangled frontend (extremely wip)

feat: working oauth flow

serenity 751268c1 c79dfd90

+207 -145
+87 -91
README.md
··· 1 - Welcome to your new TanStack app! 1 + Welcome to your new TanStack app! 2 2 3 3 # Getting Started 4 4 ··· 29 29 30 30 This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. 31 31 32 - 33 32 ## Linting & Formatting 34 - 35 33 36 34 This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available: 37 35 ··· 40 38 pnpm format 41 39 pnpm check 42 40 ``` 43 - 44 - 45 41 46 42 ## Routing 43 + 47 44 This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. 48 45 49 46 ### Adding A Route ··· 79 76 Here is an example layout that includes a header: 80 77 81 78 ```tsx 82 - import { Outlet, createRootRoute } from '@tanstack/react-router' 83 - import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' 79 + import { Outlet, createRootRoute } from "@tanstack/react-router"; 80 + import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; 84 81 85 82 import { Link } from "@tanstack/react-router"; 86 83 87 84 export const Route = createRootRoute({ 88 - component: () => ( 89 - <> 90 - <header> 91 - <nav> 92 - <Link to="/">Home</Link> 93 - <Link to="/about">About</Link> 94 - </nav> 95 - </header> 96 - <Outlet /> 97 - <TanStackRouterDevtools /> 98 - </> 99 - ), 100 - }) 85 + component: () => ( 86 + <> 87 + <header> 88 + <nav> 89 + <Link to="/">Home</Link> 90 + <Link to="/about">About</Link> 91 + </nav> 92 + </header> 93 + <Outlet /> 94 + <TanStackRouterDevtools /> 95 + </> 96 + ), 97 + }); 101 98 ``` 102 99 103 100 The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout. 104 101 105 102 More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). 106 - 107 103 108 104 ## Data Fetching 109 105 ··· 113 109 114 110 ```tsx 115 111 const peopleRoute = createRoute({ 116 - getParentRoute: () => rootRoute, 117 - path: "/people", 118 - loader: async () => { 119 - const response = await fetch("https://swapi.dev/api/people"); 120 - return response.json() as Promise<{ 121 - results: { 122 - name: string; 123 - }[]; 124 - }>; 125 - }, 126 - component: () => { 127 - const data = peopleRoute.useLoaderData(); 128 - return ( 129 - <ul> 130 - {data.results.map((person) => ( 131 - <li key={person.name}>{person.name}</li> 132 - ))} 133 - </ul> 134 - ); 135 - }, 112 + getParentRoute: () => rootRoute, 113 + path: "/people", 114 + loader: async () => { 115 + const response = await fetch("https://swapi.dev/api/people"); 116 + return response.json() as Promise<{ 117 + results: { 118 + name: string; 119 + }[]; 120 + }>; 121 + }, 122 + component: () => { 123 + const data = peopleRoute.useLoaderData(); 124 + return ( 125 + <ul> 126 + {data.results.map((person) => ( 127 + <li key={person.name}>{person.name}</li> 128 + ))} 129 + </ul> 130 + ); 131 + }, 136 132 }); 137 133 ``` 138 134 ··· 160 156 // ... 161 157 162 158 if (!rootElement.innerHTML) { 163 - const root = ReactDOM.createRoot(rootElement); 159 + const root = ReactDOM.createRoot(rootElement); 164 160 165 - root.render( 166 - <QueryClientProvider client={queryClient}> 167 - <RouterProvider router={router} /> 168 - </QueryClientProvider> 169 - ); 161 + root.render( 162 + <QueryClientProvider client={queryClient}> 163 + <RouterProvider router={router} /> 164 + </QueryClientProvider>, 165 + ); 170 166 } 171 167 ``` 172 168 ··· 176 172 import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 177 173 178 174 const rootRoute = createRootRoute({ 179 - component: () => ( 180 - <> 181 - <Outlet /> 182 - <ReactQueryDevtools buttonPosition="top-right" /> 183 - <TanStackRouterDevtools /> 184 - </> 185 - ), 175 + component: () => ( 176 + <> 177 + <Outlet /> 178 + <ReactQueryDevtools buttonPosition="top-right" /> 179 + <TanStackRouterDevtools /> 180 + </> 181 + ), 186 182 }); 187 183 ``` 188 184 ··· 194 190 import "./App.css"; 195 191 196 192 function App() { 197 - const { data } = useQuery({ 198 - queryKey: ["people"], 199 - queryFn: () => 200 - fetch("https://swapi.dev/api/people") 201 - .then((res) => res.json()) 202 - .then((data) => data.results as { name: string }[]), 203 - initialData: [], 204 - }); 193 + const { data } = useQuery({ 194 + queryKey: ["people"], 195 + queryFn: () => 196 + fetch("https://swapi.dev/api/people") 197 + .then((res) => res.json()) 198 + .then((data) => data.results as { name: string }[]), 199 + initialData: [], 200 + }); 205 201 206 - return ( 207 - <div> 208 - <ul> 209 - {data.map((person) => ( 210 - <li key={person.name}>{person.name}</li> 211 - ))} 212 - </ul> 213 - </div> 214 - ); 202 + return ( 203 + <div> 204 + <ul> 205 + {data.map((person) => ( 206 + <li key={person.name}>{person.name}</li> 207 + ))} 208 + </ul> 209 + </div> 210 + ); 215 211 } 216 212 217 213 export default App; ··· 239 235 const countStore = new Store(0); 240 236 241 237 function App() { 242 - const count = useStore(countStore); 243 - return ( 244 - <div> 245 - <button onClick={() => countStore.setState((n) => n + 1)}> 246 - Increment - {count} 247 - </button> 248 - </div> 249 - ); 238 + const count = useStore(countStore); 239 + return ( 240 + <div> 241 + <button onClick={() => countStore.setState((n) => n + 1)}> 242 + Increment - {count} 243 + </button> 244 + </div> 245 + ); 250 246 } 251 247 252 248 export default App; ··· 264 260 const countStore = new Store(0); 265 261 266 262 const doubledStore = new Derived({ 267 - fn: () => countStore.state * 2, 268 - deps: [countStore], 263 + fn: () => countStore.state * 2, 264 + deps: [countStore], 269 265 }); 270 266 doubledStore.mount(); 271 267 272 268 function App() { 273 - const count = useStore(countStore); 274 - const doubledCount = useStore(doubledStore); 269 + const count = useStore(countStore); 270 + const doubledCount = useStore(doubledStore); 275 271 276 - return ( 277 - <div> 278 - <button onClick={() => countStore.setState((n) => n + 1)}> 279 - Increment - {count} 280 - </button> 281 - <div>Doubled - {doubledCount}</div> 282 - </div> 283 - ); 272 + return ( 273 + <div> 274 + <button onClick={() => countStore.setState((n) => n + 1)}> 275 + Increment - {count} 276 + </button> 277 + <div>Doubled - {doubledCount}</div> 278 + </div> 279 + ); 284 280 } 285 281 286 282 export default App;
+52 -52
package.json
··· 1 1 { 2 - "name": "strand", 3 - "private": true, 4 - "type": "module", 5 - "scripts": { 6 - "dev": "vite dev --port 3000", 7 - "build": "vite build", 8 - "preview": "vite preview", 9 - "test": "vitest run", 10 - "lint": "eslint", 11 - "format": "prettier", 12 - "check": "prettier --write . && eslint --fix" 13 - }, 14 - "dependencies": { 15 - "@atproto/oauth-client-browser": "^0.3.40", 16 - "@fontsource-variable/hanken-grotesk": "^5.2.8", 17 - "@fontsource/amiri": "^5.2.8", 18 - "@fontsource/maple-mono": "^5.2.6", 19 - "@tailwindcss/vite": "^4.0.6", 20 - "@tanstack/react-devtools": "^0.7.0", 21 - "@tanstack/react-query": "^5.66.5", 22 - "@tanstack/react-query-devtools": "^5.84.2", 23 - "@tanstack/react-router": "^1.132.0", 24 - "@tanstack/react-router-devtools": "^1.132.0", 25 - "@tanstack/react-router-ssr-query": "^1.131.7", 26 - "@tanstack/react-start": "^1.132.0", 27 - "@tanstack/router-plugin": "^1.132.0", 28 - "lucide-react": "^0.561.0", 29 - "motion": "^12.34.0", 30 - "nitro": "latest", 31 - "react": "^19.2.0", 32 - "react-dom": "^19.2.0", 33 - "tailwindcss": "^4.0.6", 34 - "vite-tsconfig-paths": "^6.0.2" 35 - }, 36 - "devDependencies": { 37 - "@tanstack/devtools-vite": "^0.3.11", 38 - "@tanstack/eslint-config": "^0.3.0", 39 - "@testing-library/dom": "^10.4.0", 40 - "@testing-library/react": "^16.2.0", 41 - "@types/node": "^22.10.2", 42 - "@types/react": "^19.2.0", 43 - "@types/react-dom": "^19.2.0", 44 - "@vitejs/plugin-react": "^5.0.4", 45 - "babel-plugin-react-compiler": "^1.0.0", 46 - "jsdom": "^27.0.0", 47 - "prettier": "^3.5.3", 48 - "prettier-plugin-tailwindcss": "^0.7.2", 49 - "typescript": "^5.7.2", 50 - "vite": "^7.1.7", 51 - "vitest": "^3.0.5", 52 - "web-vitals": "^5.1.0" 53 - } 2 + "name": "strand", 3 + "private": true, 4 + "type": "module", 5 + "scripts": { 6 + "dev": "vite dev --port 3000", 7 + "build": "vite build", 8 + "preview": "vite preview", 9 + "test": "vitest run", 10 + "lint": "eslint", 11 + "format": "prettier", 12 + "check": "prettier --write . && eslint --fix" 13 + }, 14 + "dependencies": { 15 + "@atproto/oauth-client-browser": "^0.3.40", 16 + "@fontsource-variable/hanken-grotesk": "^5.2.8", 17 + "@fontsource/amiri": "^5.2.8", 18 + "@fontsource/maple-mono": "^5.2.6", 19 + "@tailwindcss/vite": "^4.0.6", 20 + "@tanstack/react-devtools": "^0.7.0", 21 + "@tanstack/react-query": "^5.66.5", 22 + "@tanstack/react-query-devtools": "^5.84.2", 23 + "@tanstack/react-router": "^1.132.0", 24 + "@tanstack/react-router-devtools": "^1.132.0", 25 + "@tanstack/react-router-ssr-query": "^1.131.7", 26 + "@tanstack/react-start": "^1.132.0", 27 + "@tanstack/router-plugin": "^1.132.0", 28 + "lucide-react": "^0.561.0", 29 + "motion": "^12.34.0", 30 + "nitro": "latest", 31 + "react": "^19.2.0", 32 + "react-dom": "^19.2.0", 33 + "tailwindcss": "^4.0.6", 34 + "vite-tsconfig-paths": "^6.0.2" 35 + }, 36 + "devDependencies": { 37 + "@tanstack/devtools-vite": "^0.3.11", 38 + "@tanstack/eslint-config": "^0.3.0", 39 + "@testing-library/dom": "^10.4.0", 40 + "@testing-library/react": "^16.2.0", 41 + "@types/node": "^22.10.2", 42 + "@types/react": "^19.2.0", 43 + "@types/react-dom": "^19.2.0", 44 + "@vitejs/plugin-react": "^5.0.4", 45 + "babel-plugin-react-compiler": "^1.0.0", 46 + "jsdom": "^27.0.0", 47 + "prettier": "^3.5.3", 48 + "prettier-plugin-tailwindcss": "^0.7.2", 49 + "typescript": "^5.7.2", 50 + "vite": "^7.1.7", 51 + "vitest": "^3.0.5", 52 + "web-vitals": "^5.1.0" 53 + } 54 54 }
+30
src/components/Auth/SignIn.tsx
··· 1 1 import { UnderlineLink } from "@/components/Animated/UnderlinedLink"; 2 + import { Loading } from "@/components/Icons/Loading"; 2 3 import { LucideAtSign } from "@/components/Icons/LucideAtSign"; 3 4 import { LucideCircleUserRound } from "@/components/Icons/LucideCircleUserRound"; 4 5 import { LucideInfo } from "@/components/Icons/LucideInfo"; 5 6 import { LucideLogIn } from "@/components/Icons/LucideLogIn"; 7 + import { useOAuthClient } from "@/lib/oauth"; 6 8 import { useState } from "react"; 7 9 8 10 export const SignIn = () => { 9 11 const [handle, setHandle] = useState(""); 10 12 const isValidHandle = handle.includes("."); 13 + const client = useOAuthClient(); 14 + 15 + if (!client) return <Loading />; 16 + 17 + const handleOAuthContinue = () => { 18 + const doOAuth = async () => { 19 + try { 20 + await client.signIn(handle, { 21 + ui_locales: "en", 22 + signal: new AbortController().signal, 23 + }); 24 + 25 + console.log("Never executed"); 26 + } catch (err) { 27 + console.log( 28 + 'The user aborted the authorization process by navigating "back"', 29 + ); 30 + } 31 + }; 32 + 33 + doOAuth().catch((e: unknown) => { 34 + console.error( 35 + "Something went wrong while trying to do OAuth handover.", 36 + ); 37 + console.error(e); 38 + }); 39 + }; 11 40 12 41 return ( 13 42 <div className="bg-surface0 border-surface1 m-36 flex max-w-1/4 flex-col items-center rounded-md border-1 px-6 py-4"> ··· 58 87 <button 59 88 disabled={!isValidHandle} 60 89 className="hover:bg-positive hover:text-crust hover:disabled:bg-surface1 hover:disabled:text-text bg-accent text-crust disabled:bg-surface1 disabled:text-text m-2 mt-6 mb-2 flex w-full cursor-pointer items-center justify-center gap-2 rounded-sm p-2 transition-all disabled:cursor-not-allowed" 90 + onClick={handleOAuthContinue} 61 91 > 62 92 <p>Continue</p> 63 93 <LucideLogIn />
+27
src/components/Icons/Loading.tsx
··· 1 + import { SVGProps } from "react"; 2 + 3 + export function Loading(props: SVGProps<SVGSVGElement>) { 4 + return ( 5 + <svg 6 + xmlns="http://www.w3.org/2000/svg" 7 + width="1em" 8 + height="1em" 9 + viewBox="0 0 24 24" 10 + {...props} 11 + > 12 + {/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */} 13 + <path 14 + fill="currentColor" 15 + d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z" 16 + > 17 + <animateTransform 18 + attributeName="transform" 19 + dur="0.75s" 20 + repeatCount="indefinite" 21 + type="rotate" 22 + values="0 12 12;360 12 12" 23 + /> 24 + </path> 25 + </svg> 26 + ); 27 + }
+1 -1
src/components/Nav/NavBarUnauthed.tsx
··· 24 24 iconClassName="text-crust" 25 25 labelClassName="text-crust" 26 26 underlineClassName="bg-crust" 27 - className="bg-accent rounded-sm p-1.5 pl-3 pr-3" 27 + className="bg-accent rounded-sm p-1.5 pr-3 pl-3" 28 28 position="right" 29 29 iconTransitions={{ duration: 0.2, ease: "easeInOut" }} 30 30 iconVariants={{
+6 -1
src/lib/oauth/index.tsx
··· 62 62 return <OAuthContext value={contextValue}>{children}</OAuthContext>; 63 63 }; 64 64 65 - export const useOAuthClient = () => { 65 + export const useOAuth = () => { 66 66 const ctx = useContext(OAuthContext); 67 67 if (!ctx) 68 68 throw new Error("useOAuthClient must be used within an AuthProvider"); 69 69 return ctx; 70 70 }; 71 + 72 + export const useOAuthClient = () => { 73 + const { client } = useOAuth(); 74 + return client; 75 + };
+4
vite.config.ts
··· 6 6 import tailwindcss from "@tailwindcss/vite"; 7 7 import { nitro } from "nitro/vite"; 8 8 9 + const SERVER_HOST = '127.0.0.1'; 10 + const SERVER_PORT = 3000; 11 + 9 12 const config = defineConfig({ 13 + server: { host: SERVER_HOST, port: SERVER_PORT }, 10 14 plugins: [ 11 15 devtools(), 12 16 nitro(),