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