tangled
alpha
login
or
join now
isuggest.selfce.st
/
strand
3
fork
atom
alternative tangled frontend (extremely wip)
3
fork
atom
overview
issues
pulls
pipelines
feat: working oauth flow
serenity
2 weeks ago
751268c1
c79dfd90
+207
-145
7 changed files
expand all
collapse all
unified
split
README.md
package.json
src
components
Auth
SignIn.tsx
Icons
Loading.tsx
Nav
NavBarUnauthed.tsx
lib
oauth
index.tsx
vite.config.ts
+87
-91
README.md
···
1
1
-
Welcome to your new TanStack app!
1
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
32
-
33
32
## Linting & Formatting
34
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
43
-
44
44
-
45
41
46
42
## Routing
43
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
82
-
import { Outlet, createRootRoute } from '@tanstack/react-router'
83
83
-
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
79
79
+
import { Outlet, createRootRoute } from "@tanstack/react-router";
80
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
88
-
component: () => (
89
89
-
<>
90
90
-
<header>
91
91
-
<nav>
92
92
-
<Link to="/">Home</Link>
93
93
-
<Link to="/about">About</Link>
94
94
-
</nav>
95
95
-
</header>
96
96
-
<Outlet />
97
97
-
<TanStackRouterDevtools />
98
98
-
</>
99
99
-
),
100
100
-
})
85
85
+
component: () => (
86
86
+
<>
87
87
+
<header>
88
88
+
<nav>
89
89
+
<Link to="/">Home</Link>
90
90
+
<Link to="/about">About</Link>
91
91
+
</nav>
92
92
+
</header>
93
93
+
<Outlet />
94
94
+
<TanStackRouterDevtools />
95
95
+
</>
96
96
+
),
97
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
106
-
107
103
108
104
## Data Fetching
109
105
···
113
109
114
110
```tsx
115
111
const peopleRoute = createRoute({
116
116
-
getParentRoute: () => rootRoute,
117
117
-
path: "/people",
118
118
-
loader: async () => {
119
119
-
const response = await fetch("https://swapi.dev/api/people");
120
120
-
return response.json() as Promise<{
121
121
-
results: {
122
122
-
name: string;
123
123
-
}[];
124
124
-
}>;
125
125
-
},
126
126
-
component: () => {
127
127
-
const data = peopleRoute.useLoaderData();
128
128
-
return (
129
129
-
<ul>
130
130
-
{data.results.map((person) => (
131
131
-
<li key={person.name}>{person.name}</li>
132
132
-
))}
133
133
-
</ul>
134
134
-
);
135
135
-
},
112
112
+
getParentRoute: () => rootRoute,
113
113
+
path: "/people",
114
114
+
loader: async () => {
115
115
+
const response = await fetch("https://swapi.dev/api/people");
116
116
+
return response.json() as Promise<{
117
117
+
results: {
118
118
+
name: string;
119
119
+
}[];
120
120
+
}>;
121
121
+
},
122
122
+
component: () => {
123
123
+
const data = peopleRoute.useLoaderData();
124
124
+
return (
125
125
+
<ul>
126
126
+
{data.results.map((person) => (
127
127
+
<li key={person.name}>{person.name}</li>
128
128
+
))}
129
129
+
</ul>
130
130
+
);
131
131
+
},
136
132
});
137
133
```
138
134
···
160
156
// ...
161
157
162
158
if (!rootElement.innerHTML) {
163
163
-
const root = ReactDOM.createRoot(rootElement);
159
159
+
const root = ReactDOM.createRoot(rootElement);
164
160
165
165
-
root.render(
166
166
-
<QueryClientProvider client={queryClient}>
167
167
-
<RouterProvider router={router} />
168
168
-
</QueryClientProvider>
169
169
-
);
161
161
+
root.render(
162
162
+
<QueryClientProvider client={queryClient}>
163
163
+
<RouterProvider router={router} />
164
164
+
</QueryClientProvider>,
165
165
+
);
170
166
}
171
167
```
172
168
···
176
172
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
177
173
178
174
const rootRoute = createRootRoute({
179
179
-
component: () => (
180
180
-
<>
181
181
-
<Outlet />
182
182
-
<ReactQueryDevtools buttonPosition="top-right" />
183
183
-
<TanStackRouterDevtools />
184
184
-
</>
185
185
-
),
175
175
+
component: () => (
176
176
+
<>
177
177
+
<Outlet />
178
178
+
<ReactQueryDevtools buttonPosition="top-right" />
179
179
+
<TanStackRouterDevtools />
180
180
+
</>
181
181
+
),
186
182
});
187
183
```
188
184
···
194
190
import "./App.css";
195
191
196
192
function App() {
197
197
-
const { data } = useQuery({
198
198
-
queryKey: ["people"],
199
199
-
queryFn: () =>
200
200
-
fetch("https://swapi.dev/api/people")
201
201
-
.then((res) => res.json())
202
202
-
.then((data) => data.results as { name: string }[]),
203
203
-
initialData: [],
204
204
-
});
193
193
+
const { data } = useQuery({
194
194
+
queryKey: ["people"],
195
195
+
queryFn: () =>
196
196
+
fetch("https://swapi.dev/api/people")
197
197
+
.then((res) => res.json())
198
198
+
.then((data) => data.results as { name: string }[]),
199
199
+
initialData: [],
200
200
+
});
205
201
206
206
-
return (
207
207
-
<div>
208
208
-
<ul>
209
209
-
{data.map((person) => (
210
210
-
<li key={person.name}>{person.name}</li>
211
211
-
))}
212
212
-
</ul>
213
213
-
</div>
214
214
-
);
202
202
+
return (
203
203
+
<div>
204
204
+
<ul>
205
205
+
{data.map((person) => (
206
206
+
<li key={person.name}>{person.name}</li>
207
207
+
))}
208
208
+
</ul>
209
209
+
</div>
210
210
+
);
215
211
}
216
212
217
213
export default App;
···
239
235
const countStore = new Store(0);
240
236
241
237
function App() {
242
242
-
const count = useStore(countStore);
243
243
-
return (
244
244
-
<div>
245
245
-
<button onClick={() => countStore.setState((n) => n + 1)}>
246
246
-
Increment - {count}
247
247
-
</button>
248
248
-
</div>
249
249
-
);
238
238
+
const count = useStore(countStore);
239
239
+
return (
240
240
+
<div>
241
241
+
<button onClick={() => countStore.setState((n) => n + 1)}>
242
242
+
Increment - {count}
243
243
+
</button>
244
244
+
</div>
245
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
267
-
fn: () => countStore.state * 2,
268
268
-
deps: [countStore],
263
263
+
fn: () => countStore.state * 2,
264
264
+
deps: [countStore],
269
265
});
270
266
doubledStore.mount();
271
267
272
268
function App() {
273
273
-
const count = useStore(countStore);
274
274
-
const doubledCount = useStore(doubledStore);
269
269
+
const count = useStore(countStore);
270
270
+
const doubledCount = useStore(doubledStore);
275
271
276
276
-
return (
277
277
-
<div>
278
278
-
<button onClick={() => countStore.setState((n) => n + 1)}>
279
279
-
Increment - {count}
280
280
-
</button>
281
281
-
<div>Doubled - {doubledCount}</div>
282
282
-
</div>
283
283
-
);
272
272
+
return (
273
273
+
<div>
274
274
+
<button onClick={() => countStore.setState((n) => n + 1)}>
275
275
+
Increment - {count}
276
276
+
</button>
277
277
+
<div>Doubled - {doubledCount}</div>
278
278
+
</div>
279
279
+
);
284
280
}
285
281
286
282
export default App;
+52
-52
package.json
···
1
1
{
2
2
-
"name": "strand",
3
3
-
"private": true,
4
4
-
"type": "module",
5
5
-
"scripts": {
6
6
-
"dev": "vite dev --port 3000",
7
7
-
"build": "vite build",
8
8
-
"preview": "vite preview",
9
9
-
"test": "vitest run",
10
10
-
"lint": "eslint",
11
11
-
"format": "prettier",
12
12
-
"check": "prettier --write . && eslint --fix"
13
13
-
},
14
14
-
"dependencies": {
15
15
-
"@atproto/oauth-client-browser": "^0.3.40",
16
16
-
"@fontsource-variable/hanken-grotesk": "^5.2.8",
17
17
-
"@fontsource/amiri": "^5.2.8",
18
18
-
"@fontsource/maple-mono": "^5.2.6",
19
19
-
"@tailwindcss/vite": "^4.0.6",
20
20
-
"@tanstack/react-devtools": "^0.7.0",
21
21
-
"@tanstack/react-query": "^5.66.5",
22
22
-
"@tanstack/react-query-devtools": "^5.84.2",
23
23
-
"@tanstack/react-router": "^1.132.0",
24
24
-
"@tanstack/react-router-devtools": "^1.132.0",
25
25
-
"@tanstack/react-router-ssr-query": "^1.131.7",
26
26
-
"@tanstack/react-start": "^1.132.0",
27
27
-
"@tanstack/router-plugin": "^1.132.0",
28
28
-
"lucide-react": "^0.561.0",
29
29
-
"motion": "^12.34.0",
30
30
-
"nitro": "latest",
31
31
-
"react": "^19.2.0",
32
32
-
"react-dom": "^19.2.0",
33
33
-
"tailwindcss": "^4.0.6",
34
34
-
"vite-tsconfig-paths": "^6.0.2"
35
35
-
},
36
36
-
"devDependencies": {
37
37
-
"@tanstack/devtools-vite": "^0.3.11",
38
38
-
"@tanstack/eslint-config": "^0.3.0",
39
39
-
"@testing-library/dom": "^10.4.0",
40
40
-
"@testing-library/react": "^16.2.0",
41
41
-
"@types/node": "^22.10.2",
42
42
-
"@types/react": "^19.2.0",
43
43
-
"@types/react-dom": "^19.2.0",
44
44
-
"@vitejs/plugin-react": "^5.0.4",
45
45
-
"babel-plugin-react-compiler": "^1.0.0",
46
46
-
"jsdom": "^27.0.0",
47
47
-
"prettier": "^3.5.3",
48
48
-
"prettier-plugin-tailwindcss": "^0.7.2",
49
49
-
"typescript": "^5.7.2",
50
50
-
"vite": "^7.1.7",
51
51
-
"vitest": "^3.0.5",
52
52
-
"web-vitals": "^5.1.0"
53
53
-
}
2
2
+
"name": "strand",
3
3
+
"private": true,
4
4
+
"type": "module",
5
5
+
"scripts": {
6
6
+
"dev": "vite dev --port 3000",
7
7
+
"build": "vite build",
8
8
+
"preview": "vite preview",
9
9
+
"test": "vitest run",
10
10
+
"lint": "eslint",
11
11
+
"format": "prettier",
12
12
+
"check": "prettier --write . && eslint --fix"
13
13
+
},
14
14
+
"dependencies": {
15
15
+
"@atproto/oauth-client-browser": "^0.3.40",
16
16
+
"@fontsource-variable/hanken-grotesk": "^5.2.8",
17
17
+
"@fontsource/amiri": "^5.2.8",
18
18
+
"@fontsource/maple-mono": "^5.2.6",
19
19
+
"@tailwindcss/vite": "^4.0.6",
20
20
+
"@tanstack/react-devtools": "^0.7.0",
21
21
+
"@tanstack/react-query": "^5.66.5",
22
22
+
"@tanstack/react-query-devtools": "^5.84.2",
23
23
+
"@tanstack/react-router": "^1.132.0",
24
24
+
"@tanstack/react-router-devtools": "^1.132.0",
25
25
+
"@tanstack/react-router-ssr-query": "^1.131.7",
26
26
+
"@tanstack/react-start": "^1.132.0",
27
27
+
"@tanstack/router-plugin": "^1.132.0",
28
28
+
"lucide-react": "^0.561.0",
29
29
+
"motion": "^12.34.0",
30
30
+
"nitro": "latest",
31
31
+
"react": "^19.2.0",
32
32
+
"react-dom": "^19.2.0",
33
33
+
"tailwindcss": "^4.0.6",
34
34
+
"vite-tsconfig-paths": "^6.0.2"
35
35
+
},
36
36
+
"devDependencies": {
37
37
+
"@tanstack/devtools-vite": "^0.3.11",
38
38
+
"@tanstack/eslint-config": "^0.3.0",
39
39
+
"@testing-library/dom": "^10.4.0",
40
40
+
"@testing-library/react": "^16.2.0",
41
41
+
"@types/node": "^22.10.2",
42
42
+
"@types/react": "^19.2.0",
43
43
+
"@types/react-dom": "^19.2.0",
44
44
+
"@vitejs/plugin-react": "^5.0.4",
45
45
+
"babel-plugin-react-compiler": "^1.0.0",
46
46
+
"jsdom": "^27.0.0",
47
47
+
"prettier": "^3.5.3",
48
48
+
"prettier-plugin-tailwindcss": "^0.7.2",
49
49
+
"typescript": "^5.7.2",
50
50
+
"vite": "^7.1.7",
51
51
+
"vitest": "^3.0.5",
52
52
+
"web-vitals": "^5.1.0"
53
53
+
}
54
54
}
+30
src/components/Auth/SignIn.tsx
···
1
1
import { UnderlineLink } from "@/components/Animated/UnderlinedLink";
2
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
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
13
+
const client = useOAuthClient();
14
14
+
15
15
+
if (!client) return <Loading />;
16
16
+
17
17
+
const handleOAuthContinue = () => {
18
18
+
const doOAuth = async () => {
19
19
+
try {
20
20
+
await client.signIn(handle, {
21
21
+
ui_locales: "en",
22
22
+
signal: new AbortController().signal,
23
23
+
});
24
24
+
25
25
+
console.log("Never executed");
26
26
+
} catch (err) {
27
27
+
console.log(
28
28
+
'The user aborted the authorization process by navigating "back"',
29
29
+
);
30
30
+
}
31
31
+
};
32
32
+
33
33
+
doOAuth().catch((e: unknown) => {
34
34
+
console.error(
35
35
+
"Something went wrong while trying to do OAuth handover.",
36
36
+
);
37
37
+
console.error(e);
38
38
+
});
39
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
90
+
onClick={handleOAuthContinue}
61
91
>
62
92
<p>Continue</p>
63
93
<LucideLogIn />
+27
src/components/Icons/Loading.tsx
···
1
1
+
import { SVGProps } from "react";
2
2
+
3
3
+
export function Loading(props: SVGProps<SVGSVGElement>) {
4
4
+
return (
5
5
+
<svg
6
6
+
xmlns="http://www.w3.org/2000/svg"
7
7
+
width="1em"
8
8
+
height="1em"
9
9
+
viewBox="0 0 24 24"
10
10
+
{...props}
11
11
+
>
12
12
+
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
13
13
+
<path
14
14
+
fill="currentColor"
15
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
16
+
>
17
17
+
<animateTransform
18
18
+
attributeName="transform"
19
19
+
dur="0.75s"
20
20
+
repeatCount="indefinite"
21
21
+
type="rotate"
22
22
+
values="0 12 12;360 12 12"
23
23
+
/>
24
24
+
</path>
25
25
+
</svg>
26
26
+
);
27
27
+
}
+1
-1
src/components/Nav/NavBarUnauthed.tsx
···
24
24
iconClassName="text-crust"
25
25
labelClassName="text-crust"
26
26
underlineClassName="bg-crust"
27
27
-
className="bg-accent rounded-sm p-1.5 pl-3 pr-3"
27
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
65
-
export const useOAuthClient = () => {
65
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
71
+
72
72
+
export const useOAuthClient = () => {
73
73
+
const { client } = useOAuth();
74
74
+
return client;
75
75
+
};
+4
vite.config.ts
···
6
6
import tailwindcss from "@tailwindcss/vite";
7
7
import { nitro } from "nitro/vite";
8
8
9
9
+
const SERVER_HOST = '127.0.0.1';
10
10
+
const SERVER_PORT = 3000;
11
11
+
9
12
const config = defineConfig({
13
13
+
server: { host: SERVER_HOST, port: SERVER_PORT },
10
14
plugins: [
11
15
devtools(),
12
16
nitro(),