+3
bun.lock
+3
bun.lock
···
7
7
"@astrojs/db": "^0.17.1",
8
8
"@astrojs/node": "^9.4.3",
9
9
"@fujocoded/authproto": "^0.0.4",
10
+
"@lucide/astro": "^0.542.0",
10
11
"astro": "^5.13.5",
11
12
"nanoid": "^5.1.5",
12
13
},
···
204
205
"@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="],
205
206
206
207
"@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="],
208
+
209
+
"@lucide/astro": ["@lucide/astro@0.542.0", "", { "peerDependencies": { "astro": "^4 || ^5" } }, "sha512-W1WcrLm4iZgjy40fhkAX8EW5LA4mGK23pewBixfRYhMj/J00Tba3GgHOEls5JqAsvlwQc6ClOLYSYA3Spzzb7w=="],
207
210
208
211
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
209
212
+1
db/config.ts
+1
db/config.ts
+1
package.json
+1
package.json
+44
-5
src/actions/users.ts
+44
-5
src/actions/users.ts
···
1
1
import { ActionError, defineAction } from "astro:actions";
2
2
import { z } from "astro:content";
3
-
import { db, Users } from "astro:db";
3
+
import { db, eq, Users } from "astro:db";
4
4
5
5
export const usersActions = {
6
6
addUser: defineAction({
7
7
accept: "form",
8
8
input: z.object({
9
-
did: z.string(),
9
+
nickname: z.string(),
10
10
}),
11
-
handler: async ({ did }, context) => {
11
+
handler: async (input, context) => {
12
12
const loggedInUser = context.locals.loggedInUser;
13
13
14
14
if (!loggedInUser) {
···
20
20
21
21
const user = await db
22
22
.insert(Users)
23
-
.values({ userDid: did })
23
+
.values({
24
+
...input.nickname && { nickname: input.nickname },
25
+
userDid: loggedInUser.did,
26
+
})
24
27
.returning();
25
28
26
29
return user;
27
30
},
28
-
})
31
+
}),
32
+
editUser: defineAction({
33
+
accept: "form",
34
+
input: z.object({
35
+
nickname: z.string().nonempty({ message: "Don't submit an empty nickname!" }),
36
+
}),
37
+
handler: async ({ nickname }, context) => {
38
+
const loggedInUser = context.locals.loggedInUser;
39
+
40
+
if (!loggedInUser) {
41
+
throw new ActionError({
42
+
code: "UNAUTHORIZED",
43
+
message: "You need to be logged in to set a nickname!",
44
+
});
45
+
}
46
+
47
+
// check if the user exists
48
+
const user = await db.select()
49
+
.from(Users)
50
+
.where(eq(Users.userDid, loggedInUser.did))
51
+
.limit(1);
52
+
53
+
if (user.length === 0) {
54
+
throw new ActionError({
55
+
code: "NOT_FOUND",
56
+
message: "Either you haven't connected your PDS account or something went wrong.",
57
+
});
58
+
}
59
+
60
+
const updatedUser = await db.update(Users)
61
+
.set({ nickname })
62
+
.where(eq(Users.userDid, loggedInUser.did))
63
+
.returning();
64
+
65
+
return updatedUser;
66
+
},
67
+
}),
29
68
}
+20
-10
src/actions/works.ts
+20
-10
src/actions/works.ts
···
19
19
accept: "form",
20
20
input: workSchema,
21
21
handler: async (input, context) => {
22
+
const loggedInUser = context.locals.loggedInUser;
23
+
22
24
// check against auth
23
-
if (!context.locals.loggedInUser) {
25
+
if (!loggedInUser) {
24
26
throw new ActionError({
25
27
code: "UNAUTHORIZED",
26
28
message: "You're not logged in!",
27
29
});
28
30
}
29
31
30
-
// const agent = await
31
-
32
-
// find the id of the logged in user
33
-
const userId = await db
32
+
// find the did of the logged in user
33
+
const query = await db
34
34
.select({ did: Users.userDid })
35
35
.from(Users)
36
-
.where(
37
-
eq(Users.userDid, context.locals.loggedInUser.did)
38
-
);
39
-
36
+
.where(eq(Users.userDid, loggedInUser.did))
37
+
.limit(1);
38
+
39
+
if (query.length === 0) {
40
+
throw new ActionError({
41
+
code: "UNAUTHORIZED",
42
+
message: "You can only add a work if you connected your PDS!",
43
+
});
44
+
}
45
+
46
+
const user = query[0];
40
47
// check nanoid for collision probability: https://zelark.github.io/nano-id-cc/
41
48
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
42
49
const nanoid = customAlphabet(alphabet, 16);
···
44
51
45
52
const work = await db.insert(Works).values({
46
53
slug,
47
-
author: userId[0].did,
54
+
author: user.did,
48
55
title: input.title,
49
56
content: input.content,
50
57
tags: input.tags,
51
58
}).returning();
59
+
60
+
// depending on whether someone toggled the privacy option, push this into firehouse
61
+
// const agent = await
52
62
53
63
return work;
54
64
},
+39
-2
src/components/Dialog.astro
+39
-2
src/components/Dialog.astro
···
1
1
---
2
+
import X from "@lucide/astro/icons/x";
3
+
2
4
interface Props {
3
5
id: string;
4
6
title: string;
···
11
13
<header>
12
14
<h1>{title}</h1>
13
15
<form method="dialog">
14
-
<button>close</button>
16
+
<button aria-label="close" class="close">
17
+
<X />
18
+
</button>
15
19
</form>
16
20
</header>
17
21
18
-
<slot />
22
+
<div class="dialog-content">
23
+
<slot />
24
+
</div>
19
25
</dialog>
20
26
27
+
<style>
28
+
dialog {
29
+
margin: auto;
30
+
min-height: 200px;
31
+
padding: 0;
32
+
33
+
header {
34
+
display: flex;
35
+
align-items: center;
36
+
background-color: aqua;
37
+
padding: 0.5rem;
38
+
39
+
h1 {
40
+
flex: 1;
41
+
font-size: var(--step--1);
42
+
}
43
+
44
+
.close {
45
+
cursor: pointer;
46
+
min-width: 44px;
47
+
min-height: 44px;
48
+
display: grid;
49
+
place-content: center;
50
+
}
51
+
}
52
+
53
+
.dialog-content {
54
+
padding: 1rem;
55
+
}
56
+
}
57
+
</style>
+49
-25
src/components/Popover.astro
+49
-25
src/components/Popover.astro
···
1
1
---
2
+
import { Info, TriangleAlert, Skull } from "@lucide/astro";
3
+
2
4
interface Props {
5
+
id?: string;
3
6
label: string;
4
-
icon?: string;
7
+
icon?: "info" | "warning" | "danger";
5
8
title?: string;
6
9
class?: string;
7
10
}
8
11
9
-
const { label, icon, title, class: className, ...rest } = Astro.props;
12
+
const { id, label, icon, title, class: className, ...rest } = Astro.props;
10
13
---
11
-
<details class:list={["popup", className]} {...rest}>
12
-
<summary>
13
-
{icon
14
-
? <div class="icon" aria-label={label}>x</div>
15
-
: <span>{label}</span>
16
-
}
17
-
</summary>
14
+
<!-- type button needs to be set here, otherwise it doesn't work inside forms -->
15
+
<button
16
+
type="button"
17
+
id={`${id}-trigger`}
18
+
class:list={["popup", "anchor", className]}
19
+
aria-describedby={id}
20
+
popovertarget={id}
21
+
>
22
+
{icon
23
+
?
24
+
<div class="icon" aria-label={label}>
25
+
{icon &&
26
+
(icon === "info") ? <Info /> :
27
+
(icon === "warning") ? <TriangleAlert /> :
28
+
(icon === "danger") ? <Skull /> :
29
+
<></>
30
+
}
31
+
</div>
32
+
: <span>{label}</span>
33
+
}
34
+
</button>
18
35
19
-
{title &&
36
+
<div {id} class:list={["popup", className]} role="tooltip" {...rest} popover="auto">
37
+
{title && (
20
38
<h3>{title}</h3>
21
-
}
39
+
)}
22
40
23
41
<slot />
24
-
</details>
42
+
</div>
43
+
44
+
<style define:vars={{ trigger: `${id}-anchor` }}>
45
+
.popup.anchor {
46
+
anchor: var(--trigger);
47
+
}
25
48
26
-
<style>
27
-
.popup {
28
-
display: inline-block;
49
+
div.popup {
50
+
position-anchor: var(--trigger);
51
+
inset: anchor(trigger );
52
+
position-try-fallbacks: flip-block, flip-inline;
53
+
}
54
+
</style>
29
55
30
-
summary {
31
-
font-size: var(--step--2);
32
-
cursor: pointer;
33
-
}
56
+
<script define:vars={{ id }} is:inline>
57
+
const trigger = document.getElementById(`${id}-trigger`);
58
+
const popover = document.getElementById(id);
34
59
35
-
&::details-content {
36
-
position: absolute;
37
-
z-index: 1;
38
-
}
39
-
}
40
-
</style>
60
+
trigger.addEventListener("click", (e) => {
61
+
e.preventDefault();
62
+
popover.togglePopover();
63
+
});
64
+
</script>
+7
src/pages/index.astro
+7
src/pages/index.astro
···
1
1
---
2
+
import Popover from "~/Popover.astro";
2
3
import Layout from "../layouts/Layout.astro";
3
4
4
5
const currentUser = Astro.locals.loggedInUser;
···
9
10
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Praesentium eum est quisquam distinctio magni recusandae quia vero tempore consectetur! Dolore repellat, voluptatem dignissimos sit eaque iste atque facilis in saepe?</p>
10
11
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorem, maxime libero eveniet repellat corporis, architecto voluptate maiores ullam accusamus quasi nostrum nihil placeat cum earum ex voluptatum, harum sunt quam!</p>
11
12
</main>
13
+
14
+
<form>
15
+
<Popover id="hey" label="test">
16
+
<p>hello?</p>
17
+
</Popover>
18
+
</form>
12
19
13
20
{currentUser
14
21
? <>
+6
-7
src/pages/login.astro
+6
-7
src/pages/login.astro
···
14
14
) : (
15
15
// If there's no current user, show the log in button
16
16
<form action="/oauth/login" method="post">
17
-
<label for="handle">Input your handle
18
-
<Popover label="help">
19
-
<h3>What's my handle?</h3>
20
-
<p>It'll look like a website URL without the <samp>https://</samp> or slashes, so a typical BlueSky handle will look something like: <b>alice.bsky.social</b>.</p>
21
-
<p>What yours will look like depends on whether you made a custom handle!</p>
22
-
</Popover>
23
-
</label>
17
+
<label for="handle">Input your handle</label>
18
+
<Popover id="handle-help" label="help">
19
+
<h3>What's my handle?</h3>
20
+
<p>It'll look like a website URL without the <samp>https://</samp> or slashes, so a typical BlueSky handle will look something like: <b>alice.bsky.social</b>.</p>
21
+
<p>What yours will look like depends on whether you made a custom handle!</p>
22
+
</Popover>
24
23
<input name="atproto-id" id="handle" required />
25
24
<button type="submit">Login</button>
26
25
</form>
+11
-3
src/pages/user/index.astro
+11
-3
src/pages/user/index.astro
···
3
3
import { actions } from "astro:actions";
4
4
import { db, eq, Users, Works } from "astro:db";
5
5
import Dialog from "~/Dialog.astro";
6
+
import Popover from "~/Popover.astro";
6
7
7
8
const loggedInUser = Astro.locals.loggedInUser;
8
9
···
20
21
<p>{loggedInUser?.handle}</p>
21
22
22
23
<!-- registration will only happen in the below form! -->
23
-
{!user && (
24
+
{(user.length === 0) && (
24
25
<>
25
26
<h2>Connect account</h2>
26
27
<div class="info">
···
31
32
32
33
<Dialog id="connect-account" title="Are you sure?">
33
34
<form action={actions.usersActions.addUser} method="post">
34
-
<input type="hidden" name="did" value={loggedInUser.did} />
35
-
35
+
<label for="nickname">Nickname</label>
36
+
<Popover id="nickname-info" label="info" icon="danger">
37
+
<p>You can optionally set your nickname for this site. This is separate from your handle and acts as your identifier.</p>
38
+
<p>Think of your handle as what you use to log in with, and your nickname as the name you want to publish your works under.</p>
39
+
<h3>Important</h3>
40
+
<p>If you do set a nickname, </p>
41
+
</Popover>
42
+
<input type="text" name="nickname" id="nickname" />
43
+
36
44
<button formmethod="dialog">Cancel</button>
37
45
<button>Confirm</button>
38
46
</form>