Read-it-later social network

add /handle and /handle/pub pages, update mobile styles

+479 -74
+4 -30
src/lib/components/PublicationCard.svelte
··· 2 2 import { getContext } from "svelte"; 3 3 import { createQuery } from "@tanstack/svelte-query"; 4 4 import type { QuicksliceClient } from "quickslice-client-js"; 5 - import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode, type SubscriptionNode } from "$lib/utils"; 5 + import { defaultTheme, parseAtUri, resolveHandle, type MiniDoc, type PublicationNode, type SubscriptionNode } from "$lib/utils"; 6 6 7 7 const user = getContext("user") as MiniDoc; 8 8 const atclient = getContext("atclient") as QuicksliceClient; ··· 52 52 let subscribers = $derived(countQuery.data?.subscribers || 0); 53 53 let subscriptionRkey = $derived(parseAtUri(publication.viewerSiteStandardGraphSubscriptionViaPublication?.uri || "").rkey); 54 54 let blobSyncUrl = $derived(`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.icon?.ref}`); 55 - const theme = publication.basicTheme || { 56 - $type: "site.standard.theme.basic", 57 - background: { 58 - $type: "site.standard.theme.color#rgb", 59 - b: 255, 60 - g: 255, 61 - r: 255 62 - }, 63 - accentForeground: { 64 - $type: "site.standard.theme.color#rgb", 65 - b: 0, 66 - g: 0, 67 - r: 0, 68 - }, 69 - foreground: { 70 - $type: "site.standard.theme.color#rgb", 71 - b: 0, 72 - g: 0, 73 - r: 0 74 - }, 75 - accent: { 76 - $type: "site.standard.theme.color#rgb", 77 - b: 36, 78 - g: 191, 79 - r: 251 80 - }, 81 - }; 55 + const theme = publication.basicTheme || defaultTheme; 82 56 83 57 async function toggleSubscribe() { 84 58 if (!user) { throw Error() } ··· 124 98 disableSubscribeButton = false; 125 99 } 126 100 catch (e) { 127 - console.log(e); 101 + console.error(e); 128 102 // rollback initial changes 129 103 if (pastRkey) { 130 104 subscribers++; ··· 159 133 {publication.name} 160 134 </h3> 161 135 <a 162 - href={`https://bsky.app/profile/${publication.actorHandle}`} 136 + href={`/${publication.actorHandle}`} 163 137 style={` 164 138 color: rgb(${theme.foreground.r},${theme.foreground.g},${theme.foreground.b}); 165 139 `}
+28
src/lib/utils.ts
··· 85 85 tags?: string[]; 86 86 updatedAt?: string; 87 87 }} 88 + 89 + export const defaultTheme = { 90 + $type: "site.standard.theme.basic", 91 + background: { 92 + $type: "site.standard.theme.color#rgb", 93 + b: 255, 94 + g: 255, 95 + r: 255 96 + }, 97 + accentForeground: { 98 + $type: "site.standard.theme.color#rgb", 99 + b: 0, 100 + g: 0, 101 + r: 0, 102 + }, 103 + foreground: { 104 + $type: "site.standard.theme.color#rgb", 105 + b: 0, 106 + g: 0, 107 + r: 0 108 + }, 109 + accent: { 110 + $type: "site.standard.theme.color#rgb", 111 + b: 36, 112 + g: 191, 113 + r: 251 114 + }, 115 + }
+12 -4
src/routes/+layout.svelte
··· 41 41 42 42 <div class="flex gap-4 items-center flex-wrap"> 43 43 <nav class="flex gap-4 flex-wrap items-center px-3 py-1.5"> 44 - <a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ Explore</a> 44 + <a 45 + href="/explore" 46 + title="Explore" 47 + aria-label="Explore" 48 + class="bg-amber-400 w-full lg:w-fit text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 49 + > 50 + 🛰️ Explore 51 + </a> 45 52 {#if user} 46 - <a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 Home</a> 53 + <a href="/home" class="hover:text-shadow-sm" title="Home" aria-label="Home">🏠 Home</a> 47 54 {/if} 48 55 </nav> 49 56 {#if user} ··· 53 60 {:else} 54 61 <input 55 62 type="text" 63 + name="handle" 56 64 bind:value={handleInput} 57 - placeholder="Handle (eg: zeu.dev)" 58 - class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 65 + placeholder="alice.bsky.social" 66 + class="border border-yellow-400 text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 59 67 /> 60 68 <button onclick={login} class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 61 69 Login
+44 -38
src/routes/+page.svelte
··· 16 16 } 17 17 </script> 18 18 19 - <section class="flex flex-col gap-4 my-8"> 20 - <h2 class="text-amber-400 text-2xl font-bold font-neco">Talk about what everyone's reading today</h2> 21 - <p><span class="font-neco text-lg font-bold">potatonet</span> lets you to follow, read, and discuss to fellow readers about long-form content on the internet as easy as pie.</p> 22 - </section> 23 - 24 - <section class="flex flex-col gap-4 my-8"> 25 - <h2 class="text-amber-400 text-2xl font-bold font-neco">Subscribe to any publication across the Atmosphere</h2> 26 - <p>With the AT Protocol, you can read any blog made, whether it's from here, their personal website, or any platform in the network.</p> 27 - <menu class="flex flex-col gap-2 py-4 justify-center"> 28 - <p class="italic opacity-80">Supported platforms include:</p> 29 - <div class="flex gap-2 items-center"> 30 - <a href="https://leaflet.pub" class="group transition-all duration-150 border border-white/60 hover:border-amber-400 px-8 py-6 rounded"> 31 - <LeafletIcon class="w-32 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 32 - </a> 33 - <a href="https://pckt.blog" class="px-10 py-6 rounded group transition-all duration-150 border border-white/60 hover:border-amber-400"> 34 - <PcktIcon class="w-20 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 35 - </a> 36 - <a href="https://offprint.app" class="px-8 py-6 rounded group transition-all duration-150 border border-white/60 hover:border-amber-400"> 37 - <OffprintIcon class="w-30 h-8.5 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 38 - </a> 39 - </div> 40 - </menu> 41 - </section> 42 - 43 - <section class="flex flex-col gap-4 my-8"> 44 - <h2 class="text-amber-400 text-2xl font-bold font-neco">Coming soon: RSS Feeds</h2> 45 - <p>We plan to support RSS feeds, used by news organizations and independent writers online for decades, allowing you to interact with any writing available on the internet.</p> 46 - </section> 47 - 48 19 <section class="flex flex-col gap-4 my-8 items-center justify-center"> 49 20 <h2 class="text-center text-amber-400 text-3xl font-bold font-neco">Find your next read on potatonet</h2> 50 - <div class="flex gap-4"> 51 - <a href="/explore" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🛰️ Explore</a> 21 + <div class="flex flex-col lg:flex-row gap-4"> 52 22 {#if user} 53 23 <a href="/home" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🏠 Home</a> 54 24 {:else} 55 - <input 56 - type="text" 57 - bind:value={handleInput} 58 - placeholder="Handle (eg: zeu.dev)" 59 - class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 60 - /> 25 + <label for="handle"> 26 + Handle 27 + <input 28 + type="text" 29 + name="handle" 30 + bind:value={handleInput} 31 + placeholder="alice.bsky.social" 32 + class="border border-yellow-400 text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 33 + /> 34 + </label> 61 35 <button onclick={login} class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 62 36 Login 63 37 </button> 64 38 {/if} 39 + <a href="/explore" class="text-center bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 40 + 🛰️ Explore 41 + </a> 65 42 </div> 66 - <pre class="text-xs tracking-widest"> 43 + <pre class="text-xs tracking-tight"> 67 44 68 45 69 46 ··· 106 83 107 84 </pre> 108 85 </section> 86 + 87 + <section class="flex flex-col gap-4 my-8"> 88 + <h2 class="text-amber-400 text-2xl font-bold font-neco">Talk about what everyone's reading today</h2> 89 + <p><span class="font-neco text-lg font-bold">potatonet</span> lets you to follow, read, and discuss to fellow readers about long-form content on the internet as easy as pie.</p> 90 + </section> 91 + 92 + <section class="flex flex-col gap-4 my-8"> 93 + <h2 class="text-amber-400 text-2xl font-bold font-neco">Subscribe to any publication across the Atmosphere</h2> 94 + <p>With the AT Protocol, you can read any blog made, whether it's from here, their personal website, or any platform in the network.</p> 95 + <menu class="flex flex-col gap-2 py-4 justify-center"> 96 + <p class="italic opacity-80 text-center lg:text-start">Supported platforms include:</p> 97 + <div class="flex flex-col lg:flex-row gap-2 items-center"> 98 + <a href="https://leaflet.pub" class="group transition-all duration-150 border border-white/60 hover:border-amber-400 px-8 py-6 rounded"> 99 + <LeafletIcon class="w-32 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 100 + </a> 101 + <a href="https://pckt.blog" class="px-10 py-6 rounded group transition-all duration-150 border border-white/60 hover:border-amber-400"> 102 + <PcktIcon class="w-20 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 103 + </a> 104 + <a href="https://offprint.app" class="px-8 py-6 rounded group transition-all duration-150 border border-white/60 hover:border-amber-400"> 105 + <OffprintIcon class="w-30 h-8.5 text-white/80 group-hover:text-amber-400 transition-colors duration-150" /> 106 + </a> 107 + </div> 108 + </menu> 109 + </section> 110 + 111 + <section class="flex flex-col gap-4 my-8"> 112 + <h2 class="text-amber-400 text-2xl font-bold font-neco">Coming soon: RSS Feeds</h2> 113 + <p>We plan to support RSS feeds, used by news organizations and independent writers online for decades, allowing you to interact with any writing available on the internet.</p> 114 + </section>
+7
src/routes/[handle]/+layout.ts
··· 1 + import { resolveHandle } from "$lib/utils"; 2 + import type { PageLoadEvent } from "./$types"; 3 + 4 + export async function load({ params }: PageLoadEvent) { 5 + const info = await resolveHandle(params.handle); 6 + return { author: info } 7 + }
+164
src/routes/[handle]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { Debounced } from "runed"; 3 + import type { PublicationNode, SubscriptionNode } from '$lib/utils'; 4 + import { createInfiniteQuery } from '@tanstack/svelte-query'; 5 + import PublicationCard from '$lib/components/PublicationCard.svelte'; 6 + 7 + let { data } = $props(); 8 + let { atclient, user, author } = data; 9 + 10 + let page = $state(0); 11 + let searchTerm = $state(""); 12 + const debouncedSearchTerm = new Debounced(() => searchTerm, 500); 13 + 14 + const publicationsQuery = createInfiniteQuery(() => ({ 15 + queryKey: ["publications", debouncedSearchTerm.current || undefined, author.handle], 16 + queryFn: async ({ pageParam }) => { 17 + const query = ` 18 + query GetPublications { 19 + siteStandardPublication(first: 20, after: "${pageParam}", where: { 20 + and: [{ 21 + name: { contains: "${debouncedSearchTerm.current || ""}" } 22 + }, { 23 + actorHandle: { eq: "${author.handle}" } 24 + }] 25 + }) { 26 + edges { 27 + node { 28 + viewerSiteStandardGraphSubscriptionViaPublication {} 29 + uri 30 + indexedAt 31 + cid 32 + did 33 + url 34 + name 35 + description 36 + icon {} 37 + actorHandle 38 + preferences { 39 + showInDiscover 40 + } 41 + basicTheme {} 42 + } 43 + } 44 + pageInfo { 45 + hasNextPage 46 + endCursor 47 + } 48 + } 49 + } 50 + `; 51 + 52 + let data; 53 + if (user) { 54 + data = await atclient.query(query); 55 + } 56 + else { 57 + data = await atclient.publicQuery(query); 58 + } 59 + return data as { 60 + siteStandardPublication: { 61 + edges: { 62 + node: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication: SubscriptionNode | null}, 63 + cursor: string 64 + }[], 65 + pageInfo: { 66 + hasNextPage: boolean; 67 + endCursor: string; 68 + } 69 + } 70 + } 71 + }, 72 + initialPageParam: "", 73 + getNextPageParam: (lastPage) => lastPage.siteStandardPublication.pageInfo.endCursor, 74 + select: (data) => { 75 + const items = data.pages.map((page) => page.siteStandardPublication.edges).flat(); 76 + const nodes = items.map((i) => i.node); 77 + return nodes; 78 + } 79 + })); 80 + 81 + let currentPage = $derived(publicationsQuery.data?.slice(page*20, (page*20) + 20)); 82 + </script> 83 + 84 + <header class="flex flex-col gap-2 lg:mb-8"> 85 + <h1 class="text-2xl lg:text-4xl font-bold text-yellow-400">Publications by {author.handle}</h1> 86 + </header> 87 + 88 + <menu class="flex flex-col lg:flex-row gap-4 w-full justify-between items-center"> 89 + <label> 90 + Search: 91 + <input bind:value={searchTerm} class="border px-3 py-2" /> 92 + </label> 93 + 94 + <div class=""> 95 + {#if page > 0} 96 + <button 97 + onclick={() => { 98 + if (page > 0) { 99 + page--; 100 + } 101 + }} 102 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 103 + > 104 + Previous 105 + </button> 106 + {/if} 107 + <number class="px-3">Page {page + 1}</number> 108 + {#if publicationsQuery.hasNextPage} 109 + <button 110 + onclick={() => { 111 + page++; 112 + if ((page * 20) + 20 > (publicationsQuery.data?.length || 0)) { 113 + publicationsQuery.fetchNextPage(); 114 + } 115 + }} 116 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 117 + > 118 + Next 119 + </button> 120 + {/if} 121 + </div> 122 + </menu> 123 + 124 + {#if publicationsQuery.isFetching} 125 + <p>Fetching...</p> 126 + {:else if publicationsQuery.isError} 127 + <p>Error</p> 128 + {:else if publicationsQuery.isSuccess} 129 + {#if currentPage?.length === 0} 130 + There are no publications based onb the current filters 131 + {/if} 132 + {#each currentPage as publication, i (i)} 133 + <PublicationCard {publication} /> 134 + {/each} 135 + {/if} 136 + 137 + <menu class="self-center lg:self-end"> 138 + {#if page > 0} 139 + <button 140 + onclick={() => { 141 + if (page > 0) { 142 + page--; 143 + } 144 + }} 145 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 146 + > 147 + Previous 148 + </button> 149 + {/if} 150 + <number class="px-3">Page {page + 1}</number> 151 + {#if publicationsQuery.hasNextPage} 152 + <button 153 + onclick={() => { 154 + page++; 155 + if ((page * 20) + 20 > (publicationsQuery.data?.length || 0)) { 156 + publicationsQuery.fetchNextPage(); 157 + } 158 + }} 159 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 160 + > 161 + Next 162 + </button> 163 + {/if} 164 + </menu>
+213
src/routes/[handle]/[pubRkey]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { Debounced } from "runed"; 3 + import { page as pageState } from "$app/state"; 4 + import { getContext } from "svelte"; 5 + import { defaultTheme, type DocumentNode, type MiniDoc, type PublicationNode } from "$lib/utils"; 6 + import type { QuicksliceClient } from "quickslice-client-js"; 7 + import { createInfiniteQuery, createQuery } from "@tanstack/svelte-query"; 8 + 9 + const { data } = $props(); 10 + const { author }: { author: MiniDoc } = data; 11 + const user = getContext("user") as MiniDoc; 12 + const atclient = getContext("atclient") as QuicksliceClient; 13 + 14 + let page = $state(0); 15 + let sortDirection = $state("DESC"); 16 + let searchTerm = $state(""); 17 + const debouncedSearchTerm = new Debounced(() => searchTerm, 500); 18 + 19 + function toggleSort() { 20 + sortDirection = sortDirection === "DESC" ? "ASC" : "DESC"; 21 + } 22 + 23 + const publicationQuery = createQuery(() => ({ 24 + queryKey: ["publication", author.did, pageState.params.pubRkey], 25 + queryFn: async () => { 26 + const query = ` 27 + query GetPublication { 28 + siteStandardPublication(where: { 29 + uri: { eq: "at://${author.did}/site.standard.publication/${pageState.params.pubRkey}" } 30 + }) { 31 + edges { node {}} 32 + } 33 + } 34 + `; 35 + const data = await atclient.publicQuery(query) as { 36 + siteStandardPublication: { 37 + edges: { 38 + node: Node & { value: PublicationNode } 39 + }[] 40 + } 41 + }; 42 + 43 + return data.siteStandardPublication.edges[0].node; 44 + } 45 + })); 46 + 47 + const documentsQuery = createInfiniteQuery(() => ({ 48 + queryKey: ["documents", author.did, pageState.params.pubRkey, sortDirection], 49 + queryFn: async ({ pageParam }) => { 50 + const query = ` 51 + query GetDocuments { 52 + siteStandardDocument( 53 + first: 20, 54 + after: "${pageParam}", 55 + where: { 56 + and: [{ 57 + title: { contains: "${debouncedSearchTerm.current || ""}" } 58 + }, { 59 + site: { eq: "at://${author.did}/site.standard.publication/${pageState.params.pubRkey}" } 60 + }] 61 + }, 62 + sortBy: [{ 63 + field: "publishedAt", 64 + direction: "${sortDirection}" 65 + }] 66 + ) { 67 + edges { 68 + node {} 69 + } 70 + pageInfo { 71 + hasNextPage 72 + endCursor 73 + } 74 + } 75 + } 76 + `; 77 + 78 + const data = await atclient.publicQuery(query); 79 + 80 + return data as { 81 + siteStandardDocument: { 82 + edges: { 83 + node: DocumentNode; 84 + cursor: string 85 + }[], 86 + pageInfo: { 87 + hasNextPage: boolean; 88 + endCursor: string; 89 + } 90 + } 91 + } 92 + }, 93 + initialPageParam: "", 94 + getNextPageParam: (lastPage) => lastPage.siteStandardDocument.pageInfo.endCursor, 95 + select: (data) => { 96 + const items = data.pages.map((page) => page.siteStandardDocument.edges).flat(); 97 + const nodes = items.map((i) => i.node); 98 + return nodes; 99 + }, 100 + })); 101 + 102 + let publication = $derived(publicationQuery.data?.value); 103 + let theme = $derived(publication?.basicTheme || defaultTheme); 104 + let currentPage = $derived(documentsQuery.data?.slice(page*20, (page*20) + 20)); 105 + 106 + </script> 107 + 108 + <!-- TODO: create DocumentCard component with likes & bookmarks --> 109 + {#snippet documentCard(doc: DocumentNode)} 110 + <article 111 + class="flex flex-col gap-2 border px-5 py-4 rounded" 112 + style={` 113 + background-color: rgb(${theme.background.r},${theme.background.g},${theme.background.b}); 114 + color: rgb(${theme.foreground.r},${theme.foreground.g},${theme.foreground.b}); 115 + `} 116 + > 117 + <a href={`${publication?.url}${doc.value.path}`}> 118 + <h1 class="text-lg font-bold hover:underline"> 119 + {doc.value.title} 120 + </h1> 121 + </a> 122 + <time class="opacity-80">{new Date(doc.value.publishedAt).toLocaleDateString()}</time> 123 + <p class="font-neco">{doc.value.description}</p> 124 + </article> 125 + {/snippet} 126 + 127 + <header class="flex flex-col gap-2 lg:mb-8"> 128 + <h1 class="text-2xl lg:text-4xl font-bold text-yellow-400">{publication?.name}</h1> 129 + <h2 class="font-neco lg:text-xl">{publication?.description}</h2> 130 + <h3>Author: <a href={`/${author.handle}`} class="hover:underline hover:text-yellow-400">@{author.handle}</a></h3> 131 + </header> 132 + 133 + <menu class="flex flex-col lg:flex-row gap-4 w-full justify-between items-center"> 134 + <label> 135 + Search: 136 + <input bind:value={searchTerm} class="border px-3 py-2" /> 137 + </label> 138 + 139 + <button onclick={toggleSort} class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 140 + <span class="text-xl">⇅</span> Sort by: {#if sortDirection === "DESC"}Newest{:else}Oldest{/if} 141 + </button> 142 + 143 + <div class=""> 144 + {#if page > 0} 145 + <button 146 + onclick={() => { 147 + if (page > 0) { 148 + page--; 149 + } 150 + }} 151 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 152 + > 153 + Previous 154 + </button> 155 + {/if} 156 + <number class="px-3">Page {page + 1}</number> 157 + {#if documentsQuery.hasNextPage} 158 + <button 159 + onclick={() => { 160 + page++; 161 + if ((page * 20) + 20 > (documentsQuery.data?.length || 0)) { 162 + documentsQuery.fetchNextPage(); 163 + } 164 + }} 165 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 166 + > 167 + Next 168 + </button> 169 + {/if} 170 + </div> 171 + </menu> 172 + 173 + {#if documentsQuery.isFetching} 174 + <p>Fetching...</p> 175 + {:else if documentsQuery.isError} 176 + <p>Error</p> 177 + {:else if documentsQuery.isSuccess} 178 + {#if currentPage?.length === 0} 179 + There are no documents based onb the current filters 180 + {/if} 181 + {#each currentPage as document, i (i)} 182 + {@render documentCard(document)} 183 + {/each} 184 + {/if} 185 + 186 + <menu class="self-end"> 187 + {#if page > 0} 188 + <button 189 + onclick={() => { 190 + if (page > 0) { 191 + page--; 192 + } 193 + }} 194 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 195 + > 196 + Previous 197 + </button> 198 + {/if} 199 + <number class="px-3">Page {page + 1}</number> 200 + {#if documentsQuery.hasNextPage} 201 + <button 202 + onclick={() => { 203 + page++; 204 + if ((page * 20) + 20 > (documentsQuery.data?.length || 0)) { 205 + documentsQuery.fetchNextPage(); 206 + } 207 + }} 208 + class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 209 + > 210 + Next 211 + </button> 212 + {/if} 213 + </menu>
src/routes/[handle]/[pubRkey]/+page.ts

This is a binary file and will not be displayed.

+7 -2
src/routes/explore/+page.svelte
··· 80 80 let currentPage = $derived(publicationsQuery.data?.slice(page*20, (page*20) + 20)); 81 81 </script> 82 82 83 - <menu class="flex w-full justify-between items-center"> 83 + <header class="flex flex-col gap-2 lg:mb-8"> 84 + <h1 class="text-2xl lg:text-4xl font-bold text-yellow-400">Explore</h1> 85 + <h2 class="font-neco lg:text-xl">Read from authors across the atmosphere</h2> 86 + </header> 87 + 88 + <menu class="flex flex-col lg:flex-row gap-4 w-full justify-between items-center"> 84 89 <label> 85 90 Search: 86 91 <input bind:value={searchTerm} class="border px-3 py-2" /> ··· 129 134 {/each} 130 135 {/if} 131 136 132 - <menu class="self-end"> 137 + <menu class="self-center lg:self-end"> 133 138 {#if page > 0} 134 139 <button 135 140 onclick={() => {