A very simple bookmarking webapp bookmarker.finxol.deno.net/

feat: move navigation to bottom

finxol.io a52f68bf a13faff4

verified
+122 -54
+17
src/components/ui/button.css
··· 36 36 37 37 .button-ghost { 38 38 --bg-opacity: 0; 39 + --bg-lightness: 1; 40 + --bg-saturation: 0.7; 39 41 --color: oklch(from var(--primary-text) l c h / 0.75); 42 + background-color: hsl( 43 + from var(--bg) h calc(s * var(--bg-saturation)) 44 + calc(l * var(--bg-lightness)) / var(--bg-opacity) 45 + ); 40 46 41 47 &:hover { 42 48 --bg-opacity: 0.3; 43 49 } 50 + 51 + &:active, 52 + &.active { 53 + --bg-opacity: 0.4; 54 + --bg-lightness: 1.05; 55 + --bg-saturation: 0.25; 56 + } 44 57 } 45 58 46 59 .button-danger { ··· 55 68 56 69 .button-icon { 57 70 padding: 0.5rem; 71 + } 72 + 73 + .button-round { 74 + border-radius: 100vw; 58 75 } 59 76 }
+36 -36
src/routes/__root.tsx
··· 1 1 import { createRootRoute, Outlet } from "@tanstack/solid-router" 2 - import { useQuery } from "@tanstack/solid-query" 3 2 import { Link } from "@tanstack/solid-router" 4 - import { BookmarkIcon, LogInIcon } from "lucide-solid" 3 + import { BookmarkIcon, CircleUserRoundIcon, LogInIcon } from "lucide-solid" 5 4 import { Show } from "solid-js" 6 - import { client } from "../apiclient.ts" 7 - import { ThemeSwitcher } from "../components/ThemeSwitcher.tsx" 8 5 import "./main.css" 9 6 import "./root.css" 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 - const query = useQuery(() => ({ 17 - queryKey: [client.api.v1.account.me.$url().pathname], 18 - queryFn: async () => { 19 - const res = await client.api.v1.account.me.$get() 20 - if (!res.ok) { 21 - throw new Error("Not authenticated") 22 - } 23 - return await res.json() 24 - }, 25 - retry: false, 26 - })) 14 + const query = useMe() 27 15 28 16 return ( 29 17 <main> 30 - <header> 31 - <nav> 32 - <h1> 33 - <Link to="/"> 34 - <BookmarkIcon /> 35 - Bookmarker 36 - </Link> 37 - </h1> 38 - </nav> 39 - <div> 40 - <ThemeSwitcher /> 41 - <Show when={!query.isPending && query.data}> 42 - <Link to="/account"> 43 - <img 44 - src={query.data?.avatar ?? undefined} 45 - alt={query.data?.name} 46 - /> 47 - </Link> 48 - </Show> 49 - </div> 50 - </header> 51 18 {!query.isPending && 52 19 (query.data ? <Outlet /> : <LoggedOutPage />)} 20 + 21 + <BottomNav /> 53 22 54 23 <footer>© {Temporal.Now.zonedDateTimeISO().year} finxol</footer> 55 24 </main> 25 + ) 26 + } 27 + 28 + function BottomNav() { 29 + const query = useMe() 30 + 31 + return ( 32 + <nav class="bottom-nav"> 33 + <Link 34 + to="/" 35 + class="bottom-nav-item button-ghost" 36 + > 37 + <BookmarkIcon /> 38 + Bookmarks 39 + </Link> 40 + <Link 41 + to="/account" 42 + class="bottom-nav-item button-ghost" 43 + > 44 + <Show 45 + when={!query.isPending && query.data} 46 + fallback={<CircleUserRoundIcon />} 47 + > 48 + <img 49 + src={query.data?.avatar ?? undefined} 50 + alt={query.data?.name} 51 + /> 52 + </Show> 53 + Account 54 + </Link> 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 - justify-content: flex-end; 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 + import { ThemeSwitcher } from "../components/ThemeSwitcher.tsx" 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 - const query = useQuery(() => ({ 26 - queryKey: [client.api.v1.account.me.$url().pathname], 27 - queryFn: async () => { 28 - const res = await client.api.v1.account.me.$get() 29 - if (!res.ok) { 30 - throw new Error("Not authenticated") 31 - } 32 - return await res.json() 33 - }, 34 - retry: false, 35 - })) 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 + <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 - grid-template-rows: auto 1fr auto; 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 - text-align: center; 68 + text-align: left; 69 69 font-size: 0.8rem; 70 - color: oklch(from var(--primary-text) l c h / 0.35); 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 + nav.bottom-nav { 76 + position: fixed; 77 + bottom: calc(var(--spacing) * 2); 78 + left: 0; 79 + right: 0; 80 + margin: auto; 81 + width: fit-content; 82 + display: flex; 83 + justify-content: space-between; 84 + align-items: center; 85 + gap: calc(var(--spacing) * 0.5); 86 + padding: calc(var(--spacing) * 0.5); 87 + background-color: hsl(from var(--page-bg) h s calc(l * 1.7) / 0.7); 88 + backdrop-filter: blur(10px); 89 + border-radius: 100vw; 90 + box-shadow: 0 0 10px oklch(from var(--primary-text) l c h / 0.1); 91 + z-index: 100; 92 + 93 + & > a { 94 + display: flex; 95 + flex-direction: column; 96 + align-items: center; 97 + justify-content: center; 98 + border-radius: 100vw; 99 + font-size: 0.7rem; 100 + 101 + --size: 1.6rem; 102 + 103 + img, 104 + svg { 105 + border-radius: 100vw; 106 + } 107 + 108 + svg { 109 + width: var(--size); 110 + height: var(--size); 111 + padding: 0.1rem; 112 + } 113 + } 114 + } 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 - color: oklch(from var(--primary-text) l c h / 0.5); 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 + 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 + import { useQuery } from "@tanstack/solid-query" 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 - export const client = getClient(clientID, issuerUrl) 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 - const verified = await client.verify(subjects, accessToken.value!, { 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 + 59 + export function useMe() { 60 + return useQuery(() => ({ 61 + queryKey: [client.api.v1.account.me.$url().pathname], 62 + queryFn: async () => { 63 + const res = await client.api.v1.account.me.$get() 64 + if (!res.ok) { 65 + throw new Error("Not authenticated") 66 + } 67 + return await res.json() 68 + }, 69 + retry: false, 70 + })) 71 + }