tangled
alpha
login
or
join now
finxol.io
/
bookmarker
0
fork
atom
A very simple bookmarking webapp
bookmarker.finxol.deno.net/
0
fork
atom
overview
issues
pulls
pipelines
feat: move navigation to bottom
finxol.io
1 month ago
a52f68bf
a13faff4
verified
This commit was signed with the committer's
known signature
.
finxol.io
SSH Key Fingerprint:
SHA256:olFE3asYdoBMScuJOt60UxXdJ0RFdGv5kVKrdOtIcPI=
1/1
deploy.yaml
success
11s
+122
-54
6 changed files
expand all
collapse all
unified
split
src
components
ui
button.css
routes
__root.tsx
account.css
account.tsx
root.css
utils
auth.ts
+17
src/components/ui/button.css
···
36
36
37
37
.button-ghost {
38
38
--bg-opacity: 0;
39
39
+
--bg-lightness: 1;
40
40
+
--bg-saturation: 0.7;
39
41
--color: oklch(from var(--primary-text) l c h / 0.75);
42
42
+
background-color: hsl(
43
43
+
from var(--bg) h calc(s * var(--bg-saturation))
44
44
+
calc(l * var(--bg-lightness)) / var(--bg-opacity)
45
45
+
);
40
46
41
47
&:hover {
42
48
--bg-opacity: 0.3;
43
49
}
50
50
+
51
51
+
&:active,
52
52
+
&.active {
53
53
+
--bg-opacity: 0.4;
54
54
+
--bg-lightness: 1.05;
55
55
+
--bg-saturation: 0.25;
56
56
+
}
44
57
}
45
58
46
59
.button-danger {
···
55
68
56
69
.button-icon {
57
70
padding: 0.5rem;
71
71
+
}
72
72
+
73
73
+
.button-round {
74
74
+
border-radius: 100vw;
58
75
}
59
76
}
+36
-36
src/routes/__root.tsx
···
1
1
import { createRootRoute, Outlet } from "@tanstack/solid-router"
2
2
-
import { useQuery } from "@tanstack/solid-query"
3
2
import { Link } from "@tanstack/solid-router"
4
4
-
import { BookmarkIcon, LogInIcon } from "lucide-solid"
3
3
+
import { BookmarkIcon, CircleUserRoundIcon, LogInIcon } from "lucide-solid"
5
4
import { Show } from "solid-js"
6
6
-
import { client } from "../apiclient.ts"
7
7
-
import { ThemeSwitcher } from "../components/ThemeSwitcher.tsx"
8
5
import "./main.css"
9
6
import "./root.css"
7
7
+
import { useMe } from "../utils/auth.ts"
10
8
11
9
export const Route = createRootRoute({
12
10
component: RootComponent,
13
11
})
14
12
15
13
function RootComponent() {
16
16
-
const query = useQuery(() => ({
17
17
-
queryKey: [client.api.v1.account.me.$url().pathname],
18
18
-
queryFn: async () => {
19
19
-
const res = await client.api.v1.account.me.$get()
20
20
-
if (!res.ok) {
21
21
-
throw new Error("Not authenticated")
22
22
-
}
23
23
-
return await res.json()
24
24
-
},
25
25
-
retry: false,
26
26
-
}))
14
14
+
const query = useMe()
27
15
28
16
return (
29
17
<main>
30
30
-
<header>
31
31
-
<nav>
32
32
-
<h1>
33
33
-
<Link to="/">
34
34
-
<BookmarkIcon />
35
35
-
Bookmarker
36
36
-
</Link>
37
37
-
</h1>
38
38
-
</nav>
39
39
-
<div>
40
40
-
<ThemeSwitcher />
41
41
-
<Show when={!query.isPending && query.data}>
42
42
-
<Link to="/account">
43
43
-
<img
44
44
-
src={query.data?.avatar ?? undefined}
45
45
-
alt={query.data?.name}
46
46
-
/>
47
47
-
</Link>
48
48
-
</Show>
49
49
-
</div>
50
50
-
</header>
51
18
{!query.isPending &&
52
19
(query.data ? <Outlet /> : <LoggedOutPage />)}
20
20
+
21
21
+
<BottomNav />
53
22
54
23
<footer>© {Temporal.Now.zonedDateTimeISO().year} finxol</footer>
55
24
</main>
25
25
+
)
26
26
+
}
27
27
+
28
28
+
function BottomNav() {
29
29
+
const query = useMe()
30
30
+
31
31
+
return (
32
32
+
<nav class="bottom-nav">
33
33
+
<Link
34
34
+
to="/"
35
35
+
class="bottom-nav-item button-ghost"
36
36
+
>
37
37
+
<BookmarkIcon />
38
38
+
Bookmarks
39
39
+
</Link>
40
40
+
<Link
41
41
+
to="/account"
42
42
+
class="bottom-nav-item button-ghost"
43
43
+
>
44
44
+
<Show
45
45
+
when={!query.isPending && query.data}
46
46
+
fallback={<CircleUserRoundIcon />}
47
47
+
>
48
48
+
<img
49
49
+
src={query.data?.avatar ?? undefined}
50
50
+
alt={query.data?.name}
51
51
+
/>
52
52
+
</Show>
53
53
+
Account
54
54
+
</Link>
55
55
+
</nav>
56
56
)
57
57
}
58
58
+1
-1
src/routes/account.css
···
154
154
section.actions {
155
155
display: flex;
156
156
gap: calc(var(--spacing) * 0.5);
157
157
-
justify-content: flex-end;
157
157
+
justify-content: space-between;
158
158
159
159
button {
160
160
--saturation: 80%;
+4
-11
src/routes/account.tsx
···
12
12
} from "lucide-solid"
13
13
import "../components/ui/button.css"
14
14
import "./account.css"
15
15
+
import { ThemeSwitcher } from "../components/ThemeSwitcher.tsx"
16
16
+
import { useMe } from "../utils/auth.ts"
15
17
16
18
export const Route = createFileRoute("/account")({
17
19
component: RouteComponent,
···
22
24
const navigate = useNavigate()
23
25
const queryClient = useQueryClient()
24
26
25
25
-
const query = useQuery(() => ({
26
26
-
queryKey: [client.api.v1.account.me.$url().pathname],
27
27
-
queryFn: async () => {
28
28
-
const res = await client.api.v1.account.me.$get()
29
29
-
if (!res.ok) {
30
30
-
throw new Error("Not authenticated")
31
31
-
}
32
32
-
return await res.json()
33
33
-
},
34
34
-
retry: false,
35
35
-
}))
27
27
+
const query = useMe()
36
28
37
29
const apiKeys = useQuery(() => ({
38
30
queryKey: [client.api.v1.account.keys.$url().pathname],
···
214
206
>
215
207
Log out
216
208
</button>
209
209
+
<ThemeSwitcher />
217
210
</section>
218
211
</>
219
212
)}
+46
-4
src/routes/root.css
···
2
2
color: var(--primary-text);
3
3
4
4
display: grid;
5
5
-
grid-template-rows: auto 1fr auto;
5
5
+
grid-template-rows: 1fr auto;
6
6
min-height: 100svh;
7
7
font-family: var(--font-sans);
8
8
···
65
65
66
66
footer {
67
67
padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2.5);
68
68
-
text-align: center;
68
68
+
text-align: left;
69
69
font-size: 0.8rem;
70
70
-
color: oklch(from var(--primary-text) l c h / 0.35);
70
70
+
color: oklch(from var(--primary-text) l c h / 0.65);
71
71
border-top: 1px solid oklch(from var(--primary-text) l c h / 0.06);
72
72
}
73
73
}
74
74
75
75
+
nav.bottom-nav {
76
76
+
position: fixed;
77
77
+
bottom: calc(var(--spacing) * 2);
78
78
+
left: 0;
79
79
+
right: 0;
80
80
+
margin: auto;
81
81
+
width: fit-content;
82
82
+
display: flex;
83
83
+
justify-content: space-between;
84
84
+
align-items: center;
85
85
+
gap: calc(var(--spacing) * 0.5);
86
86
+
padding: calc(var(--spacing) * 0.5);
87
87
+
background-color: hsl(from var(--page-bg) h s calc(l * 1.7) / 0.7);
88
88
+
backdrop-filter: blur(10px);
89
89
+
border-radius: 100vw;
90
90
+
box-shadow: 0 0 10px oklch(from var(--primary-text) l c h / 0.1);
91
91
+
z-index: 100;
92
92
+
93
93
+
& > a {
94
94
+
display: flex;
95
95
+
flex-direction: column;
96
96
+
align-items: center;
97
97
+
justify-content: center;
98
98
+
border-radius: 100vw;
99
99
+
font-size: 0.7rem;
100
100
+
101
101
+
--size: 1.6rem;
102
102
+
103
103
+
img,
104
104
+
svg {
105
105
+
border-radius: 100vw;
106
106
+
}
107
107
+
108
108
+
svg {
109
109
+
width: var(--size);
110
110
+
height: var(--size);
111
111
+
padding: 0.1rem;
112
112
+
}
113
113
+
}
114
114
+
}
115
115
+
75
116
.logged-out {
76
117
display: flex;
77
118
flex-direction: column;
···
80
121
gap: var(--spacing);
81
122
padding: calc(var(--spacing) * 4);
82
123
text-align: center;
83
83
-
color: oklch(from var(--primary-text) l c h / 0.5);
124
124
+
color: oklch(from var(--primary-text) l c h / 0.75);
84
125
85
126
h1 {
86
127
font-size: 1.75rem;
···
101
142
padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2);
102
143
background-color: var(--primary);
103
144
color: white;
145
145
+
color: contrast-color(var(--primary));
104
146
border: none;
105
147
border-radius: var(--radius);
106
148
text-decoration: none;
+18
-2
src/utils/auth.ts
···
1
1
import { subjects } from "@auth/subjects.ts"
2
2
import { getClient } from "@auth/client.ts"
3
3
+
import { useQuery } from "@tanstack/solid-query"
4
4
+
import { client } from "../apiclient.ts"
3
5
4
6
if (typeof cookieStore !== "undefined") {
5
7
await import("cookie-store")
···
8
10
const clientID = import.meta.env.VITE_AUTH_CLIENT_ID
9
11
const issuerUrl = import.meta.env.VITE_AUTH_ISSUER_URL
10
12
11
11
-
export const client = getClient(clientID, issuerUrl)
13
13
+
export const authClient = getClient(clientID, issuerUrl)
12
14
13
15
export async function isAuthenticated() {
14
16
const accessToken = await cookieStore.get("access_token")
···
18
20
return false
19
21
}
20
22
21
21
-
const verified = await client.verify(subjects, accessToken.value!, {
23
23
+
const verified = await authClient.verify(subjects, accessToken.value!, {
22
24
refresh: refreshToken?.value,
23
25
})
24
26
···
53
55
54
56
return verified.subject
55
57
}
58
58
+
59
59
+
export function useMe() {
60
60
+
return useQuery(() => ({
61
61
+
queryKey: [client.api.v1.account.me.$url().pathname],
62
62
+
queryFn: async () => {
63
63
+
const res = await client.api.v1.account.me.$get()
64
64
+
if (!res.ok) {
65
65
+
throw new Error("Not authenticated")
66
66
+
}
67
67
+
return await res.json()
68
68
+
},
69
69
+
retry: false,
70
70
+
}))
71
71
+
}