a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add proper reader experience and rework nav (#269)

* moved around some stuff for our navigation

* little tweak

* messing around with navigation on mobile

* Added a page title to the home layout

* added pagetitle to looseleaf and pub, added shortcut if you only have
one pub or looseleaf

* some icons

* quick little adjustments

* moved action buttons on mobile to page title area, publist commented out
for now

* added the publicaions nav to the mobile footer

* add states for logged out

* stray console log eradication

* some shifting around of things

* update the homeempty layout

* move publicationbuttons into separate compoennt, added state to handle
jsut one pub or looseleaf

* rename small prop in PubIcon to tiny

* adding tabs to Reader

* oops forgot the new files

* updated postListing design

* add a drawer for comments and mentions in reader

* new files

* remove features not in scope

* adjust width of ActionButon in desktop

* added indication that post is on non-leaflet page is applicable

* repostion interaction drawer close button

* small fixes per feedback

* add inngest function to track bsky post likes

* don't sync bsky likes of brid.gy posts

* add bsky auth view all permissions

* add aud to bsky view permissions

* add recommend notification

* fix post links to standard site blogs

* don't getpreferences on subscribe

* add recommend column to documents

* check permissions on retractions

* don't show block options if no write permissions

* handle pasting bluesky posts

* fix: copy post link button

* add indexed col to documents and update inngest

* fix type error

* add hot feed

* adding profile button to nav, removing things where necessary

* Remove accidentally committed files from bbc6c135

Remove scratch/, scripts/, database.sql, appview/test.ts,
CoordDebugger.tsx, search_loose_leafs.ts, supabase migration,
and tmp_standard-site-migration.md that were unintentionally
included in the previous commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added profile settings to nav

* WIP suspense reader pages

* add loading.tsx for route level suspense boundary

* render what's hot for logged out users and don't render subs

* highlight what's hot tab if default

* persist nav state on reader/home

* remove .claude/settings.local.json

* wire up interactions

* remove publications and fix external url logic

* don't keep previous data in interactions sidebar in feed

* fix empty subs state

* fix share links in postlisting

* remove help text

* fix cover image positioning

---------

Co-authored-by: celine <celine@hyperlink.academy>
Co-authored-by: Henrique Dias <mail@hacdias.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

Jared Pereira
Claude Opus 4.6
celine
Henrique Dias
and committed by
GitHub
10133c25 19c7b0af

+2070 -1382
+18 -12
app/(home-pages)/discover/PubListing.tsx app/(home-pages)/p/[didOrHandle]/PubListing.tsx
··· 1 1 "use client"; 2 2 import { AtUri } from "@atproto/syntax"; 3 3 import { PublicationSubscription } from "app/(home-pages)/reader/getSubscriptions"; 4 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 4 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 + import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; 5 6 import { PubIcon } from "components/ActionBar/Publications"; 6 7 import { Separator } from "components/Layout"; 7 8 import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; ··· 9 10 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 10 11 import { timeAgo } from "src/utils/timeAgo"; 11 12 12 - export const PubListing = ( 13 - props: PublicationSubscription & { 14 - resizeHeight?: boolean; 15 - }, 16 - ) => { 13 + export const PubListing = (props: PublicationSubscription) => { 17 14 let record = props.record; 18 15 let theme = usePubTheme(record?.theme); 19 16 let backgroundImage = record?.theme?.backgroundImage?.image?.ref ··· 28 25 if (!record) return null; 29 26 return ( 30 27 <BaseThemeProvider {...theme} local> 31 - <a 32 - href={record.url} 28 + <div 33 29 className={`no-underline! flex flex-row gap-2 34 30 bg-bg-leaflet 35 31 border border-border-light rounded-lg ··· 42 38 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 43 39 }} 44 40 > 41 + <a href={record.url} className="absolute inset-0" /> 45 42 <div 46 - className={`flex w-full flex-col justify-center text-center max-h-48 pt-4 pb-3 px-3 rounded-lg relative z-10 ${props.resizeHeight ? "" : "sm:h-48 h-full"} ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))] " : ""}`} 43 + className={`flex w-full flex-col justify-center text-center pt-4 pb-3 px-3 rounded-lg relative z-10 sm:h-[200px] h-full ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))] " : ""}`} 47 44 > 48 45 <div className="mx-auto pb-1"> 49 46 <PubIcon record={record} uri={props.uri} large /> ··· 51 48 52 49 <h4 className="truncate shrink-0 ">{record.name}</h4> 53 50 {record.description && ( 54 - <p className="text-secondary text-sm max-h-full overflow-hidden pb-1"> 51 + <p className="text-secondary line-clamp-1 min-h-[16px] text-sm overflow-hidden "> 55 52 {record.description} 56 53 </p> 57 54 )} 58 - <div className="flex flex-col items-center justify-center text-xs text-tertiary pt-2"> 55 + <div className="flex flex-col items-center justify-center text-xs text-tertiary pt-1"> 59 56 <div className="flex flex-row gap-2 items-center"> 60 57 {props.authorProfile?.handle} 61 58 </div> ··· 67 64 )} 68 65 </p> 69 66 </div> 67 + <div className="w-fit mx-auto mt-3 grow items-end flex"> 68 + <SubscribeWithBluesky 69 + compact 70 + pub_uri={props.uri} 71 + pubName={props.record.name} 72 + subscribers={props.publication_subscriptions || []} 73 + base_url={getPublicationURL({ ...props })} 74 + /> 75 + </div> 70 76 </div> 71 - </a> 77 + </div> 72 78 </BaseThemeProvider> 73 79 ); 74 80 };
-97
app/(home-pages)/discover/SortButtons.tsx
··· 1 - "use client"; 2 - import Link from "next/link"; 3 - import { useState } from "react"; 4 - import { theme } from "tailwind.config"; 5 - 6 - export default function SortButtons(props: { order: string }) { 7 - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( 8 - "recentlyUpdated", 9 - ); 10 - 11 - return ( 12 - <div className="flex gap-2 pt-1"> 13 - <Link href="?order=recentlyUpdated"> 14 - <SortButton selected={props.order === "recentlyUpdated"}> 15 - Recently Updated 16 - </SortButton> 17 - </Link> 18 - 19 - <Link href="?order=popular"> 20 - <SortButton selected={props.order === "popular"}>Popular</SortButton> 21 - </Link> 22 - </div> 23 - ); 24 - } 25 - 26 - const SortButton = (props: { 27 - children: React.ReactNode; 28 - selected: boolean; 29 - }) => { 30 - return ( 31 - <div className="relative"> 32 - <button 33 - style={ 34 - props.selected 35 - ? { backgroundColor: `rgba(var(--accent-1), 0.2)` } 36 - : {} 37 - } 38 - className={`text-sm rounded-md px-[8px] py-0.5 border ${props.selected ? "border-accent-contrast text-accent-1 font-bold" : "text-tertiary border-border-light"}`} 39 - > 40 - {props.children} 41 - </button> 42 - {props.selected && ( 43 - <> 44 - <div className="absolute top-0 -left-2"> 45 - <GlitterBig /> 46 - </div> 47 - <div className="absolute top-4 left-0"> 48 - <GlitterSmall /> 49 - </div> 50 - <div className="absolute -top-2 -right-1"> 51 - <GlitterSmall /> 52 - </div> 53 - </> 54 - )} 55 - </div> 56 - ); 57 - }; 58 - 59 - const GlitterBig = () => { 60 - return ( 61 - <svg 62 - width="16" 63 - height="17" 64 - viewBox="0 0 16 17" 65 - fill="none" 66 - xmlns="http://www.w3.org/2000/svg" 67 - > 68 - <path 69 - d="M8.16553 0.804321C8.5961 0.804329 8.97528 1.03925 9.22803 1.40393C9.47845 1.76546 9.6128 2.25816 9.61279 2.84338C9.61279 2.98187 9.6178 3.11647 9.62646 3.2467C9.65365 3.65499 9.72104 4.02319 9.81006 4.35022C10.0833 5.35388 10.5641 5.96726 10.7349 6.14221C10.7443 6.15184 10.7543 6.16234 10.7642 6.17249C10.9808 6.39533 11.3925 6.8162 12.0142 7.09338C12.206 7.17892 12.4177 7.2502 12.6489 7.29749C12.8402 7.3366 13.0466 7.35993 13.2681 7.35999H13.269C14.2688 7.36032 14.9747 7.96603 14.9771 8.77014C14.9793 9.57755 14.272 10.1833 13.2681 10.1832C13.0278 10.1832 12.8137 10.2034 12.6226 10.2369C12.3793 10.2796 12.1697 10.3455 11.9858 10.4254C11.4714 10.6492 11.1325 10.9918 10.7935 11.3405C10.7739 11.3605 10.7544 11.381 10.7349 11.401C10.3936 11.7507 10.0271 12.1792 9.81006 12.9352C9.72175 13.2428 9.65679 13.6119 9.63135 14.0592C9.62378 14.1924 9.61963 14.3325 9.61963 14.4801C9.61963 15.5836 9.06909 16.4876 8.17822 16.4996C7.74928 16.5053 7.36767 16.2783 7.11182 15.9147C6.85918 15.5556 6.72412 15.065 6.72412 14.4801C6.72412 14.3385 6.71808 14.2015 6.70654 14.069C6.6724 13.6774 6.59177 13.324 6.48779 13.0123C6.16402 12.0419 5.61395 11.4722 5.54443 11.401C5.54371 11.4003 5.54043 11.3977 5.53467 11.3922C5.52778 11.3857 5.51839 11.3767 5.50635 11.3658C5.4823 11.3442 5.44954 11.3158 5.40869 11.2819C5.3268 11.2139 5.21473 11.1255 5.07764 11.0289C4.80173 10.8346 4.43374 10.6113 4.01611 10.443C3.82579 10.3663 3.62728 10.3019 3.42432 10.2565C3.21687 10.21 3.00599 10.1832 2.79541 10.1832C1.79834 10.1832 1.11533 9.56575 1.11865 8.76917C1.12219 7.9773 1.80451 7.36002 2.79541 7.35999C3.01821 7.35999 3.22798 7.33422 3.42432 7.29065C3.62557 7.24597 3.81426 7.18216 3.98877 7.10608C4.6567 6.81484 5.10772 6.35442 5.3042 6.15295C5.30777 6.1493 5.31147 6.14577 5.31494 6.14221C5.51076 5.94157 6.14024 5.28964 6.48584 4.26233C6.59001 3.95264 6.66793 3.60887 6.70068 3.23303C6.71166 3.10697 6.71826 2.977 6.71826 2.84338L6.72412 2.62854C6.75331 2.13723 6.88387 1.72031 7.10303 1.40393C7.35578 1.03923 7.73495 0.804326 8.16553 0.804321Z" 70 - fill={theme.colors["accent-1"]} 71 - stroke={theme.colors["bg-leaflet"]} 72 - strokeLinecap="round" 73 - strokeLinejoin="round" 74 - /> 75 - </svg> 76 - ); 77 - }; 78 - 79 - const GlitterSmall = () => { 80 - return ( 81 - <svg 82 - width="13" 83 - height="14" 84 - viewBox="0 0 13 14" 85 - fill="none" 86 - xmlns="http://www.w3.org/2000/svg" 87 - > 88 - <path 89 - d="M6.37585 1.23596C6.7489 1.23598 7.07064 1.44034 7.28015 1.7428C7.48716 2.04187 7.59266 2.4408 7.59265 2.901C7.59266 3.00294 7.59605 3.10213 7.60242 3.19788C7.62244 3.49844 7.67183 3.76938 7.73718 4.0094C7.93813 4.74731 8.29123 5.1934 8.4071 5.31213L8.57703 5.48206C8.75042 5.64731 9.00188 5.85577 9.33777 6.00549C9.47565 6.06695 9.62723 6.11812 9.79285 6.15198C9.92991 6.18 10.0779 6.1959 10.2372 6.19592C11.0418 6.19604 11.6503 6.69195 11.6522 7.38538C11.654 8.08176 11.0444 8.57683 10.2372 8.57678C10.0618 8.57678 9.90679 8.59077 9.76941 8.61487C9.59484 8.6455 9.4456 8.69297 9.31531 8.74963C8.95055 8.9083 8.70884 9.15057 8.45203 9.41467C8.43719 9.42993 8.42207 9.44621 8.4071 9.46155C8.15582 9.71904 7.89358 10.0262 7.73718 10.5709C7.67315 10.7941 7.62512 11.064 7.60632 11.3942C7.60073 11.4925 7.59754 11.5963 7.59753 11.7057C7.59753 12.5657 7.16303 13.3455 6.38757 13.3561C6.01608 13.3611 5.6911 13.1642 5.47839 12.8619C5.26902 12.5643 5.16394 12.1657 5.16394 11.7057C5.16393 11.6022 5.15871 11.5017 5.15027 11.4049C5.1253 11.1189 5.06701 10.861 4.99109 10.6334C4.75475 9.92518 4.35324 9.51044 4.30554 9.46155C4.30554 9.46155 4.27494 9.43195 4.21179 9.37952C4.15207 9.32993 4.06961 9.26511 3.96863 9.19397C3.76515 9.05064 3.49524 8.88718 3.19031 8.76428C3.05151 8.70835 2.90782 8.66129 2.7616 8.62854C2.61218 8.59509 2.4616 8.57679 2.31238 8.57678C1.50706 8.57678 0.918891 8.07006 0.921753 7.3844C0.924612 6.70329 1.51125 6.19594 2.31238 6.19592C2.47154 6.19591 2.6213 6.17821 2.7616 6.14709C2.90554 6.11514 3.04128 6.06904 3.16687 6.01428C3.64904 5.80395 3.97684 5.47074 4.1239 5.31995C4.12648 5.3173 4.12918 5.31473 4.13171 5.31213C4.27635 5.16393 4.73656 4.68608 4.98914 3.93518C5.06511 3.70931 5.12247 3.45905 5.14636 3.18518C5.15436 3.09338 5.15905 2.99838 5.15906 2.901C5.15906 2.44078 5.26453 2.04187 5.47156 1.7428C5.68108 1.44033 6.00279 1.23595 6.37585 1.23596Z" 90 - fill={theme.colors["accent-1"]} 91 - stroke={theme.colors["bg-leaflet"]} 92 - strokeLinecap="round" 93 - strokeLinejoin="round" 94 - /> 95 - </svg> 96 - ); 97 - };
-195
app/(home-pages)/discover/SortedPublicationList.tsx
··· 1 - "use client"; 2 - import Link from "next/link"; 3 - import { useState, useEffect, useRef } from "react"; 4 - import { theme } from "tailwind.config"; 5 - import { PubListing } from "./PubListing"; 6 - import useSWRInfinite from "swr/infinite"; 7 - import { getPublications, type Cursor, type Publication } from "./getPublications"; 8 - 9 - export function SortedPublicationList(props: { 10 - publications: Publication[]; 11 - order: string; 12 - nextCursor: Cursor | null; 13 - }) { 14 - let [order, setOrder] = useState(props.order); 15 - 16 - const getKey = ( 17 - pageIndex: number, 18 - previousPageData: { publications: Publication[]; nextCursor: Cursor | null } | null, 19 - ) => { 20 - // Reached the end 21 - if (previousPageData && !previousPageData.nextCursor) return null; 22 - 23 - // First page, we don't have previousPageData 24 - if (pageIndex === 0) return ["discover-publications", order, null] as const; 25 - 26 - // Add the cursor to the key 27 - return ["discover-publications", order, previousPageData?.nextCursor] as const; 28 - }; 29 - 30 - const { data, error, size, setSize, isValidating } = useSWRInfinite( 31 - getKey, 32 - ([_, orderValue, cursor]) => { 33 - const orderParam = orderValue === "popular" ? "popular" : "recentlyUpdated"; 34 - return getPublications(orderParam, cursor); 35 - }, 36 - { 37 - fallbackData: order === props.order 38 - ? [{ publications: props.publications, nextCursor: props.nextCursor }] 39 - : undefined, 40 - revalidateFirstPage: false, 41 - }, 42 - ); 43 - 44 - const loadMoreRef = useRef<HTMLDivElement>(null); 45 - 46 - // Set up intersection observer to load more when trigger element is visible 47 - useEffect(() => { 48 - const observer = new IntersectionObserver( 49 - (entries) => { 50 - if (entries[0].isIntersecting && !isValidating) { 51 - const hasMore = data && data[data.length - 1]?.nextCursor; 52 - if (hasMore) { 53 - setSize(size + 1); 54 - } 55 - } 56 - }, 57 - { threshold: 0.1 }, 58 - ); 59 - 60 - if (loadMoreRef.current) { 61 - observer.observe(loadMoreRef.current); 62 - } 63 - 64 - return () => observer.disconnect(); 65 - }, [data, size, setSize, isValidating]); 66 - 67 - const allPublications = data ? data.flatMap((page) => page.publications) : []; 68 - 69 - return ( 70 - <div className="discoverHeader flex flex-col items-center "> 71 - <SortButtons 72 - order={order} 73 - setOrder={(o) => { 74 - const url = new URL(window.location.href); 75 - url.searchParams.set("order", o); 76 - window.history.pushState({}, "", url); 77 - setOrder(o); 78 - }} 79 - /> 80 - <div className="discoverPubList flex flex-col gap-3 pt-6 w-full relative"> 81 - {allPublications.map((pub) => ( 82 - <PubListing resizeHeight key={pub.uri} {...pub} /> 83 - ))} 84 - {/* Trigger element for loading more publications */} 85 - <div 86 - ref={loadMoreRef} 87 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 88 - aria-hidden="true" 89 - /> 90 - {isValidating && ( 91 - <div className="text-center text-tertiary py-4"> 92 - Loading more publications... 93 - </div> 94 - )} 95 - </div> 96 - </div> 97 - ); 98 - } 99 - 100 - export default function SortButtons(props: { 101 - order: string; 102 - setOrder: (order: string) => void; 103 - }) { 104 - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( 105 - "recentlyUpdated", 106 - ); 107 - 108 - return ( 109 - <div className="flex gap-2 pt-1"> 110 - <SortButton 111 - selected={props.order === "recentlyUpdated"} 112 - onClick={() => props.setOrder("recentlyUpdated")} 113 - > 114 - Recently Updated 115 - </SortButton> 116 - 117 - <SortButton 118 - selected={props.order === "popular"} 119 - onClick={() => props.setOrder("popular")} 120 - > 121 - Popular 122 - </SortButton> 123 - </div> 124 - ); 125 - } 126 - 127 - const SortButton = (props: { 128 - children: React.ReactNode; 129 - onClick: () => void; 130 - selected: boolean; 131 - }) => { 132 - return ( 133 - <div className="relative"> 134 - <button 135 - onClick={props.onClick} 136 - className={`text-sm rounded-md px-[8px] font-bold py-0.5 border ${props.selected ? "border-accent-contrast bg-accent-1 text-accent-2 " : "bg-bg-page text-accent-contrast border-accent-contrast"}`} 137 - > 138 - {props.children} 139 - </button> 140 - {props.selected && ( 141 - <> 142 - <div className="absolute top-0 -left-2"> 143 - <GlitterBig /> 144 - </div> 145 - <div className="absolute top-4 left-0"> 146 - <GlitterSmall /> 147 - </div> 148 - <div className="absolute -top-2 -right-1"> 149 - <GlitterSmall /> 150 - </div> 151 - </> 152 - )} 153 - </div> 154 - ); 155 - }; 156 - 157 - const GlitterBig = () => { 158 - return ( 159 - <svg 160 - width="16" 161 - height="17" 162 - viewBox="0 0 16 17" 163 - fill="none" 164 - xmlns="http://www.w3.org/2000/svg" 165 - > 166 - <path 167 - d="M8.16553 0.804321C8.5961 0.804329 8.97528 1.03925 9.22803 1.40393C9.47845 1.76546 9.6128 2.25816 9.61279 2.84338C9.61279 2.98187 9.6178 3.11647 9.62646 3.2467C9.65365 3.65499 9.72104 4.02319 9.81006 4.35022C10.0833 5.35388 10.5641 5.96726 10.7349 6.14221C10.7443 6.15184 10.7543 6.16234 10.7642 6.17249C10.9808 6.39533 11.3925 6.8162 12.0142 7.09338C12.206 7.17892 12.4177 7.2502 12.6489 7.29749C12.8402 7.3366 13.0466 7.35993 13.2681 7.35999H13.269C14.2688 7.36032 14.9747 7.96603 14.9771 8.77014C14.9793 9.57755 14.272 10.1833 13.2681 10.1832C13.0278 10.1832 12.8137 10.2034 12.6226 10.2369C12.3793 10.2796 12.1697 10.3455 11.9858 10.4254C11.4714 10.6492 11.1325 10.9918 10.7935 11.3405C10.7739 11.3605 10.7544 11.381 10.7349 11.401C10.3936 11.7507 10.0271 12.1792 9.81006 12.9352C9.72175 13.2428 9.65679 13.6119 9.63135 14.0592C9.62378 14.1924 9.61963 14.3325 9.61963 14.4801C9.61963 15.5836 9.06909 16.4876 8.17822 16.4996C7.74928 16.5053 7.36767 16.2783 7.11182 15.9147C6.85918 15.5556 6.72412 15.065 6.72412 14.4801C6.72412 14.3385 6.71808 14.2015 6.70654 14.069C6.6724 13.6774 6.59177 13.324 6.48779 13.0123C6.16402 12.0419 5.61395 11.4722 5.54443 11.401C5.54371 11.4003 5.54043 11.3977 5.53467 11.3922C5.52778 11.3857 5.51839 11.3767 5.50635 11.3658C5.4823 11.3442 5.44954 11.3158 5.40869 11.2819C5.3268 11.2139 5.21473 11.1255 5.07764 11.0289C4.80173 10.8346 4.43374 10.6113 4.01611 10.443C3.82579 10.3663 3.62728 10.3019 3.42432 10.2565C3.21687 10.21 3.00599 10.1832 2.79541 10.1832C1.79834 10.1832 1.11533 9.56575 1.11865 8.76917C1.12219 7.9773 1.80451 7.36002 2.79541 7.35999C3.01821 7.35999 3.22798 7.33422 3.42432 7.29065C3.62557 7.24597 3.81426 7.18216 3.98877 7.10608C4.6567 6.81484 5.10772 6.35442 5.3042 6.15295C5.30777 6.1493 5.31147 6.14577 5.31494 6.14221C5.51076 5.94157 6.14024 5.28964 6.48584 4.26233C6.59001 3.95264 6.66793 3.60887 6.70068 3.23303C6.71166 3.10697 6.71826 2.977 6.71826 2.84338L6.72412 2.62854C6.75331 2.13723 6.88387 1.72031 7.10303 1.40393C7.35578 1.03923 7.73495 0.804326 8.16553 0.804321Z" 168 - fill={theme.colors["accent-1"]} 169 - stroke={theme.colors["bg-leaflet"]} 170 - strokeLinecap="round" 171 - strokeLinejoin="round" 172 - /> 173 - </svg> 174 - ); 175 - }; 176 - 177 - const GlitterSmall = () => { 178 - return ( 179 - <svg 180 - width="13" 181 - height="14" 182 - viewBox="0 0 13 14" 183 - fill="none" 184 - xmlns="http://www.w3.org/2000/svg" 185 - > 186 - <path 187 - d="M6.37585 1.23596C6.7489 1.23598 7.07064 1.44034 7.28015 1.7428C7.48716 2.04187 7.59266 2.4408 7.59265 2.901C7.59266 3.00294 7.59605 3.10213 7.60242 3.19788C7.62244 3.49844 7.67183 3.76938 7.73718 4.0094C7.93813 4.74731 8.29123 5.1934 8.4071 5.31213L8.57703 5.48206C8.75042 5.64731 9.00188 5.85577 9.33777 6.00549C9.47565 6.06695 9.62723 6.11812 9.79285 6.15198C9.92991 6.18 10.0779 6.1959 10.2372 6.19592C11.0418 6.19604 11.6503 6.69195 11.6522 7.38538C11.654 8.08176 11.0444 8.57683 10.2372 8.57678C10.0618 8.57678 9.90679 8.59077 9.76941 8.61487C9.59484 8.6455 9.4456 8.69297 9.31531 8.74963C8.95055 8.9083 8.70884 9.15057 8.45203 9.41467C8.43719 9.42993 8.42207 9.44621 8.4071 9.46155C8.15582 9.71904 7.89358 10.0262 7.73718 10.5709C7.67315 10.7941 7.62512 11.064 7.60632 11.3942C7.60073 11.4925 7.59754 11.5963 7.59753 11.7057C7.59753 12.5657 7.16303 13.3455 6.38757 13.3561C6.01608 13.3611 5.6911 13.1642 5.47839 12.8619C5.26902 12.5643 5.16394 12.1657 5.16394 11.7057C5.16393 11.6022 5.15871 11.5017 5.15027 11.4049C5.1253 11.1189 5.06701 10.861 4.99109 10.6334C4.75475 9.92518 4.35324 9.51044 4.30554 9.46155C4.30554 9.46155 4.27494 9.43195 4.21179 9.37952C4.15207 9.32993 4.06961 9.26511 3.96863 9.19397C3.76515 9.05064 3.49524 8.88718 3.19031 8.76428C3.05151 8.70835 2.90782 8.66129 2.7616 8.62854C2.61218 8.59509 2.4616 8.57679 2.31238 8.57678C1.50706 8.57678 0.918891 8.07006 0.921753 7.3844C0.924612 6.70329 1.51125 6.19594 2.31238 6.19592C2.47154 6.19591 2.6213 6.17821 2.7616 6.14709C2.90554 6.11514 3.04128 6.06904 3.16687 6.01428C3.64904 5.80395 3.97684 5.47074 4.1239 5.31995C4.12648 5.3173 4.12918 5.31473 4.13171 5.31213C4.27635 5.16393 4.73656 4.68608 4.98914 3.93518C5.06511 3.70931 5.12247 3.45905 5.14636 3.18518C5.15436 3.09338 5.15905 2.99838 5.15906 2.901C5.15906 2.44078 5.26453 2.04187 5.47156 1.7428C5.68108 1.44033 6.00279 1.23595 6.37585 1.23596Z" 188 - fill={theme.colors["accent-1"]} 189 - stroke={theme.colors["bg-leaflet"]} 190 - strokeLinecap="round" 191 - strokeLinejoin="round" 192 - /> 193 - </svg> 194 - ); 195 - };
-133
app/(home-pages)/discover/getPublications.ts
··· 1 - "use server"; 2 - 3 - import { supabaseServerClient } from "supabase/serverClient"; 4 - import { 5 - normalizePublicationRow, 6 - hasValidPublication, 7 - } from "src/utils/normalizeRecords"; 8 - import { deduplicateByUri } from "src/utils/deduplicateRecords"; 9 - 10 - export type Cursor = { 11 - sort_date?: string; 12 - count?: number; 13 - uri: string; 14 - }; 15 - 16 - export type Publication = Awaited< 17 - ReturnType<typeof getPublications> 18 - >["publications"][number]; 19 - 20 - export async function getPublications( 21 - order: "recentlyUpdated" | "popular" = "recentlyUpdated", 22 - cursor?: Cursor | null, 23 - ): Promise<{ publications: any[]; nextCursor: Cursor | null }> { 24 - const limit = 25; 25 - 26 - // Fetch all publications with their most recent document 27 - let { data: publications, error } = await supabaseServerClient 28 - .from("publications") 29 - .select( 30 - "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", 31 - ) 32 - .or( 33 - "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", 34 - ) 35 - .order("documents(sort_date)", { 36 - referencedTable: "documents_in_publications", 37 - ascending: false, 38 - }) 39 - .limit(1, { referencedTable: "documents_in_publications" }); 40 - 41 - if (error) { 42 - console.error("Error fetching publications:", error); 43 - return { publications: [], nextCursor: null }; 44 - } 45 - 46 - // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces 47 - const dedupedPublications = deduplicateByUri(publications || []); 48 - 49 - // Filter out publications without documents 50 - const allPubs = dedupedPublications.filter( 51 - (pub) => pub.documents_in_publications.length > 0, 52 - ); 53 - 54 - // Sort on the server 55 - allPubs.sort((a, b) => { 56 - if (order === "popular") { 57 - const aCount = a.publication_subscriptions[0]?.count || 0; 58 - const bCount = b.publication_subscriptions[0]?.count || 0; 59 - if (bCount !== aCount) { 60 - return bCount - aCount; 61 - } 62 - // Secondary sort by uri for stability 63 - return b.uri.localeCompare(a.uri); 64 - } else { 65 - // recentlyUpdated 66 - const aDate = new Date( 67 - a.documents_in_publications[0]?.documents?.sort_date || 0, 68 - ).getTime(); 69 - const bDate = new Date( 70 - b.documents_in_publications[0]?.documents?.sort_date || 0, 71 - ).getTime(); 72 - if (bDate !== aDate) { 73 - return bDate - aDate; 74 - } 75 - // Secondary sort by uri for stability 76 - return b.uri.localeCompare(a.uri); 77 - } 78 - }); 79 - 80 - // Find cursor position and slice 81 - let startIndex = 0; 82 - if (cursor) { 83 - startIndex = allPubs.findIndex((pub) => { 84 - if (order === "popular") { 85 - const pubCount = pub.publication_subscriptions[0]?.count || 0; 86 - // Find first pub after cursor 87 - return ( 88 - pubCount < (cursor.count || 0) || 89 - (pubCount === cursor.count && pub.uri < cursor.uri) 90 - ); 91 - } else { 92 - const pubDate = pub.documents_in_publications[0]?.documents?.sort_date || ""; 93 - // Find first pub after cursor 94 - return ( 95 - pubDate < (cursor.sort_date || "") || 96 - (pubDate === cursor.sort_date && pub.uri < cursor.uri) 97 - ); 98 - } 99 - }); 100 - // If not found, we're at the end 101 - if (startIndex === -1) { 102 - return { publications: [], nextCursor: null }; 103 - } 104 - } 105 - 106 - // Get the page 107 - const page = allPubs.slice(startIndex, startIndex + limit); 108 - 109 - // Normalize publication records 110 - const normalizedPage = page 111 - .map(normalizePublicationRow) 112 - .filter(hasValidPublication); 113 - 114 - // Create next cursor based on last item in normalizedPage 115 - const lastItem = normalizedPage[normalizedPage.length - 1]; 116 - const nextCursor = 117 - normalizedPage.length > 0 && startIndex + limit < allPubs.length 118 - ? order === "recentlyUpdated" 119 - ? { 120 - sort_date: lastItem.documents_in_publications[0]?.documents?.sort_date, 121 - uri: lastItem.uri, 122 - } 123 - : { 124 - count: lastItem.publication_subscriptions[0]?.count || 0, 125 - uri: lastItem.uri, 126 - } 127 - : null; 128 - 129 - return { 130 - publications: normalizedPage, 131 - nextCursor, 132 - }; 133 - }
-53
app/(home-pages)/discover/page.tsx
··· 1 - import Link from "next/link"; 2 - import { SortedPublicationList } from "./SortedPublicationList"; 3 - import { Metadata } from "next"; 4 - import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 5 - import { getPublications } from "./getPublications"; 6 - 7 - export const metadata: Metadata = { 8 - title: "Leaflet Discover", 9 - description: "Explore publications on Leaflet ✨ Or make your own!", 10 - }; 11 - 12 - export default async function Discover(props: { 13 - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; 14 - }) { 15 - let order = ((await props.searchParams).order as string) || "recentlyUpdated"; 16 - 17 - return ( 18 - <DashboardLayout 19 - id="discover" 20 - currentPage="discover" 21 - defaultTab="default" 22 - actions={null} 23 - tabs={{ 24 - default: { 25 - controls: null, 26 - content: <DiscoverContent order={order} />, 27 - }, 28 - }} 29 - /> 30 - ); 31 - } 32 - 33 - const DiscoverContent = async (props: { order: string }) => { 34 - const orderValue = props.order === "popular" ? "popular" : "recentlyUpdated"; 35 - let { publications, nextCursor } = await getPublications(orderValue); 36 - 37 - return ( 38 - <div className="max-w-prose mx-auto w-full"> 39 - <div className="discoverHeader flex flex-col items-center text-center pt-2 px-4"> 40 - <h1>Discover</h1> 41 - <p className="text-lg text-secondary italic mb-2"> 42 - Explore publications on Leaflet ✨ Or{" "} 43 - <Link href="/lish/createPub">make your own</Link>! 44 - </p> 45 - </div> 46 - <SortedPublicationList 47 - publications={publications} 48 - order={props.order} 49 - nextCursor={nextCursor} 50 - /> 51 - </div> 52 - ); 53 - };
+6 -116
app/(home-pages)/home/Actions/AccountSettings.tsx
··· 1 1 "use client"; 2 2 3 3 import { ActionButton } from "components/ActionBar/ActionButton"; 4 - import { mutate } from "swr"; 5 - import { AccountSmall } from "components/Icons/AccountSmall"; 6 - import { LogoutSmall } from "components/Icons/LogoutSmall"; 7 4 import { Popover } from "components/Popover"; 8 - import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 9 - import { SpeedyLink } from "components/SpeedyLink"; 10 - import { GoBackSmall } from "components/Icons/GoBackSmall"; 11 - import { useState } from "react"; 12 5 import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter"; 13 6 import { useIsMobile } from "src/hooks/isMobile"; 7 + import { PaintSmall } from "components/Icons/PaintSmall"; 14 8 15 - export const AccountSettings = (props: { entityID: string }) => { 16 - let [state, setState] = useState<"menu" | "general" | "theme">("menu"); 9 + export const AccountTheme = (props: { entityID: string }) => { 17 10 let isMobile = useIsMobile(); 18 11 19 12 return ( 20 13 <Popover 21 14 asChild 22 - onOpenChange={() => setState("menu")} 23 15 side={isMobile ? "top" : "right"} 24 16 align={isMobile ? "center" : "start"} 25 - className={`max-w-xs w-[1000px] ${state === "theme" && "bg-white!"}`} 26 - trigger={<ActionButton icon=<AccountSmall /> label="Settings" />} 17 + className={`w-xs bg-white!`} 18 + arrowFill="bg-white" 19 + trigger={<ActionButton smallOnMobile icon=<PaintSmall /> label="Theme" />} 27 20 > 28 - {state === "general" ? ( 29 - <GeneralSettings backToMenu={() => setState("menu")} /> 30 - ) : state === "theme" ? ( 31 - <AccountThemeSettings 32 - entityID={props.entityID} 33 - backToMenu={() => setState("menu")} 34 - /> 35 - ) : ( 36 - <SettingsMenu state={state} setState={setState} /> 37 - )} 38 - </Popover> 39 - ); 40 - }; 41 - 42 - const SettingsMenu = (props: { 43 - state: "menu" | "general" | "theme"; 44 - setState: (s: typeof props.state) => void; 45 - }) => { 46 - let menuItemClassName = 47 - "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; 48 - 49 - return ( 50 - <div className="flex flex-col gap-0.5"> 51 - <AccountSettingsHeader state={"menu"} /> 52 - <button 53 - className={menuItemClassName} 54 - type="button" 55 - onClick={() => { 56 - props.setState("general"); 57 - }} 58 - > 59 - General 60 - <ArrowRightTiny /> 61 - </button> 62 - <button 63 - className={menuItemClassName} 64 - type="button" 65 - onClick={() => props.setState("theme")} 66 - > 67 - Account Theme 68 - <ArrowRightTiny /> 69 - </button> 70 - </div> 71 - ); 72 - }; 73 - 74 - const GeneralSettings = (props: { backToMenu: () => void }) => { 75 - return ( 76 - <div className="flex flex-col gap-0.5"> 77 - <AccountSettingsHeader 78 - state={"general"} 79 - backToMenuAction={() => props.backToMenu()} 80 - /> 81 - 82 - <button 83 - className="flex gap-2 font-bold" 84 - onClick={async () => { 85 - await fetch("/api/auth/logout"); 86 - mutate("identity", null); 87 - }} 88 - > 89 - <LogoutSmall /> 90 - Logout 91 - </button> 92 - </div> 93 - ); 94 - }; 95 - const AccountThemeSettings = (props: { 96 - entityID: string; 97 - backToMenu: () => void; 98 - }) => { 99 - return ( 100 - <div className="flex flex-col gap-0.5"> 101 - <AccountSettingsHeader 102 - state={"theme"} 103 - backToMenuAction={() => props.backToMenu()} 104 - /> 105 21 <ThemeSetterContent entityID={props.entityID} home /> 106 - </div> 107 - ); 108 - }; 109 - export const AccountSettingsHeader = (props: { 110 - state: "menu" | "general" | "theme"; 111 - backToMenuAction?: () => void; 112 - }) => { 113 - return ( 114 - <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1"> 115 - {props.state === "menu" 116 - ? "Settings" 117 - : props.state === "general" 118 - ? "General" 119 - : props.state === "theme" 120 - ? "Account Theme" 121 - : ""} 122 - {props.backToMenuAction && ( 123 - <button 124 - type="button" 125 - onClick={() => { 126 - props.backToMenuAction && props.backToMenuAction(); 127 - }} 128 - > 129 - <GoBackSmall className="text-accent-contrast" /> 130 - </button> 131 - )} 132 - </div> 22 + </Popover> 133 23 ); 134 24 };
+2 -6
app/(home-pages)/home/Actions/Actions.tsx
··· 2 2 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 3 3 import { CreateNewLeafletButton } from "./CreateNewButton"; 4 4 import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; 5 - import { AccountSettings } from "./AccountSettings"; 5 + import { AccountTheme } from "./AccountSettings"; 6 6 import { useIdentityData } from "components/IdentityProvider"; 7 7 import { useReplicache } from "src/replicache"; 8 8 import { LoginActionButton } from "components/LoginButton"; ··· 13 13 return ( 14 14 <> 15 15 <CreateNewLeafletButton /> 16 - {identity ? ( 17 - <AccountSettings entityID={rootEntity} /> 18 - ) : ( 19 - <LoginActionButton /> 20 - )} 16 + {identity && <AccountTheme entityID={rootEntity} />} 21 17 </> 22 18 ); 23 19 };
+2 -1
app/(home-pages)/home/Actions/CreateNewButton.tsx
··· 26 26 <ActionButton 27 27 id="new-leaflet-button" 28 28 primary 29 - icon=<AddTiny className="m-1 shrink-0" /> 29 + icon=<AddTiny className="sm:m-1 shrink-0 sm:scale-100 scale-75" /> 30 30 label="New" 31 + smallOnMobile 31 32 /> 32 33 } 33 34 >
+55 -79
app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx
··· 1 1 "use client"; 2 - 3 2 import { PubListEmptyIllo } from "components/ActionBar/Publications"; 4 - import { ButtonPrimary } from "components/Buttons"; 5 - import { AddSmall } from "components/Icons/AddSmall"; 6 - import { Link } from "react-aria-components"; 7 - import { DiscoverIllo } from "./DiscoverIllo"; 3 + import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; 8 4 import { WelcomeToLeafletIllo } from "./WelcomeToLeafletIllo"; 9 - import { DiscoverSmall } from "components/Icons/DiscoverSmall"; 10 - import { PublishSmall } from "components/Icons/PublishSmall"; 11 5 import { createNewLeaflet } from "actions/createNewLeaflet"; 12 6 import { useIsMobile } from "src/hooks/isMobile"; 7 + import { SpeedyLink } from "components/SpeedyLink"; 13 8 14 9 export function HomeEmptyState() { 15 - let isMobile = useIsMobile(); 16 10 return ( 17 - <div className="flex flex-col gap-4 font-bold"> 18 - <div className="container p-2 flex gap-4"> 19 - <div className="w-[72px]"> 20 - <WelcomeToLeafletIllo /> 21 - </div> 22 - <div className="flex flex-col grow"> 23 - <h3 className="text-xl font-semibold pt-2">Leaflet</h3> 24 - {/*<h3>A platform for social publishing.</h3>*/} 25 - <div className="font-normal text-tertiary italic"> 26 - Write and share delightful documents! 27 - </div> 28 - <ButtonPrimary 29 - className="!text-lg my-3" 30 - onClick={async () => { 31 - let openNewLeaflet = (id: string) => { 32 - if (isMobile) { 33 - window.location.href = `/${id}?focusFirstBlock`; 34 - } else { 35 - window.open(`/${id}?focusFirstBlock`, "_blank"); 36 - } 37 - }; 38 - 39 - let id = await createNewLeaflet({ 40 - pageType: "doc", 41 - redirectUser: false, 42 - }); 43 - openNewLeaflet(id); 44 - }} 45 - > 46 - <AddSmall /> Write a Doc! 47 - </ButtonPrimary> 48 - </div> 49 - </div> 50 - <div className="flex gap-2 w-full items-center text-tertiary font-normal italic"> 51 - <hr className="border-border w-full" /> 11 + <div className="flex flex-col gap-4 sm:flex-row"> 12 + <PublicationBanner /> 13 + <div className="flex sm:flex-col flex-row gap-2 sm:w-fit w-full items-center text-tertiary font-normal italic"> 14 + <hr className="border-border grow w-full sm:w-px h-px sm:h-full border-l" /> 52 15 <div>or</div> 53 - <hr className="border-border w-full" /> 16 + <hr className="border-border grow w-full sm:w-px h-px sm:h-full border-l" /> 54 17 </div> 55 - 56 - <PublicationBanner /> 57 - <DiscoverBanner /> 58 - <div className="text-tertiary italic text-sm font-normal -mt-2"> 59 - Right now docs and publications are separate. Soon you'll be able to add 60 - docs to pubs! 61 - </div> 18 + <DocBanner /> 62 19 </div> 63 20 ); 64 21 } 65 22 66 - export const PublicationBanner = (props: { small?: boolean }) => { 23 + let bannerStyles = 24 + "flex flex-row sm:flex-col py-4 px-4 sm:items-center items-start gap-4"; 25 + 26 + const PublicationBanner = () => { 67 27 return ( 68 - <div 69 - className={`accent-container flex sm:py-2 items-center ${props.small ? "items-start gap-2 p-2 text-sm font-normal" : "items-center p-4 gap-4"}`} 70 - > 71 - {props.small ? ( 72 - <PublishSmall className="shrink-0 text-accent-contrast" /> 73 - ) : ( 28 + <div className={`accent-container sm:basis-2/3 ${bannerStyles}`}> 29 + <div className="my-auto flex flex-row sm:flex-col gap-4"> 74 30 <div className="w-[64px] mx-auto"> 75 31 <PubListEmptyIllo /> 76 32 </div> 77 - )} 78 - <div className={`${props.small ? "pt-[2px]" : ""} grow`}> 79 - <Link href={"/lish/createPub"} className="font-bold"> 80 - Start a Publication 81 - </Link>{" "} 82 - and blog in the Atmosphere 33 + <div className={`flex flex-col sm:text-center text-left w-full`}> 34 + <h3>Create a Publication!</h3> 35 + <div className="mb-2 text-tertiary"> 36 + You can decide to share or publish it later. 37 + </div> 38 + <SpeedyLink href="/lish/createPub" className="sm:mx-auto mx-0 "> 39 + <ButtonPrimary>Create a Publication</ButtonPrimary> 40 + </SpeedyLink> 41 + </div> 83 42 </div> 84 43 </div> 85 44 ); 86 45 }; 87 46 88 - export const DiscoverBanner = (props: { small?: boolean }) => { 47 + const DocBanner = () => { 48 + let isMobile = useIsMobile(); 49 + 89 50 return ( 90 - <div 91 - className={`accent-container flex sm:py-2 items-center ${props.small ? "items-start gap-2 p-2 text-sm font-normal" : "items-center p-4 gap-4"}`} 92 - > 93 - {props.small ? ( 94 - <DiscoverSmall className="shrink-0 text-accent-contrast" /> 95 - ) : ( 96 - <div className="w-[64px] mx-auto"> 97 - <DiscoverIllo /> 51 + <div className={`text-sm sm:basis-1/3 py-0! sm:py-4! ${bannerStyles}`}> 52 + <div className="w-[48px] mx-auto"> 53 + <WelcomeToLeafletIllo /> 54 + </div> 55 + 56 + <div className={`grow flex flex-col sm:text-center text-left w-full`}> 57 + <h4>Just write something</h4> 58 + <div className="mb-2 text-tertiary"> 59 + You can decide to share or publish it later. 98 60 </div> 99 - )} 100 - <div className={`${props.small ? "pt-[2px]" : ""} grow`}> 101 - <Link href={"/discover"} className="font-bold"> 102 - Explore Publications 103 - </Link>{" "} 104 - on art, tech, games, music & more! 61 + <ButtonSecondary 62 + className="sm:mx-auto mx-0" 63 + onClick={async () => { 64 + let openNewLeaflet = (id: string) => { 65 + if (isMobile) { 66 + window.location.href = `/${id}?focusFirstBlock`; 67 + } else { 68 + window.open(`/${id}?focusFirstBlock`, "_blank"); 69 + } 70 + }; 71 + 72 + let id = await createNewLeaflet({ 73 + pageType: "doc", 74 + redirectUser: false, 75 + }); 76 + openNewLeaflet(id); 77 + }} 78 + > 79 + New Doc! 80 + </ButtonSecondary> 105 81 </div> 106 82 </div> 107 83 );
+2 -2
app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx
··· 3 3 export const WelcomeToLeafletIllo = () => { 4 4 return ( 5 5 <svg 6 - width="73" 7 - height="68" 6 + width="48" 7 + height="44" 8 8 viewBox="0 0 73 68" 9 9 fill="none" 10 10 xmlns="http://www.w3.org/2000/svg"
+3 -10
app/(home-pages)/home/HomeLayout.tsx
··· 23 23 import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 24 24 import { useState } from "react"; 25 25 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 26 - import { 27 - DiscoverBanner, 28 - HomeEmptyState, 29 - PublicationBanner, 30 - } from "./HomeEmpty/HomeEmpty"; 26 + import { HomeEmptyState } from "./HomeEmpty/HomeEmpty"; 31 27 32 28 export type Leaflet = { 33 29 added_at: string; ··· 103 99 ), 104 100 }, 105 101 }} 102 + pageTitle={"Home"} 106 103 /> 107 104 ); 108 105 }; ··· 170 167 showPreview 171 168 /> 172 169 <div className="spacer h-4 w-full bg-transparent shrink-0 " /> 173 - 174 - {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === 175 - 0 && <PublicationBanner small />} 176 - <DiscoverBanner small /> 177 170 </> 178 171 ); 179 172 } ··· 204 197 className={` 205 198 leafletList 206 199 w-full 207 - ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2 pt-2"} `} 200 + ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2"} `} 208 201 > 209 202 {props.leaflets.map(({ token: leaflet, added_at, archived }, index) => ( 210 203 <ReplicacheProvider
+3
app/(home-pages)/layout.tsx
··· 1 1 import { getIdentityData } from "actions/getIdentityData"; 2 2 import { EntitySetProvider } from "components/EntitySetProvider"; 3 + import { NavStateTracker } from "components/NavStateTracker"; 3 4 import { 4 5 ThemeProvider, 5 6 ThemeBackgroundProvider, ··· 13 14 if (!identityData?.home_leaflet) 14 15 return ( 15 16 <> 17 + <NavStateTracker /> 16 18 <ThemeProvider entityID={""}>{props.children}</ThemeProvider> 17 19 </> 18 20 ); ··· 34 36 > 35 37 <ThemeProvider entityID={root_entity}> 36 38 <ThemeBackgroundProvider entityID={root_entity}> 39 + <NavStateTracker /> 37 40 {props.children} 38 41 </ThemeBackgroundProvider> 39 42 </ThemeProvider>
+1
app/(home-pages)/looseleafs/LooseleafsLayout.tsx
··· 47 47 ), 48 48 }, 49 49 }} 50 + pageTitle="Looseleafs" 50 51 /> 51 52 ); 52 53 };
+1 -1
app/(home-pages)/p/[didOrHandle]/PostsContent.tsx
··· 68 68 } 69 69 70 70 return ( 71 - <div className="flex flex-col gap-3 text-left relative"> 71 + <div className="profilePosts h-full flex py-4 flex-col gap-3 text-left relative"> 72 72 {allPosts.map((post) => ( 73 73 <PostListing key={post.documents.uri} {...post} /> 74 74 ))}
+2 -4
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
··· 42 42 @{props.profile.handle} 43 43 </div> 44 44 ); 45 - console.log(props.profile); 46 - 47 45 return ( 48 46 <div 49 47 className={`profileHeader flex flex-col relative `} ··· 74 72 </pre> 75 73 </div> 76 74 77 - <div className="profilePublicationCards w-full overflow-x-scroll"> 75 + <div className="profilePubCardContainer w-full overflow-x-scroll"> 78 76 <div 79 - className={`grid grid-flow-col gap-2 mx-auto w-fit px-3 sm:px-4 ${props.popover ? "auto-cols-[164px]" : "auto-cols-[164px] sm:auto-cols-[240px]"}`} 77 + className={`profilePubCards grid grid-flow-col gap-2 mx-auto w-fit ${props.popover ? "auto-cols-[164px]" : "auto-cols-[164px] sm:auto-cols-[240px]"}`} 80 78 > 81 79 {props.publications.map((p) => ( 82 80 <PublicationCard key={p.uri} record={p.record} uri={p.uri} />
+2 -2
app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
··· 11 11 ${ 12 12 cardBorderHidden 13 13 ? "" 14 - : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page" 14 + : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page px-3 sm:px-4" 15 15 } 16 16 max-w-prose mx-auto w-full 17 - flex flex-col 17 + flex flex-col pb-3 18 18 text-center 19 19 `} 20 20 >
+1 -1
app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
··· 41 41 const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; 42 42 43 43 return ( 44 - <div className="flex flex-col w-full sticky top-3 sm:top-4 z-20 sm:px-4 px-3 pt-6"> 44 + <div className="flex flex-col w-full sticky top-3 sm:top-4 z-20 pt-6"> 45 45 <div 46 46 style={ 47 47 scrollPosWithinTabContent < 20
+1 -1
app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
··· 85 85 } 86 86 87 87 return ( 88 - <div className="flex flex-col gap-2 text-left relative"> 88 + <div className="flex flex-col gap-2 py-4 text-left relative"> 89 89 {allComments.map((comment) => ( 90 90 <CommentItem key={comment.uri} comment={comment} /> 91 91 ))}
+2 -3
app/(home-pages)/p/[didOrHandle]/layout.tsx
··· 82 82 id="profile" 83 83 defaultTab="default" 84 84 currentPage="profile" 85 + profileDid={did} 85 86 actions={null} 86 87 tabs={{ 87 88 default: { ··· 93 94 publications={publications || []} 94 95 /> 95 96 <ProfileTabs didOrHandle={params.didOrHandle} /> 96 - <div className="h-full pt-3 pb-4 px-3 sm:px-4 flex flex-col"> 97 - {props.children} 98 - </div> 97 + <>{props.children}</> 99 98 </ProfileLayout> 100 99 ), 101 100 },
+2 -2
app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx
··· 2 2 3 3 import { useEffect, useRef } from "react"; 4 4 import useSWRInfinite from "swr/infinite"; 5 - import { PubListing } from "app/(home-pages)/discover/PubListing"; 5 + import { PubListing } from "app/(home-pages)/p/[didOrHandle]/PubListing"; 6 6 import { 7 7 getSubscriptions, 8 8 type PublicationSubscription, ··· 82 82 83 83 return ( 84 84 <div className="relative"> 85 - <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3"> 85 + <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3 py-4"> 86 86 {allSubscriptions.map((sub) => ( 87 87 <PubListing key={sub.uri} {...sub} /> 88 88 ))}
+31
app/(home-pages)/page.tsx
··· 1 + import { cookies } from "next/headers"; 2 + import ReaderLayout from "./reader/layout"; 3 + import ReaderPage from "./reader/page"; 4 + import HomePage from "./home/page"; 5 + 6 + export default async function RootPage() { 7 + const cookieStore = await cookies(); 8 + const hasAuth = 9 + cookieStore.has("auth_token") || 10 + cookieStore.has("external_auth_token"); 11 + 12 + if (!hasAuth) { 13 + return ( 14 + <ReaderLayout> 15 + <ReaderPage /> 16 + </ReaderLayout> 17 + ); 18 + } 19 + 20 + const navState = cookieStore.get("nav-state")?.value; 21 + 22 + if (navState === "reader") { 23 + return ( 24 + <ReaderLayout> 25 + <ReaderPage /> 26 + </ReaderLayout> 27 + ); 28 + } 29 + 30 + return <HomePage />; 31 + }
+13
app/(home-pages)/reader/FeedSkeleton.tsx
··· 1 + export function FeedSkeleton() { 2 + return ( 3 + <div className="flex flex-col gap-6 w-full animate-pulse"> 4 + {[...Array(3)].map((_, i) => ( 5 + <div key={i} className="flex flex-col gap-3 p-2"> 6 + <div className="h-4 bg-border-light rounded w-1/3" /> 7 + <div className="h-3 bg-border-light rounded w-2/3" /> 8 + <div className="h-3 bg-border-light rounded w-1/2" /> 9 + </div> 10 + ))} 11 + </div> 12 + ); 13 + }
+49
app/(home-pages)/reader/GlobalContent.tsx
··· 1 + "use client"; 2 + import { use } from "react"; 3 + import useSWR from "swr"; 4 + import { callRPC } from "app/api/rpc/client"; 5 + import { PostListing } from "components/PostListing"; 6 + import type { Post } from "./getReaderFeed"; 7 + import { 8 + DesktopInteractionPreviewDrawer, 9 + MobileInteractionPreviewDrawer, 10 + } from "./InteractionDrawers"; 11 + 12 + export const GlobalContent = (props: { 13 + promise: Promise<{ posts: Post[] }>; 14 + }) => { 15 + const initialData = use(props.promise); 16 + 17 + const { data } = useSWR( 18 + "hot_feed", 19 + async () => { 20 + const res = await callRPC("get_hot_feed", {}); 21 + return res as unknown as { posts: Post[] }; 22 + }, 23 + { 24 + fallbackData: { posts: initialData.posts }, 25 + }, 26 + ); 27 + 28 + const posts = data?.posts ?? []; 29 + 30 + if (posts.length === 0) { 31 + return ( 32 + <div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center text-tertiary"> 33 + Nothing trending right now. Check back soon! 34 + </div> 35 + ); 36 + } 37 + 38 + return ( 39 + <div className="flex flex-row gap-6 w-full"> 40 + <div className="flex flex-col gap-8 w-full"> 41 + {posts.map((p) => ( 42 + <PostListing {...p} key={p.documents.uri} /> 43 + ))} 44 + </div> 45 + <DesktopInteractionPreviewDrawer /> 46 + <MobileInteractionPreviewDrawer /> 47 + </div> 48 + ); 49 + };
+108
app/(home-pages)/reader/InteractionDrawers.tsx
··· 1 + "use client"; 2 + import { ButtonPrimary } from "components/Buttons"; 3 + import { 4 + SelectedPostListing, 5 + useSelectedPostListing, 6 + } from "src/useSelectedPostState"; 7 + import { CommentsDrawerContent } from "app/lish/[did]/[publication]/[rkey]/Interactions/Comments"; 8 + import { CloseTiny } from "components/Icons/CloseTiny"; 9 + import { SpeedyLink } from "components/SpeedyLink"; 10 + import { GoToArrow } from "components/Icons/GoToArrow"; 11 + import { DotLoader } from "components/utils/DotLoader"; 12 + import { ReaderMentionsContent } from "./ReaderMentionsContent"; 13 + import { callRPC } from "app/api/rpc/client"; 14 + import useSWR from "swr"; 15 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 16 + 17 + export const MobileInteractionPreviewDrawer = () => { 18 + let selectedPost = useSelectedPostListing((s) => s.selectedPostListing); 19 + 20 + return ( 21 + <div 22 + className={`z-20 fixed bottom-0 left-0 right-0 border border-border-light shrink-0 w-screen h-[90vh] px-3 bg-bg-leaflet rounded-t-lg overflow-auto ${selectedPost === null ? "hidden" : "block md:hidden "}`} 23 + > 24 + <PreviewDrawerContent selectedPost={selectedPost} /> 25 + </div> 26 + ); 27 + }; 28 + 29 + export const DesktopInteractionPreviewDrawer = () => { 30 + let selectedPost = useSelectedPostListing((s) => s.selectedPostListing); 31 + 32 + return ( 33 + <div 34 + className={`hidden md:block border border-border-light shrink-0 w-96 mr-2 px-3 h-[calc(100vh-100px)] sticky top-11 bottom-4 right-0 rounded-lg overflow-auto ${selectedPost === null ? "shadow-none border-dashed bg-transparent" : "shadow-md border-border bg-bg-page "}`} 35 + > 36 + <PreviewDrawerContent selectedPost={selectedPost} /> 37 + </div> 38 + ); 39 + }; 40 + 41 + const PreviewDrawerContent = (props: { 42 + selectedPost: SelectedPostListing | null; 43 + }) => { 44 + const documentUri = props.selectedPost?.document_uri || null; 45 + const drawer = props.selectedPost?.drawer || null; 46 + 47 + const { data, isLoading } = useSWR( 48 + documentUri ? ["get_document_interactions", documentUri] : null, 49 + async () => { 50 + const res = await callRPC("get_document_interactions", { 51 + document_uri: documentUri!, 52 + }); 53 + return res; 54 + }, 55 + { keepPreviousData: false }, 56 + ); 57 + 58 + if (!props.selectedPost || !props.selectedPost.document) return null; 59 + 60 + const postUrl = getDocumentURL( 61 + props.selectedPost.document, 62 + props.selectedPost.document_uri, 63 + ); 64 + 65 + const drawerTitle = 66 + drawer === "quotes" 67 + ? `Mentions of ${props.selectedPost.document.title}` 68 + : `Comments for ${props.selectedPost.document.title}`; 69 + 70 + return ( 71 + <> 72 + <div className="w-full text-sm text-tertiary flex justify-between pt-3 gap-3"> 73 + <div className="truncate min-w-0 grow">{drawerTitle}</div> 74 + <button 75 + className="text-tertiary" 76 + onClick={() => 77 + useSelectedPostListing.getState().setSelectedPostListing(null) 78 + } 79 + > 80 + <CloseTiny /> 81 + </button> 82 + </div> 83 + <SpeedyLink className="shrink-0 flex gap-1 items-center" href={postUrl}> 84 + <ButtonPrimary fullWidth compact className="text-sm! mt-1"> 85 + See Full Post <GoToArrow /> 86 + </ButtonPrimary> 87 + </SpeedyLink> 88 + {isLoading ? ( 89 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm mt-8"> 90 + <span>loading</span> 91 + <DotLoader /> 92 + </div> 93 + ) : drawer === "quotes" ? ( 94 + <div className="mt-3"> 95 + <ReaderMentionsContent 96 + quotesAndMentions={data?.quotesAndMentions || []} 97 + /> 98 + </div> 99 + ) : ( 100 + <CommentsDrawerContent 101 + noCommentBox 102 + document_uri={props.selectedPost.document_uri} 103 + comments={data?.comments || []} 104 + /> 105 + )} 106 + </> 107 + ); 108 + };
+93
app/(home-pages)/reader/NewContent.tsx
··· 1 + "use client"; 2 + 3 + import { use } from "react"; 4 + import type { Cursor, Post } from "./getReaderFeed"; 5 + import useSWRInfinite from "swr/infinite"; 6 + import { getNewFeed } from "./getNewFeed"; 7 + import { useEffect, useRef } from "react"; 8 + import { PostListing } from "components/PostListing"; 9 + import { 10 + DesktopInteractionPreviewDrawer, 11 + MobileInteractionPreviewDrawer, 12 + } from "./InteractionDrawers"; 13 + 14 + export const NewContent = (props: { 15 + promise: Promise<{ posts: Post[]; nextCursor: Cursor | null }>; 16 + }) => { 17 + const { posts, nextCursor } = use(props.promise); 18 + 19 + const getKey = ( 20 + pageIndex: number, 21 + previousPageData: { 22 + posts: Post[]; 23 + nextCursor: Cursor | null; 24 + } | null, 25 + ) => { 26 + if (previousPageData && !previousPageData.nextCursor) return null; 27 + if (pageIndex === 0) return ["new-feed", null] as const; 28 + return ["new-feed", previousPageData?.nextCursor] as const; 29 + }; 30 + 31 + const { data, size, setSize, isValidating } = useSWRInfinite( 32 + getKey, 33 + ([_, cursor]) => getNewFeed(cursor), 34 + { 35 + fallbackData: [{ posts, nextCursor }], 36 + revalidateFirstPage: false, 37 + }, 38 + ); 39 + 40 + const loadMoreRef = useRef<HTMLDivElement>(null); 41 + 42 + useEffect(() => { 43 + const observer = new IntersectionObserver( 44 + (entries) => { 45 + if (entries[0].isIntersecting && !isValidating) { 46 + const hasMore = data && data[data.length - 1]?.nextCursor; 47 + if (hasMore) { 48 + setSize(size + 1); 49 + } 50 + } 51 + }, 52 + { threshold: 0.1 }, 53 + ); 54 + 55 + if (loadMoreRef.current) { 56 + observer.observe(loadMoreRef.current); 57 + } 58 + 59 + return () => observer.disconnect(); 60 + }, [data, size, setSize, isValidating]); 61 + 62 + const allPosts = data ? data.flatMap((page) => page.posts) : []; 63 + 64 + if (allPosts.length === 0 && !isValidating) { 65 + return ( 66 + <div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center text-tertiary"> 67 + No posts yet. Check back soon! 68 + </div> 69 + ); 70 + } 71 + 72 + return ( 73 + <div className="flex flex-row gap-6 w-full"> 74 + <div className="flex flex-col gap-6 w-full relative"> 75 + {allPosts.map((p) => ( 76 + <PostListing {...p} key={p.documents.uri} /> 77 + ))} 78 + <div 79 + ref={loadMoreRef} 80 + className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 81 + aria-hidden="true" 82 + /> 83 + {isValidating && ( 84 + <div className="text-center text-tertiary py-4"> 85 + Loading more posts... 86 + </div> 87 + )} 88 + </div> 89 + <DesktopInteractionPreviewDrawer /> 90 + <MobileInteractionPreviewDrawer /> 91 + </div> 92 + ); 93 + };
app/(home-pages)/reader/PreviewDrawer.tsx

This is a binary file and will not be displayed.

+40 -21
app/(home-pages)/reader/ReaderContent.tsx app/(home-pages)/reader/InboxContent.tsx
··· 1 1 "use client"; 2 + import { use } from "react"; 2 3 import { ButtonPrimary } from "components/Buttons"; 3 4 import { DiscoverSmall } from "components/Icons/DiscoverSmall"; 4 5 import type { Cursor, Post } from "./getReaderFeed"; ··· 7 8 import { useEffect, useRef } from "react"; 8 9 import Link from "next/link"; 9 10 import { PostListing } from "components/PostListing"; 11 + import { useHasBackgroundImage } from "components/Pages/useHasBackgroundImage"; 12 + import { 13 + DesktopInteractionPreviewDrawer, 14 + MobileInteractionPreviewDrawer, 15 + } from "./InteractionDrawers"; 10 16 11 - export const ReaderContent = (props: { 12 - posts: Post[]; 13 - nextCursor: Cursor | null; 17 + export const InboxContent = (props: { 18 + promise: Promise<{ posts: Post[]; nextCursor: Cursor | null }>; 14 19 }) => { 20 + const { posts, nextCursor } = use(props.promise); 21 + 15 22 const getKey = ( 16 23 pageIndex: number, 17 24 previousPageData: { ··· 33 40 getKey, 34 41 ([_, cursor]) => getReaderFeed(cursor), 35 42 { 36 - fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], 43 + fallbackData: [{ posts, nextCursor }], 37 44 revalidateFirstPage: false, 38 45 }, 39 46 ); ··· 63 70 64 71 const allPosts = data ? data.flatMap((page) => page.posts) : []; 65 72 73 + const sortedPosts = allPosts.sort( 74 + (a, b) => 75 + new Date(b.documents.data?.publishedAt || 0).getTime() - 76 + new Date(a.documents.data?.publishedAt || 0).getTime(), 77 + ); 78 + 66 79 if (allPosts.length === 0 && !isValidating) return <ReaderEmpty />; 67 80 81 + let hasBackgroundImage = useHasBackgroundImage(); 82 + 68 83 return ( 69 - <div className="flex flex-col gap-3 relative"> 70 - {allPosts.map((p) => ( 71 - <PostListing {...p} key={p.documents.uri} /> 72 - ))} 73 - {/* Trigger element for loading more posts */} 74 - <div 75 - ref={loadMoreRef} 76 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 77 - aria-hidden="true" 78 - /> 79 - {isValidating && ( 80 - <div className="text-center text-tertiary py-4"> 81 - Loading more posts... 82 - </div> 83 - )} 84 + <div className="flex flex-row gap-6 w-full "> 85 + <div className="flex flex-col gap-6 w-full relative"> 86 + {sortedPosts.map((p) => ( 87 + <PostListing {...p} key={p.documents.uri} /> 88 + ))} 89 + {/* Trigger element for loading more posts */} 90 + <div 91 + ref={loadMoreRef} 92 + className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 93 + aria-hidden="true" 94 + /> 95 + {isValidating && ( 96 + <div className="text-center text-tertiary py-4"> 97 + Loading more posts... 98 + </div> 99 + )} 100 + </div> 101 + <DesktopInteractionPreviewDrawer /> 102 + <MobileInteractionPreviewDrawer /> 84 103 </div> 85 104 ); 86 105 }; ··· 90 109 <div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center text-tertiary"> 91 110 Nothing to read yet… <br /> 92 111 Subscribe to publications and find their posts here! 93 - <Link href={"/discover"}> 112 + <Link href={"/reader/hot"}> 94 113 <ButtonPrimary className="mx-auto place-self-center"> 95 - <DiscoverSmall /> Discover Publications 114 + <DiscoverSmall /> See what posts people are reading! 96 115 </ButtonPrimary> 97 116 </Link> 98 117 </div>
+72
app/(home-pages)/reader/ReaderMentionsContent.tsx
··· 1 + "use client"; 2 + import useSWR from "swr"; 3 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 4 + import { BskyPostContent } from "app/lish/[did]/[publication]/[rkey]/BskyPostContent"; 5 + import { DotLoader } from "components/utils/DotLoader"; 6 + 7 + async function fetchBskyPosts(uris: string[]): Promise<PostView[]> { 8 + const params = new URLSearchParams({ 9 + uris: JSON.stringify(uris), 10 + }); 11 + const response = await fetch(`/api/bsky/hydrate?${params.toString()}`); 12 + if (!response.ok) throw new Error("Failed to fetch Bluesky posts"); 13 + return response.json(); 14 + } 15 + 16 + export function ReaderMentionsContent(props: { 17 + quotesAndMentions: { uri: string; link?: string }[]; 18 + }) { 19 + const uris = props.quotesAndMentions.map((q) => q.uri); 20 + const key = uris.length > 0 21 + ? `/api/bsky/hydrate?${new URLSearchParams({ uris: JSON.stringify(uris) }).toString()}` 22 + : null; 23 + 24 + const { data: bskyPosts, isLoading } = useSWR(key, () => 25 + fetchBskyPosts(uris), 26 + ); 27 + 28 + if (props.quotesAndMentions.length === 0) { 29 + return ( 30 + <div className="opaque-container flex flex-col gap-0.5 p-[6px] text-tertiary italic text-sm text-center"> 31 + <div className="font-bold">no mentions yet!</div> 32 + </div> 33 + ); 34 + } 35 + 36 + if (isLoading) { 37 + return ( 38 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm mt-8"> 39 + <span>loading</span> 40 + <DotLoader /> 41 + </div> 42 + ); 43 + } 44 + 45 + const postViewMap = new Map<string, PostView>(); 46 + bskyPosts?.forEach((pv) => postViewMap.set(pv.uri, pv)); 47 + 48 + return ( 49 + <div className="flex flex-col gap-4 w-full"> 50 + {props.quotesAndMentions.map((q, index) => { 51 + const post = postViewMap.get(q.uri); 52 + if (!post) return null; 53 + return ( 54 + <div key={q.uri}> 55 + <BskyPostContent 56 + post={post} 57 + parent={undefined} 58 + showBlueskyLink={true} 59 + showEmbed={true} 60 + avatarSize="medium" 61 + className="text-sm" 62 + compactEmbed 63 + /> 64 + {index < props.quotesAndMentions.length - 1 && ( 65 + <hr className="border-border-light mt-4" /> 66 + )} 67 + </div> 68 + ); 69 + })} 70 + </div> 71 + ); 72 + }
-105
app/(home-pages)/reader/SubscriptionsContent.tsx
··· 1 - "use client"; 2 - import { PubListing } from "app/(home-pages)/discover/PubListing"; 3 - import { ButtonPrimary } from "components/Buttons"; 4 - import { DiscoverSmall } from "components/Icons/DiscoverSmall"; 5 - import { Json } from "supabase/database.types"; 6 - import { PublicationSubscription, getSubscriptions } from "./getSubscriptions"; 7 - import useSWRInfinite from "swr/infinite"; 8 - import { useEffect, useRef } from "react"; 9 - import { Cursor } from "./getReaderFeed"; 10 - import Link from "next/link"; 11 - 12 - export const SubscriptionsContent = (props: { 13 - publications: PublicationSubscription[]; 14 - nextCursor: Cursor | null; 15 - }) => { 16 - const getKey = ( 17 - pageIndex: number, 18 - previousPageData: { 19 - subscriptions: PublicationSubscription[]; 20 - nextCursor: Cursor | null; 21 - } | null, 22 - ) => { 23 - // Reached the end 24 - if (previousPageData && !previousPageData.nextCursor) return null; 25 - 26 - // First page, we don't have previousPageData 27 - if (pageIndex === 0) return ["subscriptions", null] as const; 28 - 29 - // Add the cursor to the key 30 - return ["subscriptions", previousPageData?.nextCursor] as const; 31 - }; 32 - 33 - const { data, error, size, setSize, isValidating } = useSWRInfinite( 34 - getKey, 35 - ([_, cursor]) => getSubscriptions(null, cursor), 36 - { 37 - fallbackData: [ 38 - { subscriptions: props.publications, nextCursor: props.nextCursor }, 39 - ], 40 - revalidateFirstPage: false, 41 - }, 42 - ); 43 - 44 - const loadMoreRef = useRef<HTMLDivElement>(null); 45 - 46 - // Set up intersection observer to load more when trigger element is visible 47 - useEffect(() => { 48 - const observer = new IntersectionObserver( 49 - (entries) => { 50 - if (entries[0].isIntersecting && !isValidating) { 51 - const hasMore = data && data[data.length - 1]?.nextCursor; 52 - if (hasMore) { 53 - setSize(size + 1); 54 - } 55 - } 56 - }, 57 - { threshold: 0.1 }, 58 - ); 59 - 60 - if (loadMoreRef.current) { 61 - observer.observe(loadMoreRef.current); 62 - } 63 - 64 - return () => observer.disconnect(); 65 - }, [data, size, setSize, isValidating]); 66 - 67 - const allPublications = data 68 - ? data.flatMap((page) => page.subscriptions) 69 - : []; 70 - 71 - if (allPublications.length === 0 && !isValidating) 72 - return <SubscriptionsEmpty />; 73 - 74 - return ( 75 - <div className="relative"> 76 - <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3"> 77 - {allPublications?.map((p, index) => <PubListing key={p.uri} {...p} />)} 78 - </div> 79 - {/* Trigger element for loading more subscriptions */} 80 - <div 81 - ref={loadMoreRef} 82 - className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 83 - aria-hidden="true" 84 - /> 85 - {isValidating && ( 86 - <div className="text-center text-tertiary py-4"> 87 - Loading more subscriptions... 88 - </div> 89 - )} 90 - </div> 91 - ); 92 - }; 93 - 94 - export const SubscriptionsEmpty = () => { 95 - return ( 96 - <div className="flex flex-col gap-2 container bg-[rgba(var(--bg-page),.7)] sm:p-4 p-3 justify-between text-center text-tertiary"> 97 - You haven't subscribed to any publications yet! 98 - <Link href={"/discover"}> 99 - <ButtonPrimary className="mx-auto place-self-center"> 100 - <DiscoverSmall /> Discover Publications 101 - </ButtonPrimary> 102 - </Link> 103 - </div> 104 - ); 105 - };
+88
app/(home-pages)/reader/enrichPost.ts
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 3 + import { getConstellationBacklinks } from "app/lish/[did]/[publication]/[rkey]/getPostPageData"; 4 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 5 + import { 6 + normalizeDocumentRecord, 7 + normalizePublicationRecord, 8 + } from "src/utils/normalizeRecords"; 9 + import { idResolver } from "./idResolver"; 10 + import type { Post } from "./getReaderFeed"; 11 + 12 + type RawDocument = { 13 + data: unknown; 14 + uri: string; 15 + sort_date: string; 16 + comments_on_documents: { count: number }[]; 17 + document_mentions_in_bsky: { count: number }[]; 18 + recommends_on_documents: { count: number }[]; 19 + documents_in_publications: { 20 + publications: { 21 + uri: string; 22 + record: unknown; 23 + name: string | null; 24 + [key: string]: unknown; 25 + } | null; 26 + }[]; 27 + }; 28 + 29 + export async function enrichDocumentToPost( 30 + doc: RawDocument, 31 + ): Promise<Post | null> { 32 + const pub = doc.documents_in_publications?.[0]?.publications; 33 + const uri = new AtUri(doc.uri); 34 + const handle = await idResolver.did.resolve(uri.host); 35 + 36 + const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); 37 + if (!normalizedData) return null; 38 + 39 + const normalizedPubRecord = pub 40 + ? normalizePublicationRecord(pub.record) 41 + : null; 42 + 43 + const mentionsCount = await getAccurateMentionsCount( 44 + normalizedData, 45 + doc.uri, 46 + normalizedPubRecord, 47 + doc.document_mentions_in_bsky?.[0]?.count || 0, 48 + ); 49 + 50 + return { 51 + publication: pub 52 + ? { 53 + href: getPublicationURL(pub), 54 + pubRecord: normalizedPubRecord, 55 + uri: pub.uri || "", 56 + } 57 + : undefined, 58 + author: handle?.alsoKnownAs?.[0] 59 + ? `@${handle.alsoKnownAs[0].slice(5)}` 60 + : null, 61 + documents: { 62 + comments_on_documents: doc.comments_on_documents, 63 + document_mentions_in_bsky: doc.document_mentions_in_bsky, 64 + recommends_on_documents: doc.recommends_on_documents, 65 + mentionsCount, 66 + data: normalizedData, 67 + uri: doc.uri, 68 + sort_date: doc.sort_date, 69 + }, 70 + }; 71 + } 72 + 73 + async function getAccurateMentionsCount( 74 + normalizedData: NonNullable<ReturnType<typeof normalizeDocumentRecord>>, 75 + docUri: string, 76 + normalizedPubRecord: ReturnType<typeof normalizePublicationRecord>, 77 + dbMentionsCount: number, 78 + ): Promise<number> { 79 + const postUrl = getDocumentURL(normalizedData, docUri, normalizedPubRecord); 80 + const absoluteUrl = postUrl.startsWith("/") 81 + ? `https://leaflet.pub${postUrl}` 82 + : postUrl; 83 + const constellationBacklinks = await getConstellationBacklinks(absoluteUrl); 84 + const uniqueBacklinkCount = new Set( 85 + constellationBacklinks.map((b) => b.uri), 86 + ).size; 87 + return dbMentionsCount + uniqueBacklinkCount; 88 + }
+100
app/(home-pages)/reader/getHotFeed.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/node-postgres"; 4 + import { sql } from "drizzle-orm"; 5 + import { pool } from "supabase/pool"; 6 + import Client from "ioredis"; 7 + import { AtUri } from "@atproto/api"; 8 + import { supabaseServerClient } from "supabase/serverClient"; 9 + import { enrichDocumentToPost } from "./enrichPost"; 10 + import type { Post } from "./getReaderFeed"; 11 + 12 + let redisClient: Client | null = null; 13 + if (process.env.REDIS_URL && process.env.NODE_ENV === "production") { 14 + redisClient = new Client(process.env.REDIS_URL); 15 + } 16 + 17 + const CACHE_KEY = "hot_feed_v1"; 18 + const CACHE_TTL = 300; // 5 minutes 19 + 20 + export async function getHotFeed(): Promise<{ posts: Post[] }> { 21 + // Check Redis cache 22 + if (redisClient) { 23 + const cached = await redisClient.get(CACHE_KEY); 24 + if (cached) { 25 + return JSON.parse(cached) as { posts: Post[] }; 26 + } 27 + } 28 + 29 + // Run ranked SQL query to get top 50 URIs 30 + const client = await pool.connect(); 31 + const db = drizzle(client); 32 + 33 + let uris: string[]; 34 + try { 35 + const ranked = await db.execute(sql` 36 + SELECT uri 37 + FROM documents 38 + WHERE indexed = true 39 + AND sort_date > now() - interval '7 days' 40 + ORDER BY 41 + (bsky_like_count + recommend_count * 5)::numeric 42 + / power(extract(epoch from (now() - sort_date)) / 3600 + 2, 1.5) DESC 43 + LIMIT 50 44 + `); 45 + uris = ranked.rows.map((row: any) => row.uri as string); 46 + } finally { 47 + client.release(); 48 + } 49 + 50 + if (uris.length === 0) { 51 + return { posts: [] }; 52 + } 53 + 54 + // Batch-fetch documents with publication joins and interaction counts 55 + const { data: documents } = await supabaseServerClient 56 + .from("documents") 57 + .select( 58 + `*, 59 + comments_on_documents(count), 60 + document_mentions_in_bsky(count), 61 + recommends_on_documents(count), 62 + documents_in_publications(publications(*))`, 63 + ) 64 + .in("uri", uris); 65 + 66 + // Build lookup map for enrichment 67 + const docMap = new Map((documents || []).map((d) => [d.uri, d])); 68 + 69 + // Process in ranked order, deduplicating by identity key (DID/rkey) 70 + const seen = new Set<string>(); 71 + const orderedDocs: NonNullable<typeof documents>[number][] = []; 72 + for (const uri of uris) { 73 + try { 74 + const parsed = new AtUri(uri); 75 + const identityKey = `${parsed.host}/${parsed.rkey}`; 76 + if (seen.has(identityKey)) continue; 77 + seen.add(identityKey); 78 + } catch { 79 + // invalid URI, skip dedup check 80 + } 81 + const doc = docMap.get(uri); 82 + if (doc) orderedDocs.push(doc); 83 + } 84 + 85 + // Enrich into Post[] 86 + const posts = ( 87 + await Promise.all( 88 + orderedDocs.map((doc) => enrichDocumentToPost(doc as any)), 89 + ) 90 + ).filter((post): post is Post => post !== null); 91 + 92 + const response = { posts }; 93 + 94 + // Cache in Redis 95 + if (redisClient) { 96 + await redisClient.setex(CACHE_KEY, CACHE_TTL, JSON.stringify(response)); 97 + } 98 + 99 + return response; 100 + }
+54
app/(home-pages)/reader/getNewFeed.ts
··· 1 + "use server"; 2 + 3 + import { supabaseServerClient } from "supabase/serverClient"; 4 + import { deduplicateByUriOrdered } from "src/utils/deduplicateRecords"; 5 + import { enrichDocumentToPost } from "./enrichPost"; 6 + import type { Cursor, Post } from "./getReaderFeed"; 7 + 8 + export async function getNewFeed( 9 + cursor?: Cursor | null, 10 + ): Promise<{ posts: Post[]; nextCursor: Cursor | null }> { 11 + let query = supabaseServerClient 12 + .from("documents") 13 + .select( 14 + `*, 15 + comments_on_documents(count), 16 + document_mentions_in_bsky(count), 17 + recommends_on_documents(count), 18 + documents_in_publications!inner(publications!inner(*))`, 19 + ) 20 + .or( 21 + "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", 22 + { referencedTable: "documents_in_publications.publications" }, 23 + ) 24 + .order("sort_date", { ascending: false }) 25 + .order("uri", { ascending: false }) 26 + .limit(25); 27 + 28 + if (cursor) { 29 + query = query.or( 30 + `sort_date.lt.${cursor.timestamp},and(sort_date.eq.${cursor.timestamp},uri.lt.${cursor.uri})`, 31 + ); 32 + } 33 + 34 + let { data: rawFeed, error } = await query; 35 + 36 + const feed = deduplicateByUriOrdered(rawFeed || []); 37 + 38 + let posts = ( 39 + await Promise.all(feed.map((post) => enrichDocumentToPost(post as any))) 40 + ).filter((post): post is Post => post !== null); 41 + 42 + const nextCursor = 43 + posts.length > 0 44 + ? { 45 + timestamp: posts[posts.length - 1].documents.sort_date, 46 + uri: posts[posts.length - 1].documents.uri, 47 + } 48 + : null; 49 + 50 + return { 51 + posts, 52 + nextCursor, 53 + }; 54 + }
+7 -44
app/(home-pages)/reader/getReaderFeed.ts
··· 1 1 "use server"; 2 2 3 3 import { getIdentityData } from "actions/getIdentityData"; 4 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 4 import { supabaseServerClient } from "supabase/serverClient"; 6 - import { IdResolver } from "@atproto/identity"; 7 - import type { DidCache, CacheResult, DidDocument } from "@atproto/identity"; 8 - import Client from "ioredis"; 9 - import { AtUri } from "@atproto/api"; 10 - import { idResolver } from "./idResolver"; 11 - import { 12 - normalizeDocumentRecord, 13 - normalizePublicationRecord, 14 - type NormalizedDocument, 15 - type NormalizedPublication, 5 + import type { 6 + NormalizedDocument, 7 + NormalizedPublication, 16 8 } from "src/utils/normalizeRecords"; 17 9 import { deduplicateByUriOrdered } from "src/utils/deduplicateRecords"; 10 + import { enrichDocumentToPost } from "./enrichPost"; 18 11 19 12 export type Cursor = { 20 13 timestamp: string; ··· 53 46 const feed = deduplicateByUriOrdered(rawFeed || []); 54 47 55 48 let posts = ( 56 - await Promise.all( 57 - feed.map(async (post) => { 58 - let pub = post.documents_in_publications[0].publications!; 59 - let uri = new AtUri(post.uri); 60 - let handle = await idResolver.did.resolve(uri.host); 61 - 62 - // Normalize records - filter out unrecognized formats 63 - const normalizedData = normalizeDocumentRecord(post.data, post.uri); 64 - if (!normalizedData) return null; 49 + await Promise.all(feed.map((post) => enrichDocumentToPost(post as any))) 50 + ).filter((post): post is Post => post !== null); 65 51 66 - const normalizedPubRecord = normalizePublicationRecord(pub?.record); 67 - 68 - let p: Post = { 69 - publication: { 70 - href: getPublicationURL(pub), 71 - pubRecord: normalizedPubRecord, 72 - uri: pub?.uri || "", 73 - }, 74 - author: handle?.alsoKnownAs?.[0] 75 - ? `@${handle.alsoKnownAs[0].slice(5)}` 76 - : null, 77 - documents: { 78 - comments_on_documents: post.comments_on_documents, 79 - document_mentions_in_bsky: post.document_mentions_in_bsky, 80 - recommends_on_documents: post.recommends_on_documents, 81 - data: normalizedData, 82 - uri: post.uri, 83 - sort_date: post.sort_date, 84 - }, 85 - }; 86 - return p; 87 - }) || [], 88 - ) 89 - ).filter((post): post is Post => post !== null); 90 52 const nextCursor = 91 53 posts.length > 0 92 54 ? { ··· 115 77 comments_on_documents: { count: number }[] | undefined; 116 78 document_mentions_in_bsky: { count: number }[] | undefined; 117 79 recommends_on_documents: { count: number }[] | undefined; 80 + mentionsCount?: number; 118 81 }; 119 82 };
+6 -4
app/(home-pages)/reader/getSubscriptions.ts
··· 29 29 30 30 let query = supabaseServerClient 31 31 .from("publication_subscriptions") 32 - .select(`*, publications(*, documents_in_publications(*, documents(*)))`) 32 + .select( 33 + `*, publications(*, publication_subscriptions(*), documents_in_publications(*, documents(*)))`, 34 + ) 33 35 .order(`created_at`, { ascending: false }) 34 36 .order(`uri`, { ascending: false }) 35 37 .order("documents(sort_date)", { ··· 51 53 await Promise.all( 52 54 pubs?.map(async (pub) => { 53 55 const normalizedRecord = normalizePublicationRecord( 54 - pub.publications?.record 56 + pub.publications?.record, 55 57 ); 56 58 if (!normalizedRecord) return null; 57 59 let id = await idResolver.did.resolve(pub.publications?.identity_did!); ··· 62 64 ? { handle: `@${id.alsoKnownAs[0].slice(5)}` } 63 65 : undefined, 64 66 } as PublicationSubscription; 65 - }) || [] 67 + }) || [], 66 68 ) 67 69 ).filter((sub): sub is PublicationSubscription => sub !== null); 68 - 69 70 const nextCursor = 70 71 pubs && pubs.length > 0 71 72 ? { ··· 83 84 export type PublicationSubscription = { 84 85 authorProfile?: { handle: string }; 85 86 record: NormalizedPublication; 87 + publication_subscriptions: { identity: string }[]; 86 88 uri: string; 87 89 documents_in_publications: { 88 90 documents: { data?: Json; sort_date: string } | null;
+7
app/(home-pages)/reader/hot/page.tsx
··· 1 + import { getHotFeed } from "../getHotFeed"; 2 + import { GlobalContent } from "../GlobalContent"; 3 + 4 + export default async function HotPage() { 5 + const feedPromise = getHotFeed(); 6 + return <GlobalContent promise={feedPromise} />; 7 + }
+79
app/(home-pages)/reader/layout.tsx
··· 1 + "use client"; 2 + 3 + import { usePathname } from "next/navigation"; 4 + import Link from "next/link"; 5 + import { Header } from "components/PageHeader"; 6 + import { Footer } from "components/ActionBar/Footer"; 7 + import { DesktopNavigation } from "components/ActionBar/DesktopNavigation"; 8 + import { MobileNavigation } from "components/ActionBar/MobileNavigation"; 9 + import { MediaContents } from "components/Media"; 10 + import { DashboardIdContext } from "components/PageLayouts/DashboardLayout"; 11 + import { useIdentityData } from "components/IdentityProvider"; 12 + 13 + const allTabs = [ 14 + { name: "Subs", href: "/reader", requiresAuth: true }, 15 + { name: "What's Hot", href: "/reader/hot", requiresAuth: false }, 16 + { name: "New", href: "/reader/new", requiresAuth: false }, 17 + ]; 18 + 19 + export default function ReaderLayout({ 20 + children, 21 + }: { 22 + children: React.ReactNode; 23 + }) { 24 + const pathname = usePathname(); 25 + const { identity } = useIdentityData(); 26 + const isLoggedIn = !!identity?.atp_did; 27 + const tabs = allTabs.filter((tab) => !tab.requiresAuth || isLoggedIn); 28 + 29 + const isActive = (href: string) => { 30 + if (href === "/reader") 31 + return pathname === "/reader" || pathname === "/"; 32 + if ( 33 + href === "/reader/hot" && 34 + !isLoggedIn && 35 + (pathname === "/reader" || pathname === "/") 36 + ) 37 + return true; 38 + return pathname.startsWith(href); 39 + }; 40 + 41 + return ( 42 + <DashboardIdContext.Provider value="reader"> 43 + <div className="dashboard pwa-padding relative max-w-(--breakpoint-lg) w-full h-full mx-auto flex sm:flex-row flex-col sm:items-stretch sm:px-6"> 44 + <MediaContents mobile={false}> 45 + <div className="flex flex-col gap-3 my-6"> 46 + <DesktopNavigation currentPage="reader" /> 47 + </div> 48 + </MediaContents> 49 + <div 50 + className="w-full h-full flex flex-col gap-2 relative overflow-y-scroll pt-3 pb-3 px-3 sm:pt-8 sm:pb-3 sm:pl-6 sm:pr-4" 51 + id="home-content" 52 + > 53 + <Header> 54 + <div className="pubDashTabs flex flex-row gap-1"> 55 + {tabs.map((tab) => ( 56 + <Link key={tab.name} href={tab.href}> 57 + <div 58 + className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${ 59 + isActive(tab.href) 60 + ? "text-accent-2 bg-accent-1 font-bold -mb-px" 61 + : "text-tertiary" 62 + }`} 63 + > 64 + {tab.name} 65 + </div> 66 + </Link> 67 + ))} 68 + </div> 69 + <div className="sm:block grow" /> 70 + </Header> 71 + {children} 72 + </div> 73 + <Footer> 74 + <MobileNavigation currentPage="reader" /> 75 + </Footer> 76 + </div> 77 + </DashboardIdContext.Provider> 78 + ); 79 + }
+4
app/(home-pages)/reader/loading.tsx
··· 1 + import { FeedSkeleton } from "./FeedSkeleton"; 2 + export default function Loading() { 3 + return <FeedSkeleton />; 4 + }
+7
app/(home-pages)/reader/new/page.tsx
··· 1 + import { getNewFeed } from "../getNewFeed"; 2 + import { NewContent } from "../NewContent"; 3 + 4 + export default async function NewPage() { 5 + const feedPromise = getNewFeed(); 6 + return <NewContent promise={feedPromise} />; 7 + }
+12 -33
app/(home-pages)/reader/page.tsx
··· 1 1 import { getIdentityData } from "actions/getIdentityData"; 2 + import { getReaderFeed } from "./getReaderFeed"; 3 + import { getHotFeed } from "./getHotFeed"; 4 + import { InboxContent } from "./InboxContent"; 5 + import { GlobalContent } from "./GlobalContent"; 2 6 3 - import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 4 - import { ReaderContent } from "./ReaderContent"; 5 - import { SubscriptionsContent } from "./SubscriptionsContent"; 6 - import { getReaderFeed } from "./getReaderFeed"; 7 - import { getSubscriptions } from "./getSubscriptions"; 7 + export default async function Reader() { 8 + let identityData = await getIdentityData(); 9 + if (!identityData?.atp_did) { 10 + const feedPromise = getHotFeed(); 11 + return <GlobalContent promise={feedPromise} />; 12 + } 8 13 9 - export default async function Reader(props: {}) { 10 - let posts = await getReaderFeed(); 11 - let publications = await getSubscriptions(); 12 - return ( 13 - <DashboardLayout 14 - id="reader" 15 - currentPage="reader" 16 - defaultTab="Read" 17 - actions={null} 18 - tabs={{ 19 - Read: { 20 - controls: null, 21 - content: ( 22 - <ReaderContent nextCursor={posts.nextCursor} posts={posts.posts} /> 23 - ), 24 - }, 25 - Subscriptions: { 26 - controls: null, 27 - content: ( 28 - <SubscriptionsContent 29 - publications={publications.subscriptions} 30 - nextCursor={publications.nextCursor} 31 - /> 32 - ), 33 - }, 34 - }} 35 - /> 36 - ); 14 + const feedPromise = getReaderFeed(); 15 + return <InboxContent promise={feedPromise} />; 37 16 }
+13 -9
app/[leaflet_id]/Footer.tsx
··· 1 1 "use client"; 2 2 import { useUIState } from "src/useUIState"; 3 - import { Footer as ActionFooter } from "components/ActionBar/Footer"; 3 + import { Footer } from "components/ActionBar/Footer"; 4 4 import { Media } from "components/Media"; 5 5 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 6 6 import { Toolbar } from "components/Toolbar"; ··· 36 36 .value; 37 37 38 38 return ( 39 - <Media mobile className="mobileFooter w-full z-10 touch-none -mt-[54px] "> 39 + <Media 40 + mobile 41 + className="mobileLeafletFooter w-full z-10 touch-none -mt-[54px] " 42 + > 40 43 {focusedBlock && 41 44 focusedBlock.entityType == "block" && 42 45 hasBlockToolbar(blockType) && ··· 54 57 /> 55 58 </div> 56 59 ) : entity_set.permissions.write ? ( 57 - <ActionFooter> 60 + <Footer> 58 61 {pub?.publications && 59 62 identity?.atp_did && 60 63 pub.publications.identity_did === identity.atp_did ? ( ··· 62 65 ) : ( 63 66 <HomeButton /> 64 67 )} 65 - 66 - <PublishButton entityID={props.entityID} /> 67 - <ShareOptions /> 68 - <PostSettings /> 69 - <ThemePopover entityID={props.entityID} /> 70 - </ActionFooter> 68 + <div className="mobileLeafletActions flex gap-2 shrink-0"> 69 + <PublishButton entityID={props.entityID} /> 70 + <ShareOptions /> 71 + <PostSettings /> 72 + <ThemePopover entityID={props.entityID} /> 73 + </div> 74 + </Footer> 71 75 ) : ( 72 76 <div className="pb-2 px-2 z-10 flex justify-end"> 73 77 <Watermark mobile />
-2
app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
··· 410 410 range: { from: number; to: number }, 411 411 view: EditorView, 412 412 ) => { 413 - console.log("view", view); 414 413 if (!view) return; 415 414 const { from, to } = range; 416 415 const tr = view.state.tr; ··· 437 436 }); 438 437 tr.insert(from, atMentionNode); 439 438 } 440 - console.log("yo", mention); 441 439 442 440 // Add a space after the mention 443 441 tr.insertText(" ", from + 1);
+90
app/api/rpc/[command]/get_document_interactions.ts
··· 1 + import { z } from "zod"; 2 + import { makeRoute } from "../lib"; 3 + import type { Env } from "./route"; 4 + import { getConstellationBacklinks } from "app/lish/[did]/[publication]/[rkey]/getPostPageData"; 5 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 6 + import { 7 + normalizeDocumentRecord, 8 + normalizePublicationRecord, 9 + } from "src/utils/normalizeRecords"; 10 + 11 + export const get_document_interactions = makeRoute({ 12 + route: "get_document_interactions", 13 + input: z.object({ 14 + document_uri: z.string(), 15 + }), 16 + handler: async ( 17 + { document_uri }, 18 + { supabase }: Pick<Env, "supabase">, 19 + ) => { 20 + let { data: document } = await supabase 21 + .from("documents") 22 + .select( 23 + ` 24 + data, 25 + uri, 26 + comments_on_documents(*, bsky_profiles(*)), 27 + document_mentions_in_bsky(*), 28 + documents_in_publications(publications(*)) 29 + `, 30 + ) 31 + .eq("uri", document_uri) 32 + .limit(1) 33 + .single(); 34 + 35 + if (!document) { 36 + return { comments: [], quotesAndMentions: [], totalMentionsCount: 0 }; 37 + } 38 + 39 + const normalizedData = normalizeDocumentRecord( 40 + document.data, 41 + document.uri, 42 + ); 43 + 44 + const pub = document.documents_in_publications?.[0]?.publications; 45 + const normalizedPubRecord = pub 46 + ? normalizePublicationRecord(pub.record) 47 + : null; 48 + 49 + // Compute document URL for constellation lookup 50 + let absoluteUrl = ""; 51 + if (normalizedData) { 52 + const postUrl = getDocumentURL( 53 + normalizedData, 54 + document.uri, 55 + normalizedPubRecord, 56 + ); 57 + absoluteUrl = postUrl.startsWith("/") 58 + ? `https://leaflet.pub${postUrl}` 59 + : postUrl; 60 + } 61 + 62 + // Fetch constellation backlinks 63 + const constellationBacklinks = absoluteUrl 64 + ? await getConstellationBacklinks(absoluteUrl) 65 + : []; 66 + 67 + // Deduplicate constellation backlinks internally 68 + const uniqueBacklinks = Array.from( 69 + new Map(constellationBacklinks.map((b) => [b.uri, b])).values(), 70 + ); 71 + 72 + // Combine DB mentions and constellation backlinks, deduplicating by URI 73 + const dbMentionUris = new Set( 74 + document.document_mentions_in_bsky.map((m) => m.uri), 75 + ); 76 + const quotesAndMentions: { uri: string; link?: string }[] = [ 77 + ...document.document_mentions_in_bsky.map((m) => ({ 78 + uri: m.uri, 79 + link: m.link, 80 + })), 81 + ...uniqueBacklinks.filter((b) => !dbMentionUris.has(b.uri)), 82 + ]; 83 + 84 + return { 85 + comments: document.comments_on_documents, 86 + quotesAndMentions, 87 + totalMentionsCount: quotesAndMentions.length, 88 + }; 89 + }, 90 + });
+17
app/api/rpc/[command]/get_hot_feed.ts
··· 1 + import { z } from "zod"; 2 + import { makeRoute } from "../lib"; 3 + import type { Env } from "./route"; 4 + import { getHotFeed } from "app/(home-pages)/reader/getHotFeed"; 5 + import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 6 + 7 + export type GetHotFeedReturnType = Awaited< 8 + ReturnType<(typeof get_hot_feed)["handler"]> 9 + >; 10 + 11 + export const get_hot_feed = makeRoute({ 12 + route: "get_hot_feed", 13 + input: z.object({}), 14 + handler: async ({}, {}: Pick<Env, "supabase">) => { 15 + return await getHotFeed(); 16 + }, 17 + });
+4
app/api/rpc/[command]/route.ts
··· 15 15 import { search_publication_documents } from "./search_publication_documents"; 16 16 import { get_profile_data } from "./get_profile_data"; 17 17 import { get_user_recommendations } from "./get_user_recommendations"; 18 + import { get_hot_feed } from "./get_hot_feed"; 19 + import { get_document_interactions } from "./get_document_interactions"; 18 20 19 21 let supabase = createClient<Database>( 20 22 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 43 45 search_publication_documents, 44 46 get_profile_data, 45 47 get_user_recommendations, 48 + get_hot_feed, 49 + get_document_interactions, 46 50 ]; 47 51 export async function POST( 48 52 req: Request,
+16
app/api/update-nav-state/route.ts
··· 1 + import { NextRequest, NextResponse } from "next/server"; 2 + 3 + export async function POST(request: NextRequest) { 4 + const { state } = (await request.json()) as { state: string }; 5 + if (state !== "home" && state !== "reader") { 6 + return NextResponse.json({ error: "Invalid state" }, { status: 400 }); 7 + } 8 + 9 + const response = NextResponse.json({ ok: true }); 10 + response.cookies.set("nav-state", state, { 11 + path: "/", 12 + sameSite: "lax", 13 + maxAge: 60 * 60 * 24 * 365, 14 + }); 15 + return response; 16 + }
+47 -9
app/lish/Subscribe.tsx
··· 24 24 import LoginForm from "app/login/LoginForm"; 25 25 import { RSSSmall } from "components/Icons/RSSSmall"; 26 26 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 27 + import { RSSTiny } from "components/Icons/RSSTiny"; 27 28 28 29 export const SubscribeWithBluesky = (props: { 30 + compact?: boolean; 29 31 pubName: string; 30 32 pub_uri: string; 31 33 base_url: string; ··· 36 38 let [successModalOpen, setSuccessModalOpen] = useState( 37 39 !!searchParams.has("showSubscribeSuccess"), 38 40 ); 41 + let [localSubscribeState, setLocalSubscribeState] = useState< 42 + "subscribed" | "unsubscribed" 43 + >("subscribed"); 39 44 let subscribed = 40 45 identity?.atp_did && 46 + localSubscribeState !== "unsubscribed" && 41 47 props.subscribers.find((s) => s.identity === identity.atp_did); 42 48 43 49 if (successModalOpen) ··· 48 54 /> 49 55 ); 50 56 if (subscribed) { 51 - return <ManageSubscription {...props} />; 57 + return ( 58 + <ManageSubscription 59 + {...props} 60 + onUnsubscribe={() => setLocalSubscribeState("unsubscribed")} 61 + /> 62 + ); 52 63 } 53 64 return ( 54 65 <div className="flex flex-col gap-2 text-center justify-center"> 55 66 <div className="flex flex-row gap-2 place-self-center"> 56 67 <BlueskySubscribeButton 68 + setLocalSubscribeState={() => setLocalSubscribeState("subscribed")} 69 + compact={props.compact} 57 70 pub_uri={props.pub_uri} 58 71 setSuccessModalOpen={setSuccessModalOpen} 59 72 /> ··· 63 76 target="_blank" 64 77 aria-label="Subscribe to RSS" 65 78 > 66 - <RSSSmall className="self-center" aria-hidden /> 79 + {props.compact ? ( 80 + <RSSTiny className="self-center" aria-hidden /> 81 + ) : ( 82 + <RSSSmall className="self-center" aria-hidden /> 83 + )} 67 84 </a> 68 85 </div> 69 86 </div> ··· 74 91 pub_uri: string; 75 92 subscribers: { identity: string }[]; 76 93 base_url: string; 94 + compact?: boolean; 95 + onUnsubscribe?: () => void; 77 96 }) => { 78 97 let toaster = useToaster(); 79 98 let [hasFeed] = useState(false); ··· 83 102 content: "You unsubscribed.", 84 103 type: "success", 85 104 }); 105 + props.onUnsubscribe?.(); 86 106 }, null); 87 107 return ( 88 108 <Popover 89 109 trigger={ 90 - <div className="text-accent-contrast text-sm w-fit"> 110 + <div 111 + className={`text-accent-contrast w-fit ${props.compact ? "text-xs" : "text-sm"}`} 112 + > 91 113 Manage Subscription 92 114 </div> 93 115 } 94 116 > 95 - <div className="max-w-sm flex flex-col gap-1"> 117 + <div 118 + className={`max-w-sm flex flex-col gap-1 ${props.compact && "text-sm"}`} 119 + > 96 120 <h4>Update Options</h4> 97 121 98 122 {!hasFeed && ( ··· 102 126 className=" place-self-center" 103 127 > 104 128 <ButtonPrimary fullWidth compact className="!px-4"> 105 - View Bluesky Custom Feed 129 + Bluesky Custom Feed 106 130 </ButtonPrimary> 107 131 </a> 108 132 )} ··· 121 145 <hr className="border-border-light my-1" /> 122 146 123 147 <form action={unsubscribe}> 124 - <button className="font-bold text-accent-contrast w-max place-self-center"> 125 - {unsubscribePending ? <DotLoader /> : "Unsubscribe"} 148 + <button className="font-bold w-full text-accent-contrast text-center mx-auto"> 149 + {unsubscribePending ? ( 150 + <DotLoader className="w-fit mx-auto" /> 151 + ) : ( 152 + "Unsubscribe" 153 + )} 126 154 </button> 127 155 </form> 128 156 </div> ··· 133 161 let BlueskySubscribeButton = (props: { 134 162 pub_uri: string; 135 163 setSuccessModalOpen: (open: boolean) => void; 164 + compact?: boolean; 165 + setLocalSubscribeState: () => void; 136 166 }) => { 137 167 let { identity } = useIdentityData(); 138 168 let toaster = useToaster(); ··· 155 185 props.setSuccessModalOpen(true); 156 186 } 157 187 toaster({ content: <div>You're Subscribed!</div>, type: "success" }); 188 + props.setLocalSubscribeState(); 158 189 }, null); 159 190 160 191 let [isClient, setIsClient] = useState(false); ··· 166 197 return ( 167 198 <Popover 168 199 asChild 200 + className="max-w-xs" 169 201 trigger={ 170 - <ButtonPrimary className="place-self-center"> 202 + <ButtonPrimary 203 + compact={props.compact} 204 + className={`place-self-center ${props.compact && "text-sm"}`} 205 + > 171 206 <BlueskyTiny /> Subscribe with Bluesky 172 207 </ButtonPrimary> 173 208 } ··· 190 225 action={subscribe} 191 226 className="place-self-center flex flex-row gap-1" 192 227 > 193 - <ButtonPrimary> 228 + <ButtonPrimary 229 + compact={props.compact} 230 + className={props.compact ? "text-sm" : ""} 231 + > 194 232 {subscribePending ? ( 195 233 <DotLoader /> 196 234 ) : (
+13 -19
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 29 29 document_uri: string; 30 30 comments: Comment[]; 31 31 pageId?: string; 32 + noCommentBox?: boolean; 32 33 }) { 33 34 let { identity } = useIdentityData(); 34 35 let { localComments } = useInteractionState(props.document_uri); ··· 55 56 id={"commentsDrawer"} 56 57 className="flex flex-col gap-2 relative text-sm text-secondary" 57 58 > 58 - <div className="w-full flex justify-between"> 59 - <h4> Comments</h4> 60 - <button 61 - className="text-tertiary" 62 - onClick={() => 63 - setInteractionState(props.document_uri, { drawerOpen: false }) 64 - } 65 - > 66 - <CloseTiny /> 67 - </button> 68 - </div> 69 - {identity?.atp_did ? ( 70 - <CommentBox doc_uri={props.document_uri} pageId={props.pageId} /> 71 - ) : ( 72 - <div className="w-full accent-container text-tertiary text-center italic p-3 flex flex-col gap-2"> 73 - Connect a Bluesky account to comment 74 - <BlueskyLogin redirectRoute={redirectRoute} /> 75 - </div> 59 + {!props.noCommentBox && ( 60 + <> 61 + {identity?.atp_did ? ( 62 + <CommentBox doc_uri={props.document_uri} pageId={props.pageId} /> 63 + ) : ( 64 + <div className="w-full accent-container text-tertiary text-center italic p-3 flex flex-col gap-2"> 65 + Connect a Bluesky account to comment 66 + <BlueskyLogin redirectRoute={redirectRoute} /> 67 + </div> 68 + )} 69 + <hr className="border-border-light" /> 70 + </> 76 71 )} 77 - <hr className="border-border-light" /> 78 72 <div className="flex flex-col gap-4 py-2"> 79 73 {comments 80 74 .sort((a, b) => {
+41 -11
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 1 1 "use client"; 2 2 import { Media } from "components/Media"; 3 3 import { MentionsDrawerContent } from "./Quotes"; 4 - import { InteractionState, useInteractionState } from "./Interactions"; 4 + import { 5 + InteractionState, 6 + setInteractionState, 7 + useInteractionState, 8 + } from "./Interactions"; 5 9 import { Json } from "supabase/database.types"; 6 10 import { Comment, CommentsDrawerContent } from "./Comments"; 7 11 import { useSearchParams } from "next/navigation"; 8 12 import { SandwichSpacer } from "components/LeafletLayout"; 9 13 import { decodeQuotePosition } from "../quotePosition"; 14 + import { CloseTiny } from "components/Icons/CloseTiny"; 10 15 11 16 export const InteractionDrawer = (props: { 12 17 showPageBackground: boolean | undefined; ··· 39 44 <div className="snap-center h-full flex z-10 shrink-0 sm:max-w-prose sm:w-full w-[calc(100vw-12px)]"> 40 45 <div 41 46 id="interaction-drawer" 42 - className={`opaque-container h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`} 47 + className={`opaque-container relative h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll flex flex-col ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`} 43 48 > 44 49 {drawer.drawer === "quotes" ? ( 45 - <MentionsDrawerContent 46 - {...props} 47 - quotesAndMentions={filteredQuotesAndMentions} 48 - /> 50 + <> 51 + <button 52 + className="text-tertiary absolute top-4 right-4" 53 + onClick={() => 54 + setInteractionState(props.document_uri, { drawerOpen: false }) 55 + } 56 + > 57 + <CloseTiny /> 58 + </button> 59 + <MentionsDrawerContent 60 + {...props} 61 + quotesAndMentions={filteredQuotesAndMentions} 62 + /> 63 + </> 49 64 ) : ( 50 - <CommentsDrawerContent 51 - document_uri={props.document_uri} 52 - comments={filteredComments} 53 - pageId={props.pageId} 54 - /> 65 + <> 66 + <div className="w-full flex justify-between"> 67 + <h4> Comments</h4> 68 + <button 69 + className="text-tertiary" 70 + onClick={() => 71 + setInteractionState(props.document_uri, { 72 + drawerOpen: false, 73 + }) 74 + } 75 + > 76 + <CloseTiny /> 77 + </button> 78 + </div> 79 + <CommentsDrawerContent 80 + document_uri={props.document_uri} 81 + comments={filteredComments} 82 + pageId={props.pageId} 83 + /> 84 + </> 55 85 )} 56 86 </div> 57 87 </div>
+2 -8
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 86 86 }); 87 87 88 88 return ( 89 - <div className="relative w-full flex justify-between "> 90 - <button 91 - className="text-tertiary absolute top-0 right-0" 92 - onClick={() => setInteractionState(document_uri, { drawerOpen: false })} 93 - > 94 - <CloseTiny /> 95 - </button> 89 + <> 96 90 {props.quotesAndMentions.length === 0 ? ( 97 91 <div className="opaque-container flex flex-col gap-0.5 p-[6px] text-tertiary italic text-sm text-center"> 98 92 <div className="font-bold">no quotes yet!</div> ··· 160 154 )} 161 155 </div> 162 156 )} 163 - </div> 157 + </> 164 158 ); 165 159 }; 166 160
-1
app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx
··· 298 298 e.stopPropagation(); 299 299 300 300 props.toggleCollapsed(parentPostUri); 301 - console.log("reply clicked"); 302 301 }} 303 302 /> 304 303 </>
-4
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 3 3 import { 4 4 normalizeDocumentRecord, 5 5 normalizePublicationRecord, 6 - type NormalizedDocument, 7 - type NormalizedPublication, 8 6 } from "src/utils/normalizeRecords"; 9 7 import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; 10 8 import { documentUriFilter } from "src/utils/uriHelpers"; ··· 145 143 publication_subscriptions: rawPub.publication_subscriptions || [], 146 144 } 147 145 : null; 148 - 149 - // Get recommends count from the aggregated query result 150 146 const recommendsCount = document.recommends_on_documents?.[0]?.count ?? 0; 151 147 152 148 return {
+1
app/lish/[did]/[publication]/dashboard/Actions.tsx
··· 38 38 icon=<ShareSmall /> 39 39 label="Share" 40 40 onClick={() => {}} 41 + smallOnMobile 41 42 /> 42 43 } 43 44 >
+2 -8
app/lish/[did]/[publication]/dashboard/DraftList.tsx
··· 1 1 "use client"; 2 2 3 - import { NewDraftSecondaryButton } from "./NewDraftButton"; 4 3 import React from "react"; 5 4 import { 6 5 usePublicationData, ··· 22 21 if (!normalizedPubRecord) return null; 23 22 24 23 return ( 25 - <div className="flex flex-col gap-4"> 26 - <NewDraftSecondaryButton 27 - fullWidth 28 - publication={pub_data?.publication?.uri} 29 - /> 30 - 24 + <div className="flex flex-col"> 31 25 <LeafletList 32 26 searchValue={props.searchValue} 33 27 showPreview={false} ··· 60 54 ), 61 55 }} 62 56 /> 63 - <div className="spacer h-12 w-full bg-transparent shrink-0 " /> 57 + <div className="spacer h-16 w-full bg-transparent shrink-0 " /> 64 58 </div> 65 59 ); 66 60 }
+3 -23
app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx
··· 16 16 let newLeaflet = await createPublicationDraft(props.publication); 17 17 router.push(`/${newLeaflet}`); 18 18 }} 19 - icon=<AddTiny className="m-1 shrink-0" /> 20 - label="New" 19 + icon=<AddTiny className="sm:m-1 shrink-0 sm:scale-100 scale-75" /> 20 + smallOnMobile 21 + label="Draft" 21 22 /> 22 23 ); 23 24 } 24 - 25 - export function NewDraftSecondaryButton(props: { 26 - publication: string; 27 - fullWidth?: boolean; 28 - }) { 29 - let router = useRouter(); 30 - 31 - return ( 32 - <ButtonSecondary 33 - fullWidth={props.fullWidth} 34 - id="new-leaflet-button" 35 - onClick={async () => { 36 - let newLeaflet = await createPublicationDraft(props.publication); 37 - router.push(`/${newLeaflet}`); 38 - }} 39 - > 40 - <AddTiny className="m-1 shrink-0" /> 41 - <span>New Draft</span> 42 - </ButtonSecondary> 43 - ); 44 - }
+1
app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
··· 76 76 actions={<Actions publication={publication.uri} />} 77 77 currentPage="pub" 78 78 publication={publication.uri} 79 + pageTitle={record.name} 79 80 /> 80 81 ); 81 82 }
+3 -3
app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx
··· 12 12 13 13 // Derive all types from the RPC return type 14 14 export type PublicationData = GetPublicationDataReturnType["result"]; 15 - export type PublishedDocument = NonNullable<PublicationData>["documents"][number]; 15 + export type PublishedDocument = 16 + NonNullable<PublicationData>["documents"][number]; 16 17 export type PublicationDraft = NonNullable<PublicationData>["drafts"][number]; 17 18 18 19 const PublicationContext = createContext({ name: "", did: "" }); ··· 24 25 }) { 25 26 let key = `publication-data-${props.publication_did}-${props.publication_rkey}`; 26 27 useEffect(() => { 27 - console.log("UPDATING"); 28 28 mutate(key, props.publication_data); 29 29 }, [props.publication_data]); 30 30 return ( ··· 64 64 const { data } = usePublicationData(); 65 65 return useMemo( 66 66 () => normalizePublicationRecord(data?.publication?.record), 67 - [data?.publication?.record] 67 + [data?.publication?.record], 68 68 ); 69 69 } 70 70
+1
app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
··· 34 34 id="pub-settings-button" 35 35 icon=<SettingsSmall /> 36 36 label="Settings" 37 + smallOnMobile 37 38 /> 38 39 } 39 40 >
-11
app/route.ts
··· 1 - import { createNewLeaflet } from "actions/createNewLeaflet"; 2 - import { cookies } from "next/headers"; 3 - import { redirect } from "next/navigation"; 4 - 5 - export const preferredRegion = ["sfo1"]; 6 - export const dynamic = "force-dynamic"; 7 - export const fetchCache = "force-no-store"; 8 - 9 - export async function GET() { 10 - redirect("/home"); 11 - }
+12 -8
components/ActionBar/ActionButton.tsx
··· 19 19 className?: string; 20 20 subtext?: string; 21 21 labelOnMobile?: boolean; 22 + smallOnMobile?: boolean; 22 23 z?: boolean; 23 24 } 24 25 >((_props, ref) => { ··· 30 31 secondary, 31 32 nav, 32 33 labelOnMobile, 34 + smallOnMobile, 33 35 subtext, 34 36 className, 35 37 ...buttonProps ··· 55 57 className={` 56 58 actionButton relative font-bold 57 59 rounded-md border 58 - flex gap-2 items-start sm:justify-start justify-center 59 - p-1 sm:mx-0 60 - ${showLabelOnMobile && !secondary ? "w-full" : "sm:w-full w-max"} 60 + flex gap-2 items-center justify-start 61 + sm:w-full sm:max-w-full p-1 62 + w-max 63 + ${smallOnMobile && "sm:text-base text-sm py-0! sm:py-1! sm:h-fit h-6"} 61 64 ${ 62 65 primary 63 - ? "bg-accent-1 border-accent-1 text-accent-2 transparent-outline sm:hover:outline-accent-contrast focus:outline-accent-1 outline-offset-1 mx-1 first:ml-0" 66 + ? "bg-accent-1 border-accent-1 text-accent-2 transparent-outline sm:hover:outline-accent-contrast focus:outline-accent-1 outline-offset-1 " 64 67 : secondary 65 - ? " bg-bg-page border-accent-contrast text-accent-contrast transparent-outline focus:outline-accent-contrast sm:hover:outline-accent-contrast outline-offset-1 mx-1 first:ml-0" 68 + ? " bg-bg-page border-accent-contrast text-accent-contrast transparent-outline focus:outline-accent-contrast sm:hover:outline-accent-contrast outline-offset-1" 66 69 : nav 67 - ? "border-transparent text-secondary sm:hover:border-border justify-start!" 70 + ? "border-transparent text-secondary sm:hover:border-border justify-start! max-w-full" 68 71 : "border-transparent text-accent-contrast sm:hover:border-accent-contrast" 69 72 } 70 73 ${className} 71 74 `} 72 75 > 73 - <div className="shrink-0">{icon}</div> 76 + <div className="shrink-0 flex flex-row gap-0.5">{icon}</div> 74 77 <div 75 - className={`flex flex-col pr-1 ${subtext && "leading-snug"} max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 78 + className={`flex flex-col ${subtext && "leading-snug"} sm:max-w-full min-w-0 mr-1 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 79 + style={{ width: "-webkit-fill-available" }} 76 80 > 77 81 <div className="truncate text-left">{label}</div> 78 82 {subtext && (
+66
components/ActionBar/DesktopNavigation.tsx
··· 1 + import { useIdentityData } from "components/IdentityProvider"; 2 + import { 3 + navPages, 4 + HomeButton, 5 + ReaderButton, 6 + NotificationButton, 7 + WriterButton, 8 + } from "./NavigationButtons"; 9 + import { PublicationButtons } from "./Publications"; 10 + import { Sidebar } from "./Sidebar"; 11 + import { LoginActionButton, LoginButton } from "components/LoginButton"; 12 + import { ProfileButton } from "./ProfileButton"; 13 + 14 + export const DesktopNavigation = (props: { 15 + currentPage: navPages; 16 + publication?: string; 17 + }) => { 18 + let { identity } = useIdentityData(); 19 + let thisPublication = identity?.publications?.find( 20 + (pub) => pub.uri === props.publication, 21 + ); 22 + 23 + let currentlyWriter = 24 + props.currentPage === "home" || 25 + props.currentPage === "looseleafs" || 26 + props.currentPage === "pub"; 27 + return ( 28 + <div className="flex flex-col gap-3"> 29 + <Sidebar alwaysOpen> 30 + {identity?.atp_did ? ( 31 + <> 32 + <ProfileButton /> 33 + <NotificationButton 34 + current={props.currentPage === "notifications"} 35 + /> 36 + </> 37 + ) : ( 38 + <LoginActionButton /> 39 + )} 40 + </Sidebar> 41 + 42 + <Sidebar alwaysOpen> 43 + <ReaderButton 44 + current={props.currentPage === "reader"} 45 + subs={ 46 + identity?.publication_subscriptions?.length !== 0 && 47 + identity?.publication_subscriptions?.length !== undefined 48 + } 49 + /> 50 + <WriterButton 51 + currentPage={props.currentPage} 52 + currentPubUri={thisPublication?.uri} 53 + /> 54 + {currentlyWriter && ( 55 + <> 56 + <hr className="border-border-light border-dashed" /> 57 + <PublicationButtons 58 + currentPage={props.currentPage} 59 + currentPubUri={thisPublication?.uri} 60 + /> 61 + </> 62 + )} 63 + </Sidebar> 64 + </div> 65 + ); 66 + };
+1 -1
components/ActionBar/Footer.tsx
··· 8 8 actionFooter touch-none shrink-0 9 9 w-full z-10 10 10 px-2 pt-1 pwa-padding-bottom 11 - flex justify-start gap-1 11 + flex justify-between 12 12 h-[calc(38px+var(--safe-padding-bottom))] 13 13 bg-[rgba(var(--bg-page),0.5)] border-top border-bg-page`} 14 14 >
+63
components/ActionBar/MobileNavigation.tsx
··· 1 + import { useIdentityData } from "components/IdentityProvider"; 2 + import { Separator } from "components/Layout"; 3 + import { 4 + navPages, 5 + NotificationButton, 6 + ReaderButton, 7 + WriterButton, 8 + } from "./NavigationButtons"; 9 + import { PublicationNavigation } from "./PublicationNavigation"; 10 + import { LoginActionButton } from "components/LoginButton"; 11 + import { ProfileButton } from "./ProfileButton"; 12 + 13 + export const MobileNavigation = (props: { 14 + currentPage: navPages; 15 + currentPublicationUri?: string; 16 + currentProfileDid?: string; 17 + }) => { 18 + let { identity } = useIdentityData(); 19 + 20 + let compactOnMobile = 21 + props.currentPage === "home" || 22 + props.currentPage === "looseleafs" || 23 + props.currentPage === "pub"; 24 + 25 + return ( 26 + <div 27 + className={`mobileFooter w-full flex gap-4 px-1 text-secondary grow items-center justify-between`} 28 + > 29 + <div className="mobileNav flex gap-2 items-center justify-start min-w-0"> 30 + <ReaderButton 31 + compactOnMobile={compactOnMobile} 32 + current={props.currentPage === "reader"} 33 + subs={ 34 + identity?.publication_subscriptions?.length !== 0 && 35 + identity?.publication_subscriptions?.length !== undefined 36 + } 37 + /> 38 + <WriterButton 39 + compactOnMobile={compactOnMobile} 40 + currentPage={props.currentPage} 41 + currentPubUri={props.currentPublicationUri} 42 + /> 43 + 44 + {compactOnMobile && ( 45 + <> 46 + <PublicationNavigation 47 + currentPage={props.currentPage} 48 + currentPubUri={props.currentPublicationUri} 49 + /> 50 + </> 51 + )} 52 + </div> 53 + {identity?.atp_did ? ( 54 + <div className="flex gap-2"> 55 + <NotificationButton /> 56 + <ProfileButton /> 57 + </div> 58 + ) : ( 59 + <LoginActionButton /> 60 + )} 61 + </div> 62 + ); 63 + };
-179
components/ActionBar/Navigation.tsx
··· 1 - import { HomeSmall } from "components/Icons/HomeSmall"; 2 - import { ActionButton } from "./ActionButton"; 3 - import { Sidebar } from "./Sidebar"; 4 - import { useIdentityData } from "components/IdentityProvider"; 5 - import Link from "next/link"; 6 - import { DiscoverSmall } from "components/Icons/DiscoverSmall"; 7 - import { PublicationButtons } from "./Publications"; 8 - import { Popover } from "components/Popover"; 9 - import { MenuSmall } from "components/Icons/MenuSmall"; 10 - import { 11 - ReaderReadSmall, 12 - ReaderUnreadSmall, 13 - } from "components/Icons/ReaderSmall"; 14 - import { 15 - NotificationsReadSmall, 16 - NotificationsUnreadSmall, 17 - } from "components/Icons/NotificationSmall"; 18 - import { SpeedyLink } from "components/SpeedyLink"; 19 - import { Separator } from "components/Layout"; 20 - 21 - export type navPages = 22 - | "home" 23 - | "reader" 24 - | "pub" 25 - | "discover" 26 - | "notifications" 27 - | "looseleafs" 28 - | "tag" 29 - | "profile"; 30 - 31 - export const DesktopNavigation = (props: { 32 - currentPage: navPages; 33 - publication?: string; 34 - }) => { 35 - let { identity } = useIdentityData(); 36 - return ( 37 - <div className="flex flex-col gap-3"> 38 - <Sidebar alwaysOpen> 39 - <NavigationOptions 40 - currentPage={props.currentPage} 41 - publication={props.publication} 42 - /> 43 - </Sidebar> 44 - {identity?.atp_did && ( 45 - <Sidebar alwaysOpen> 46 - <NotificationButton current={props.currentPage === "notifications"} /> 47 - </Sidebar> 48 - )} 49 - </div> 50 - ); 51 - }; 52 - 53 - export const MobileNavigation = (props: { 54 - currentPage: navPages; 55 - publication?: string; 56 - }) => { 57 - let { identity } = useIdentityData(); 58 - 59 - return ( 60 - <div className="flex gap-1 "> 61 - <Popover 62 - onOpenAutoFocus={(e) => e.preventDefault()} 63 - asChild 64 - className="px-2! !max-w-[256px]" 65 - trigger={ 66 - <div className="shrink-0 p-1 text-accent-contrast h-full flex gap-2 font-bold items-center"> 67 - <MenuSmall /> 68 - </div> 69 - } 70 - > 71 - <NavigationOptions 72 - currentPage={props.currentPage} 73 - publication={props.publication} 74 - isMobile 75 - /> 76 - </Popover> 77 - {identity?.atp_did && ( 78 - <> 79 - <Separator /> 80 - <NotificationButton /> 81 - </> 82 - )} 83 - </div> 84 - ); 85 - }; 86 - 87 - const NavigationOptions = (props: { 88 - currentPage: navPages; 89 - publication?: string; 90 - isMobile?: boolean; 91 - }) => { 92 - let { identity } = useIdentityData(); 93 - let thisPublication = identity?.publications?.find( 94 - (pub) => pub.uri === props.publication, 95 - ); 96 - return ( 97 - <> 98 - <HomeButton current={props.currentPage === "home"} /> 99 - <ReaderButton 100 - current={props.currentPage === "reader"} 101 - subs={ 102 - identity?.publication_subscriptions?.length !== 0 && 103 - identity?.publication_subscriptions?.length !== undefined 104 - } 105 - /> 106 - <DiscoverButton current={props.currentPage === "discover"} /> 107 - 108 - <hr className="border-border-light my-1" /> 109 - <PublicationButtons 110 - currentPage={props.currentPage} 111 - currentPubUri={thisPublication?.uri} 112 - /> 113 - </> 114 - ); 115 - }; 116 - 117 - const HomeButton = (props: { current?: boolean }) => { 118 - return ( 119 - <SpeedyLink href={"/home"} className="hover:!no-underline"> 120 - <ActionButton 121 - nav 122 - icon={<HomeSmall />} 123 - label="Home" 124 - className={props.current ? "bg-bg-page! border-border-light!" : ""} 125 - /> 126 - </SpeedyLink> 127 - ); 128 - }; 129 - 130 - const ReaderButton = (props: { current?: boolean; subs: boolean }) => { 131 - if (!props.subs) return; 132 - return ( 133 - <SpeedyLink href={"/reader"} className="hover:no-underline!"> 134 - <ActionButton 135 - nav 136 - icon={<ReaderUnreadSmall />} 137 - label="Reader" 138 - className={props.current ? "bg-bg-page! border-border-light!" : ""} 139 - /> 140 - </SpeedyLink> 141 - ); 142 - }; 143 - 144 - const DiscoverButton = (props: { current?: boolean }) => { 145 - return ( 146 - <Link href={"/discover"} className="hover:no-underline!"> 147 - <ActionButton 148 - nav 149 - icon={<DiscoverSmall />} 150 - label="Discover" 151 - subtext="" 152 - className={props.current ? "bg-bg-page! border-border-light!" : ""} 153 - /> 154 - </Link> 155 - ); 156 - }; 157 - 158 - export function NotificationButton(props: { current?: boolean }) { 159 - let { identity } = useIdentityData(); 160 - let unreads = identity?.notifications[0]?.count; 161 - 162 - return ( 163 - <SpeedyLink href={"/notifications"} className="hover:no-underline!"> 164 - <ActionButton 165 - nav 166 - labelOnMobile={false} 167 - icon={ 168 - unreads ? ( 169 - <NotificationsUnreadSmall className="text-accent-contrast" /> 170 - ) : ( 171 - <NotificationsReadSmall /> 172 - ) 173 - } 174 - label="Notifications" 175 - className={`${props.current ? "bg-bg-page! border-border-light!" : ""} ${unreads ? "text-accent-contrast!" : ""}`} 176 - /> 177 - </SpeedyLink> 178 - ); 179 - }
+101
components/ActionBar/NavigationButtons.tsx
··· 1 + import { HomeSmall } from "components/Icons/HomeSmall"; 2 + import { ActionButton } from "./ActionButton"; 3 + import { useIdentityData } from "components/IdentityProvider"; 4 + import { PublicationButtons } from "./Publications"; 5 + import { ReaderUnreadSmall } from "components/Icons/ReaderSmall"; 6 + import { 7 + NotificationsReadSmall, 8 + NotificationsUnreadSmall, 9 + } from "components/Icons/NotificationSmall"; 10 + import { SpeedyLink } from "components/SpeedyLink"; 11 + import { Popover } from "components/Popover"; 12 + import { WriterSmall } from "components/Icons/WriterSmall"; 13 + export type navPages = 14 + | "home" 15 + | "reader" 16 + | "pub" 17 + | "notifications" 18 + | "looseleafs" 19 + | "tag" 20 + | "profile" 21 + | "discover"; 22 + 23 + export const HomeButton = (props: { 24 + current?: boolean; 25 + className?: string; 26 + }) => { 27 + return ( 28 + <SpeedyLink href={"/home"} className="hover:!no-underline"> 29 + <ActionButton 30 + nav 31 + icon={<HomeSmall />} 32 + label="Home" 33 + className={`${props.current ? "bg-bg-page! border-border-light!" : ""} w-full! ${props.className}`} 34 + /> 35 + </SpeedyLink> 36 + ); 37 + }; 38 + 39 + export const WriterButton = (props: { 40 + currentPage: navPages; 41 + currentPubUri?: string; 42 + compactOnMobile?: boolean; 43 + }) => { 44 + let current = 45 + props.currentPage === "home" || 46 + props.currentPage === "looseleafs" || 47 + props.currentPage === "pub"; 48 + 49 + return ( 50 + <SpeedyLink href={"/home"} className="hover:!no-underline"> 51 + <ActionButton 52 + nav 53 + labelOnMobile={!props.compactOnMobile} 54 + icon={<WriterSmall />} 55 + label="Write" 56 + className={`${current ? "bg-bg-page! border-border-light!" : ""}`} 57 + /> 58 + </SpeedyLink> 59 + ); 60 + }; 61 + 62 + export const ReaderButton = (props: { 63 + current?: boolean; 64 + subs: boolean; 65 + compactOnMobile?: boolean; 66 + }) => { 67 + return ( 68 + <SpeedyLink href={"/reader"} className="hover:no-underline!"> 69 + <ActionButton 70 + nav 71 + labelOnMobile={!props.compactOnMobile} 72 + icon={<ReaderUnreadSmall />} 73 + label="Read" 74 + className={props.current ? "bg-bg-page! border-border-light!" : ""} 75 + /> 76 + </SpeedyLink> 77 + ); 78 + }; 79 + 80 + export function NotificationButton(props: { current?: boolean }) { 81 + let { identity } = useIdentityData(); 82 + let unreads = identity?.notifications[0]?.count; 83 + 84 + return ( 85 + <SpeedyLink href={"/notifications"} className="hover:no-underline!"> 86 + <ActionButton 87 + nav 88 + labelOnMobile={false} 89 + icon={ 90 + unreads ? ( 91 + <NotificationsUnreadSmall className="text-accent-contrast" /> 92 + ) : ( 93 + <NotificationsReadSmall /> 94 + ) 95 + } 96 + label="Notifications" 97 + className={`${props.current ? "bg-bg-page! border-border-light!" : ""} ${unreads ? "text-accent-contrast!" : ""}`} 98 + /> 99 + </SpeedyLink> 100 + ); 101 + }
+61
components/ActionBar/ProfileButton.tsx
··· 1 + import { Avatar } from "components/Avatar"; 2 + import { ActionButton } from "./ActionButton"; 3 + import { useIdentityData } from "components/IdentityProvider"; 4 + import { AccountSmall } from "components/Icons/AccountSmall"; 5 + import { useRecordFromDid } from "src/utils/useRecordFromDid"; 6 + import { Menu, MenuItem } from "components/Menu"; 7 + import { useIsMobile } from "src/hooks/isMobile"; 8 + import { LogoutSmall } from "components/Icons/LogoutSmall"; 9 + import { mutate } from "swr"; 10 + import { SpeedyLink } from "components/SpeedyLink"; 11 + 12 + export const ProfileButton = () => { 13 + let { identity } = useIdentityData(); 14 + let { data: record } = useRecordFromDid(identity?.atp_did); 15 + let isMobile = useIsMobile(); 16 + 17 + return ( 18 + <Menu 19 + asChild 20 + side={isMobile ? "top" : "right"} 21 + align={isMobile ? "center" : "start"} 22 + trigger={ 23 + <ActionButton 24 + nav 25 + labelOnMobile={false} 26 + icon={ 27 + record ? ( 28 + <Avatar 29 + src={record.avatar} 30 + displayName={record.displayName || record.handle} 31 + /> 32 + ) : ( 33 + <AccountSmall /> 34 + ) 35 + } 36 + label={record ? record.displayName || record.handle : "Account"} 37 + className={`w-full`} 38 + /> 39 + } 40 + > 41 + {record && ( 42 + <> 43 + <SpeedyLink className="no-underline!" href={`/p/${record.handle}`}> 44 + <MenuItem onSelect={() => {}}>View Profile</MenuItem> 45 + </SpeedyLink> 46 + 47 + <hr className="border-border-light border-dashed" /> 48 + </> 49 + )} 50 + <MenuItem 51 + onSelect={async () => { 52 + await fetch("/api/auth/logout"); 53 + mutate("identity", null); 54 + }} 55 + > 56 + <LogoutSmall /> 57 + Log Out 58 + </MenuItem> 59 + </Menu> 60 + ); 61 + };
+148
components/ActionBar/PublicationNavigation.tsx
··· 1 + "use client"; 2 + import { useIdentityData } from "components/IdentityProvider"; 3 + import { getBasePublicationURL } from "app/lish/createPub/getPublicationURL"; 4 + import { 5 + normalizePublicationRecord, 6 + type NormalizedPublication, 7 + } from "src/utils/normalizeRecords"; 8 + import { SpeedyLink } from "components/SpeedyLink"; 9 + import { Popover } from "components/Popover"; 10 + import { ButtonPrimary } from "components/Buttons"; 11 + import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 12 + import { HomeButton, type navPages } from "./NavigationButtons"; 13 + import { HomeSmall } from "components/Icons/HomeSmall"; 14 + import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 15 + import { PubIcon, PublicationButtons } from "./Publications"; 16 + import { HomeTiny } from "components/Icons/HomeTiny"; 17 + import { LooseleafTiny } from "components/Icons/LooseleafTiny"; 18 + import { Separator } from "components/Layout"; 19 + import { Menu, MenuItem } from "components/Menu"; 20 + import { AddTiny } from "components/Icons/AddTiny"; 21 + 22 + export const PublicationNavigation = (props: { 23 + currentPage: navPages; 24 + currentPubUri?: string; 25 + }) => { 26 + let { identity } = useIdentityData(); 27 + 28 + if (!identity) return; 29 + 30 + let hasLooseleafs = !!identity?.permission_token_on_homepage.find( 31 + (f) => 32 + f.permission_tokens.leaflets_to_documents && 33 + f.permission_tokens.leaflets_to_documents[0]?.document, 34 + ); 35 + 36 + let pubCount = identity?.publications.length ?? 0; 37 + let onlyOnePub = pubCount === 1 && hasLooseleafs; 38 + let onlyLooseleafs = pubCount === 0 && hasLooseleafs; 39 + let className = 40 + "font-bold text-secondary flex gap-2 items-center grow min-w-0 text-sm h-[34px] px-2 accent-container"; 41 + 42 + // if not publications or looseleafs 43 + if (!identity.publications && !hasLooseleafs) { 44 + return ( 45 + <SpeedyLink href="/lish/createPub"> 46 + <ButtonPrimary compact className="text-sm!"> 47 + Create a Publication! 48 + </ButtonPrimary> 49 + </SpeedyLink> 50 + ); 51 + } 52 + 53 + switch (props.currentPage) { 54 + case "looseleafs": 55 + case "pub": 56 + if (onlyLooseleafs || onlyOnePub) 57 + return ( 58 + <> 59 + <SpeedyLink href={`/home`} className={className}> 60 + <HomeTiny className="shrink-0" /> 61 + Home 62 + </SpeedyLink> 63 + </> 64 + ); 65 + break; 66 + case "home": { 67 + if (onlyLooseleafs || onlyOnePub) { 68 + let pub = identity.publications[0]; 69 + return ( 70 + <div className={className}> 71 + <Menu trigger={<MoreOptionsVerticalTiny className="shrink-0" />}> 72 + <SpeedyLink href="/createPub"> 73 + <MenuItem className="items-center! text-sm" onSelect={() => {}}> 74 + <AddTiny /> 75 + Create New Publication 76 + </MenuItem> 77 + </SpeedyLink> 78 + </Menu> 79 + <Separator classname="h-6!" /> 80 + {onlyLooseleafs ? ( 81 + <SpeedyLink 82 + href="/looseleafs" 83 + className="hover:no-underline! text-inherit flex gap-2 items-center pr-2 w-full min-w-0" 84 + > 85 + <LooseleafTiny className="shrink-0" /> Looseleafs 86 + </SpeedyLink> 87 + ) : ( 88 + <SpeedyLink 89 + href={`${getBasePublicationURL(pub)}/dashboard`} 90 + className="hover:no-underline! text-inherit flex gap-2 items-center pr-2 w0ull min-w-0" 91 + > 92 + <PubIcon 93 + small 94 + record={normalizePublicationRecord(pub.record)} 95 + uri={pub.uri} 96 + /> 97 + <div className="truncate min-w-0">{pub.name}</div> 98 + </SpeedyLink> 99 + )} 100 + </div> 101 + ); 102 + } 103 + break; 104 + } 105 + } 106 + 107 + return ( 108 + <Popover 109 + trigger={ 110 + <div className={className}> 111 + <PubIcons 112 + publications={identity.publications.map((pub) => ({ 113 + record: normalizePublicationRecord(pub.record), 114 + uri: pub.uri, 115 + }))} 116 + />{" "} 117 + Publications 118 + </div> 119 + } 120 + className="pt-1 px-2!" 121 + > 122 + <HomeButton current={props.currentPage === "home"} /> 123 + <hr className="my-1 border-border-light" /> 124 + <PublicationButtons 125 + currentPage={props.currentPage} 126 + currentPubUri={props.currentPubUri} 127 + /> 128 + </Popover> 129 + ); 130 + }; 131 + 132 + function PubIcons(props: { 133 + publications: { record: NormalizedPublication | null; uri: string }[]; 134 + }) { 135 + if (props.publications.length < 1) return null; 136 + return ( 137 + <div className="flex"> 138 + {props.publications.map((pub, index) => { 139 + if (index <= 2) 140 + return ( 141 + <div className="-ml-[6px] first:ml-0" key={pub.uri}> 142 + <PubIcon small record={pub.record} uri={pub.uri} /> 143 + </div> 144 + ); 145 + })} 146 + </div> 147 + ); 148 + }
+18 -11
components/ActionBar/Publications.tsx
··· 19 19 import { useIsMobile } from "src/hooks/isMobile"; 20 20 import { useState } from "react"; 21 21 import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 22 - import { navPages } from "./Navigation"; 22 + import { type navPages } from "./NavigationButtons"; 23 23 24 24 export const PublicationButtons = (props: { 25 25 currentPage: navPages; 26 26 currentPubUri: string | undefined; 27 + className?: string; 28 + optionClassName?: string; 27 29 }) => { 28 30 let { identity } = useIdentityData(); 29 31 let hasLooseleafs = !!identity?.permission_token_on_homepage.find( ··· 38 40 return <PubListEmpty />; 39 41 40 42 return ( 41 - <div className="pubListWrapper w-full flex flex-col gap-1 sm:bg-transparent sm:border-0"> 43 + <div 44 + className={`pubListWrapper w-full flex flex-col sm:bg-transparent sm:border-0 ${props.className}`} 45 + > 42 46 {hasLooseleafs && ( 43 47 <> 44 48 <SpeedyLink 45 49 href={`/looseleafs`} 46 - className="flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full" 50 + className={`flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full `} 47 51 > 48 52 {/*TODO How should i get if this is the current page or not? 49 53 theres not "pub" to check the uri for. Do i need to add it as an option to NavPages? thats kinda annoying*/} ··· 51 55 label="Looseleafs" 52 56 icon={<LooseLeafSmall />} 53 57 nav 54 - className={ 58 + className={`w-full! ${ 55 59 props.currentPage === "looseleafs" 56 60 ? "bg-bg-page! border-border!" 57 61 : "" 58 62 } 63 + ${props.optionClassName}`} 59 64 /> 60 65 </SpeedyLink> 61 - <hr className="border-border-light border-dashed mx-1" /> 62 66 </> 63 67 )} 64 68 ··· 86 90 })} 87 91 <Link 88 92 href={"/lish/createPub"} 89 - className="pubListCreateNew text-accent-contrast text-sm place-self-end hover:text-accent-contrast" 93 + className={`pubListCreateNew group/new-pub text-tertiary hover:text-accent-contrast flex gap-2 items-center p-1 no-underline! ${props.optionClassName}`} 90 94 > 91 - New 95 + <div className="group-hover/new-pub:border-accent-contrast w-6 h-6 border-border-light border-2 border-dashed rounded-full" /> 96 + New Publication 92 97 </Link> 93 98 </div> 94 99 ); ··· 99 104 name: string; 100 105 record: Json; 101 106 current?: boolean; 107 + className?: string; 102 108 }) => { 103 109 let record = normalizePublicationRecord(props.record); 104 110 if (!record) return; ··· 106 112 return ( 107 113 <SpeedyLink 108 114 href={`${getBasePublicationURL(props)}/dashboard`} 109 - className="flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full" 115 + className={`flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full `} 110 116 > 111 117 <ActionButton 112 118 label={record.name} 113 119 icon={<PubIcon record={record} uri={props.uri} />} 114 120 nav 115 - className={props.current ? "bg-bg-page! border-border!" : ""} 121 + className={`w-full! ${props.current ? "bg-bg-page! border-border!" : ""} ${props.className}`} 116 122 /> 117 123 </SpeedyLink> 118 124 ); ··· 198 204 export const PubIcon = (props: { 199 205 record: NormalizedPublication | null; 200 206 uri: string; 207 + tiny?: boolean; 201 208 small?: boolean; 202 209 large?: boolean; 203 210 className?: string; 204 211 }) => { 205 212 if (!props.record) return null; 206 213 207 - let iconSizeClassName = `${props.small ? "w-4 h-4" : props.large ? "w-12 h-12" : "w-6 h-6"} rounded-full`; 214 + let iconSizeClassName = `${props.tiny ? "w-4 h-4" : props.small ? "w-5 h-5" : props.large ? "w-12 h-12" : "w-6 h-6"} rounded-full`; 208 215 209 216 return props.record.icon ? ( 210 217 <div ··· 221 228 ) : ( 222 229 <div className={`${iconSizeClassName} bg-accent-1 relative`}> 223 230 <div 224 - className={`${props.small ? "text-xs" : props.large ? "text-2xl" : "text-sm"} font-bold absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-accent-2`} 231 + className={`${props.tiny ? "text-xs" : props.large ? "text-2xl" : "text-sm"} font-bold absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-accent-2`} 225 232 > 226 233 {props.record?.name.slice(0, 1).toUpperCase()} 227 234 </div>
-1
components/Blocks/BaseTextareaBlock.tsx
··· 83 83 let block = props.block.nextBlock; 84 84 85 85 let coord = getCoordinatesInTextarea(e.currentTarget, selection); 86 - console.log(coord); 87 86 if (block) { 88 87 focusBlock(block, { 89 88 left: coord.left + e.currentTarget.getBoundingClientRect().left,
+21
components/Icons/HomeTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const HomeTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + fillRule="evenodd" 15 + clipRule="evenodd" 16 + d="M3.5906 0.287577C3.39798 0.211458 3.18013 0.305893 3.10402 0.498504C3.0706 0.583064 3.07005 0.672489 3.09625 0.75265C3.09112 0.756892 3.08729 0.759504 3.08536 0.760636C3.01766 0.800372 2.93363 0.858436 2.8665 0.940226C2.79683 1.02511 2.71837 1.17222 2.75663 1.35689C2.7922 1.52854 2.91018 1.63661 3.00046 1.69802C3.09505 1.76237 3.2113 1.81163 3.33752 1.85239C3.39934 1.87236 3.45003 1.8919 3.49051 1.91036C3.48033 1.91756 3.46441 1.92788 3.43885 1.94314L3.42397 1.95192C3.38779 1.97318 3.32461 2.0103 3.27192 2.05173C3.20599 2.10358 3.10812 2.19751 3.06658 2.34833C3.0237 2.50404 3.05981 2.64893 3.11488 2.76333C3.2047 2.94994 3.4288 3.02841 3.61541 2.93858C3.75466 2.87155 3.83369 2.72977 3.82752 2.58459C3.87484 2.55627 3.94958 2.5103 4.01834 2.44579C4.12918 2.3418 4.23095 2.21532 4.28001 2.06267C4.33473 1.89238 4.3137 1.71934 4.2251 1.57134C4.14607 1.43931 4.0277 1.35059 3.92325 1.29025C3.85742 1.25222 3.78599 1.21914 3.71221 1.19005C3.76063 1.12961 3.80094 1.06476 3.83053 1.00004C3.86565 0.923217 3.90498 0.804913 3.88814 0.670895C3.86764 0.507679 3.76561 0.356744 3.5906 0.287577ZM3.14984 0.684673C3.14991 0.684654 3.14956 0.685711 3.14857 0.687897C3.14928 0.685784 3.14977 0.684691 3.14984 0.684673ZM3.41985 1.07611L3.4216 1.07742C3.42036 1.07656 3.41981 1.07612 3.41985 1.07611ZM11.0044 2.01981C10.8258 1.79122 10.5134 1.71555 10.25 1.83709L5.05737 4.23314V3.71118C5.05737 3.43504 4.83352 3.21118 4.55737 3.21118H2.49927C2.22313 3.21118 1.99927 3.43504 1.99927 3.71118V6.72759L0.587556 7.98327C0.329642 8.21268 0.306533 8.60773 0.535942 8.86564C0.711092 9.06256 0.982798 9.1226 1.21631 9.03789V12.7554C1.21631 12.9616 1.34294 13.1467 1.53518 13.2214L3.35104 13.9272C3.50487 13.9869 3.6783 13.967 3.81451 13.8738C3.82806 13.8645 3.84106 13.8546 3.8535 13.8442L5.42986 12.9319V14.2399C5.42986 14.4459 5.55627 14.6309 5.74826 14.7057L8.04946 15.6028C8.1866 15.6563 8.34034 15.6466 8.46969 15.5763L14.5223 12.2892C14.6834 12.2017 14.7837 12.0331 14.7837 11.8498V8.93436L15.2609 8.71207C15.4336 8.63163 15.5602 8.4768 15.6047 8.29156C15.6492 8.10632 15.6068 7.91088 15.4895 7.76075L11.0044 2.01981ZM5.42026 11.7821C5.39313 11.546 5.30858 11.2485 5.16635 10.9817C4.98526 10.6419 4.77171 10.4608 4.58539 10.4181C4.47274 10.3923 4.36021 10.4233 4.2466 10.5542C4.1247 10.6947 4.03217 10.9273 4.03217 11.1918V12.5854L5.42026 11.7821ZM4.05737 4.87826C4.05737 4.88442 4.05748 4.89055 4.05771 4.89665L2.99927 5.83811V4.21118H4.05737V4.87826ZM13.7837 9.40019L9.23842 11.5175C8.96198 11.6463 8.63296 11.5568 8.4599 11.3057L4.76308 5.94217L2.21631 8.20746V12.4133L3.03217 12.7304V11.1918C3.03217 10.7137 3.19458 10.2407 3.49138 9.89876C3.79647 9.54724 4.26639 9.31907 4.80875 9.44335C5.40674 9.58038 5.80959 10.0625 6.04881 10.5113C6.29331 10.97 6.42986 11.507 6.42986 11.938V13.8981L7.76685 14.4193V12.47C7.76685 12.1938 7.9907 11.97 8.26685 11.97C8.54299 11.97 8.76685 12.1938 8.76685 12.47V14.277L13.7837 11.5523V9.40019ZM9.18936 10.1614L5.81211 5.26154L10.3242 3.17952L14.0205 7.91089L9.18936 10.1614Z" 17 + fill="currentColor" 18 + /> 19 + </svg> 20 + ); 21 + };
+19
components/Icons/LooseleafTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const LooseleafTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M11.2738 3.16162L15.0267 3.56201C15.3663 3.5985 15.6139 3.90154 15.5824 4.2417C15.4811 5.33541 15.3687 6.86768 15.1898 8.27196C15.1 8.97649 14.9914 9.66447 14.8558 10.2641C14.7231 10.8509 14.5533 11.4049 14.3197 11.81C13.8598 12.6071 13.0155 13.1869 12.3167 13.5483C11.9571 13.7343 11.6082 13.8768 11.3206 13.9702C11.1775 14.0167 11.0401 14.0541 10.9193 14.0776C10.7541 14.1097 10.5913 14.1051 10.4251 14.0962C10.1231 14.0799 9.42901 14.0296 8.58334 13.8823C7.47126 13.6886 6.03035 13.3156 4.90268 12.5825C4.57534 12.3697 4.21248 12.0972 3.84506 11.8022C3.46225 12.2159 3.05921 12.5047 2.64194 12.6431C2.0308 12.8455 1.43066 12.7101 1.00424 12.2759C0.608272 11.8724 0.427321 11.2804 0.415374 10.6606C0.403254 10.0297 0.562907 9.31159 0.915374 8.57372C1.00241 8.39185 1.17191 8.26311 1.37045 8.22802C1.77258 8.15726 2.03737 8.51678 2.2933 8.76024C2.5295 8.98492 2.85946 9.29431 3.23373 9.63231C3.99203 10.3171 4.90157 11.0908 5.58432 11.5347C6.49837 12.1288 7.73879 12.4665 8.79721 12.6509C9.57737 12.7867 10.2253 12.8337 10.4925 12.8481C10.6571 12.857 10.7671 12.8352 10.9349 12.7807C11.155 12.7093 11.441 12.5939 11.7425 12.438C12.3673 12.1148 12.9557 11.6727 13.2367 11.186C13.3732 10.9493 13.511 10.5452 13.6371 9.98778C13.7602 9.44308 13.8622 8.79923 13.9496 8.11376C14.0946 6.97515 14.1935 5.76335 14.2826 4.73974L11.141 4.40381C10.7981 4.36676 10.5497 4.05925 10.5863 3.71631C10.623 3.37337 10.9309 3.12535 11.2738 3.16162ZM1.74155 9.95751C1.68375 10.2082 1.66055 10.4373 1.6644 10.6372C1.67254 11.0552 1.7951 11.2962 1.89682 11.3999C1.96817 11.4725 2.06103 11.5185 2.24838 11.4565C2.40601 11.4043 2.62567 11.2693 2.88803 10.9956C2.71779 10.8466 2.55167 10.7008 2.39584 10.56C2.1555 10.343 1.93347 10.1371 1.74155 9.95751ZM7.96322 6.41162C8.31566 6.24529 8.70512 6.24366 9.04233 6.44872C9.34367 6.63216 9.52187 6.93155 9.62338 7.20556C9.72836 7.48916 9.77666 7.81215 9.7767 8.1372C9.77667 8.6247 9.74792 9.11115 9.78354 9.59813C9.92459 9.40912 10.1034 9.15794 10.2708 8.95556C10.4074 8.79053 10.6272 8.54235 10.9017 8.42333C11.0496 8.37061 11.1951 8.34487 11.3519 8.38329C11.5136 8.42299 11.628 8.51182 11.6996 8.58642C11.8258 8.71812 11.8852 8.87607 11.9124 8.95458C11.9724 9.12791 12.0164 9.36363 12.0472 9.51122C12.0685 9.61332 12.0877 9.69219 12.1038 9.75145C12.2564 9.83216 12.3762 9.97288 12.4203 10.1538C12.5016 10.489 12.2964 10.827 11.9613 10.9087C11.7048 10.971 11.4618 10.9006 11.2826 10.7563C11.133 10.6358 11.0491 10.4826 11.0033 10.3823C10.9715 10.3127 10.9455 10.2369 10.9222 10.1616C10.8894 10.2062 10.8558 10.2528 10.8206 10.3003C10.6776 10.4935 10.5049 10.7208 10.3285 10.8921C10.2401 10.9778 10.1241 11.0763 9.98471 11.1479C9.84326 11.2205 9.62468 11.2914 9.37338 11.229C9.12739 11.1678 8.96103 11.0117 8.85873 10.8696C8.75978 10.7319 8.6993 10.5791 8.65951 10.4477C8.57995 10.1849 8.54518 9.87525 8.52865 9.5952C8.49614 9.04324 8.52667 8.38518 8.5267 8.1372C8.52667 7.95206 8.50644 7.75603 8.42709 7.58642C8.35866 7.63907 8.25391 7.74085 8.12045 7.92724C7.96238 8.14867 7.88446 8.42797 7.77572 8.67626C7.60415 9.06796 7.39016 9.49817 6.98471 9.69188C6.67331 9.84037 6.29944 9.70824 6.15072 9.39696C6.00888 9.09909 6.12486 8.74671 6.40756 8.58642C6.51366 8.47888 6.57211 8.31017 6.63119 8.17528C6.77553 7.84573 6.89305 7.49444 7.10385 7.1997C7.35704 6.84609 7.64105 6.56381 7.96322 6.41162ZM5.92026 2.61377C6.26261 2.65786 6.50438 2.97161 6.4603 3.31396C6.34872 4.18082 6.36724 5.06034 6.26889 5.92919C6.15284 6.95425 5.90409 8.0946 5.25912 9.16552C5.08097 9.46107 4.69635 9.55648 4.40073 9.37841C4.10557 9.20023 4.0101 8.81645 4.18783 8.52099C4.703 7.66575 4.92072 6.72471 5.0267 5.78857C5.1258 4.91308 5.10851 4.02764 5.22104 3.15381C5.26521 2.81184 5.57829 2.57015 5.92026 2.61377ZM8.44369 1.89307C9.32915 1.89314 10.0472 2.61111 10.0472 3.49658C10.0472 3.77125 9.97757 4.02935 9.8558 4.25537L10.2923 4.7417C10.5227 4.99871 10.5005 5.39412 10.2435 5.62451C9.98648 5.85467 9.59102 5.83358 9.36069 5.57666L8.87826 5.03857C8.74 5.07745 8.59436 5.10008 8.44369 5.10009C7.55837 5.09991 6.84025 4.38192 6.84018 3.49658C6.84018 2.61117 7.55833 1.89325 8.44369 1.89307ZM8.44369 3.14306C8.24868 3.14325 8.09018 3.30153 8.09018 3.49658C8.09025 3.69157 8.24873 3.84991 8.44369 3.8501C8.63875 3.85002 8.79713 3.69163 8.79721 3.49658C8.79721 3.30146 8.63879 3.14314 8.44369 3.14306Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+1
components/Icons/MoreOptionsVerticalTiny.tsx
··· 8 8 viewBox="0 0 12 24" 9 9 fill="none" 10 10 xmlns="http://www.w3.org/2000/svg" 11 + {...props} 11 12 > 12 13 <path 13 14 d="M6 15.5C6.82843 15.5 7.5 16.1716 7.5 17C7.5 17.8284 6.82843 18.5 6 18.5C5.17157 18.5 4.5 17.8284 4.5 17C4.5 16.1716 5.17157 15.5 6 15.5ZM6 10.5C6.82843 10.5 7.5 11.1716 7.5 12C7.5 12.8284 6.82843 13.5 6 13.5C5.17157 13.5 4.5 12.8284 4.5 12C4.5 11.1716 5.17157 10.5 6 10.5ZM6 5.5C6.82843 5.5 7.5 6.17157 7.5 7C7.5 7.82843 6.82843 8.5 6 8.5C5.17157 8.5 4.5 7.82843 4.5 7C4.5 6.17157 5.17157 5.5 6 5.5Z"
+1 -36
components/Icons/NotificationSmall.tsx
··· 26 26 viewBox="0 0 24 24" 27 27 fill="none" 28 28 xmlns="http://www.w3.org/2000/svg" 29 - > 30 - <path 31 - d="M12.3779 0.890636C13.5297 0.868361 14.2312 1.35069 14.6104 1.8047C15.1942 2.50387 15.2636 3.34086 15.2129 3.95314C17.7074 4.96061 18.8531 7.45818 19.375 10.3975C19.5903 11.1929 20.0262 11.5635 20.585 11.9336C21.1502 12.3079 22.0847 12.7839 22.5879 13.7998C23.4577 15.556 22.8886 17.8555 20.9297 19.083C20.1439 19.5754 19.2029 20.1471 17.8496 20.5869C17.1962 20.7993 16.454 20.9768 15.5928 21.1055C15.2068 22.4811 13.9287 23.4821 12.4238 23.4824C10.9225 23.4824 9.64464 22.4867 9.25489 21.1162C8.37384 20.9871 7.61998 20.8046 6.95899 20.5869C5.62158 20.1464 4.69688 19.5723 3.91602 19.083C1.95717 17.8555 1.38802 15.556 2.25782 13.7998C2.76329 12.7794 3.60199 12.3493 4.18653 12.0068C4.7551 11.6737 5.1753 11.386 5.45606 10.7432C5.62517 9.31217 5.93987 8.01645 6.4668 6.92482C7.1312 5.54855 8.13407 4.49633 9.56251 3.92482C9.53157 3.34709 9.6391 2.63284 10.1133 1.98927C10.1972 1.87543 10.4043 1.594 10.7822 1.34669C11.1653 1.09611 11.6872 0.904101 12.3779 0.890636ZM14.1709 21.2608C13.6203 21.3007 13.0279 21.3242 12.3887 21.3242C11.7757 21.3242 11.2072 21.3024 10.6777 21.2656C11.0335 21.8421 11.6776 22.2324 12.4238 22.2324C13.1718 22.2321 13.816 21.8396 14.1709 21.2608ZM12.4004 2.38966C11.9872 2.39776 11.7419 2.50852 11.5996 2.60157C11.4528 2.6977 11.3746 2.801 11.3193 2.87599C11.088 3.19 11.031 3.56921 11.0664 3.92677C11.084 4.10311 11.1233 4.258 11.1631 4.37013C11.1875 4.43883 11.205 4.47361 11.21 4.48341C11.452 4.78119 11.4299 5.22068 11.1484 5.49415C10.8507 5.78325 10.3748 5.77716 10.0869 5.48048C10.0533 5.44582 10.0231 5.40711 9.99415 5.3672C9.0215 5.79157 8.31886 6.53162 7.81641 7.57228C7.21929 8.80941 6.91013 10.4656 6.82129 12.4746L6.81934 12.5137L6.81446 12.5518C6.73876 13.0607 6.67109 13.5103 6.53418 13.9121C6.38567 14.3476 6.16406 14.7061 5.82032 15.0899C5.54351 15.3988 5.06973 15.4268 4.76172 15.1514C4.45392 14.8758 4.42871 14.4019 4.70508 14.0928C4.93763 13.8332 5.04272 13.6453 5.11524 13.4326C5.14365 13.3492 5.16552 13.2588 5.18848 13.1553C5.10586 13.2062 5.02441 13.2544 4.94532 13.3008C4.28651 13.6868 3.87545 13.9129 3.60157 14.4658C3.08548 15.5082 3.38433 16.9793 4.71192 17.8115C5.4776 18.2913 6.27423 18.7818 7.42872 19.1621C8.58507 19.543 10.1358 19.8242 12.3887 19.8242C14.6416 19.8242 16.2108 19.5429 17.3857 19.1611C18.5582 18.7801 19.3721 18.2882 20.1328 17.8115C21.4611 16.9793 21.7595 15.5084 21.2432 14.4658C20.9668 13.9081 20.515 13.6867 19.7568 13.1846C19.7553 13.1835 19.7535 13.1827 19.752 13.1817C19.799 13.3591 19.8588 13.5202 19.9287 13.6514C20.021 13.8244 20.1034 13.8927 20.1533 13.917C20.5249 14.0981 20.6783 14.5465 20.4961 14.919C20.3135 15.2913 19.8639 15.4467 19.4922 15.2656C19.0607 15.0553 18.7821 14.6963 18.6035 14.3613C18.4238 14.0242 18.3154 13.6559 18.2471 13.3379C18.1778 13.0155 18.1437 12.7147 18.127 12.4971C18.1185 12.3873 18.1145 12.2956 18.1123 12.2305C18.1115 12.2065 18.1107 12.1856 18.1104 12.169C18.0569 11.6585 17.9885 11.1724 17.9082 10.7109C17.9002 10.6794 17.8913 10.6476 17.8838 10.6152L17.8906 10.6133C17.4166 7.97573 16.4732 6.17239 14.791 5.40821C14.5832 5.64607 14.2423 5.73912 13.9365 5.61036C13.5557 5.44988 13.3777 5.01056 13.5391 4.62892C13.5394 4.62821 13.5397 4.62699 13.54 4.62599C13.5425 4.61977 13.5479 4.6087 13.5537 4.59278C13.5658 4.55999 13.5837 4.50758 13.6035 4.44142C13.6438 4.30713 13.6903 4.12034 13.7139 3.91212C13.7631 3.47644 13.7038 3.06402 13.457 2.76857C13.3434 2.63264 13.0616 2.37678 12.4004 2.38966ZM10.1055 16.625C11.6872 16.8411 12.8931 16.8585 13.8174 16.7539C14.2287 16.7076 14.5997 17.0028 14.6465 17.4141C14.693 17.8256 14.3969 18.1976 13.9854 18.2442C12.9038 18.3665 11.5684 18.3389 9.90235 18.1113C9.49223 18.0551 9.20488 17.6768 9.26075 17.2666C9.3168 16.8563 9.6952 16.5691 10.1055 16.625ZM16.3887 16.3047C16.7403 16.086 17.203 16.1935 17.4219 16.5449C17.6406 16.8967 17.5324 17.3594 17.1807 17.5781C16.9689 17.7097 16.6577 17.8424 16.4033 17.9131C16.0045 18.0237 15.5914 17.7904 15.4805 17.3916C15.3696 16.9926 15.6031 16.5788 16.002 16.4678C16.1344 16.431 16.3112 16.3527 16.3887 16.3047Z" 32 - fill="currentColor" 33 - /> 34 - </svg> 35 - ); 36 - }; 37 - 38 - export const ReaderUnread = (props: Props) => { 39 - return ( 40 - <svg 41 - width="24" 42 - height="24" 43 - viewBox="0 0 24 24" 44 - fill="none" 45 - xmlns="http://www.w3.org/2000/svg" 46 29 {...props} 47 30 > 48 31 <path 49 - d="M2.40963 18.0472C2.40983 17.3641 2.99636 17.3642 2.99655 18.0472C2.99654 18.7307 3.28805 19.1528 3.38815 19.263C3.48823 19.3732 3.66105 19.7179 4.28463 19.7181C4.90847 19.7182 4.90847 20.3255 4.28463 20.3255C3.66107 20.3256 3.56221 20.5899 3.38815 20.7816C3.21412 20.9732 2.99855 21.2572 2.9985 21.9232C2.9985 22.5896 2.41159 22.6066 2.41159 21.9232C2.41151 21.2398 2.04325 20.8276 2.00143 20.7816C1.959 20.735 1.52689 20.3255 1.03854 20.3255C0.549966 20.3254 0.554718 19.7182 1.03854 19.7181C1.52186 19.7181 1.83806 19.3644 1.93014 19.263C2.02172 19.1622 2.40963 18.7307 2.40963 18.0472ZM18.3989 13.7962C18.3991 13.1543 18.8958 13.1543 18.896 13.7962C18.896 14.4386 19.1424 14.8352 19.227 14.9388C19.3117 15.0425 19.4577 15.3663 19.9848 15.3665C20.5125 15.3666 20.5125 15.9378 19.9848 15.9378C19.4575 15.9379 19.3742 16.1864 19.227 16.3665C19.0798 16.5466 18.897 16.8131 18.8969 17.4388C18.8969 18.0651 18.4008 18.0811 18.4008 17.4388C18.4007 16.7986 18.0911 16.4117 18.0542 16.3665C18.0188 16.3233 17.653 15.9381 17.2397 15.9378C16.8263 15.9378 16.8303 15.3665 17.2397 15.3665C17.6486 15.3663 17.916 15.0338 17.9936 14.9388C18.0711 14.844 18.3989 14.4386 18.3989 13.7962ZM5.68893 1.79134C6.32061 1.54967 6.8992 1.76772 7.31002 2.04427C7.71467 2.31686 8.06882 2.71494 8.35299 3.07649C8.63964 3.44125 8.90666 3.83827 9.10299 4.11653C9.3417 4.45493 9.26161 4.92361 8.9233 5.16243C8.58506 5.40111 8.1173 5.31974 7.87838 4.98177C7.65546 4.66583 7.42734 4.32673 7.17233 4.00227C6.91507 3.67506 6.67898 3.42822 6.47311 3.28938C6.3745 3.22296 6.30685 3.19837 6.27096 3.19075C6.24474 3.18528 6.23525 3.18881 6.22506 3.1927C6.18423 3.20859 5.9833 3.32114 5.7192 3.88899C5.59862 4.14843 5.29699 5.01402 4.93209 6.10677C4.85847 6.12987 4.80511 6.14661 4.79831 6.15462C4.79485 6.15884 4.79121 6.16214 4.78854 6.16536C4.4966 6.19308 4.21288 6.31734 3.99459 6.54427C3.75682 6.79162 3.64898 7.10768 3.64791 7.40462C3.64696 7.70171 3.75272 8.02065 3.99264 8.27083C4.04882 8.32937 4.1096 8.38044 4.1733 8.42513C3.95587 9.09776 3.75091 9.74532 3.5776 10.2884C3.85066 10.3144 4.12174 10.3779 4.38034 10.4691C5.16136 10.7446 5.92693 11.3005 6.54928 12.0501C6.71967 11.9646 6.90306 11.9158 7.08932 11.8929C7.52605 11.8392 7.97349 11.9251 8.39889 12.1146C8.52073 11.5922 8.77238 11.1032 9.17428 10.6868C10.661 9.14683 13.2111 9.62241 14.8227 11.1781C14.9553 11.306 15.0799 11.4413 15.1977 11.5814C15.3522 11.0931 15.5412 10.4983 15.7387 9.87044C16.2338 8.29641 16.7876 6.52428 17.0298 5.71028C17.333 4.69126 17.7647 3.91964 18.3823 3.49056C19.0569 3.02218 19.7993 3.06179 20.4389 3.34017C21.047 3.60494 21.6053 4.09649 22.0825 4.63802C22.5684 5.18942 23.0142 5.84393 23.3862 6.50032C23.59 6.86052 23.4639 7.31757 23.104 7.52181C22.7438 7.72592 22.2859 7.59954 22.0815 7.23958C21.7486 6.6521 21.3605 6.08653 20.9575 5.62923C20.546 5.16239 20.1597 4.85528 19.8403 4.71614C19.5524 4.5908 19.3833 4.62119 19.2368 4.72298C19.0332 4.86476 18.7324 5.24367 18.4663 6.13802C18.2201 6.9656 17.6633 8.74963 17.1694 10.3197C16.922 11.1061 16.6893 11.8418 16.519 12.3802C16.4339 12.6494 16.3652 12.87 16.3169 13.0228C16.2928 13.0988 16.274 13.1581 16.2612 13.1986C16.2548 13.2188 16.2489 13.235 16.2456 13.2454C16.2441 13.2501 16.2425 13.2536 16.2417 13.2562V13.2591L16.2407 13.2601C16.2268 13.3038 16.2077 13.3449 16.187 13.3841C16.5566 14.5982 16.4051 15.8801 15.5122 16.805C14.0254 18.345 11.4753 17.8696 9.86373 16.3138C9.18414 15.6576 8.69133 14.8276 8.45944 13.972C8.05454 13.5525 7.64913 13.397 7.38424 13.3831C7.59712 13.8598 7.7328 14.345 7.7778 14.8138C7.86052 15.6775 7.64387 16.5969 6.91061 17.1702C6.17739 17.7433 5.23363 17.7322 4.41549 17.4437C3.58744 17.1515 2.77404 16.5464 2.13327 15.7269C1.49254 14.9074 1.10184 13.972 1.01803 13.098C0.936885 12.25 1.1447 11.3498 1.84616 10.7747C2.09807 9.97534 2.5868 8.4405 3.06979 6.96224C3.59658 5.34994 4.14583 3.71844 4.3608 3.25618C4.68344 2.56265 5.10288 2.01575 5.68893 1.79134ZM13.7807 12.2572C12.4881 11.0094 10.9295 11.0284 10.2534 11.7288C9.57757 12.4294 9.61347 13.987 10.9057 15.2347C12.1981 16.4823 13.7567 16.464 14.4331 15.764C15.1092 15.0636 15.0731 13.505 13.7807 12.2572ZM3.88229 11.8841C3.3528 11.6973 2.99993 11.7759 2.80905 11.9251C2.61843 12.0746 2.45867 12.3971 2.51217 12.9554C2.5648 13.5041 2.82326 14.1742 3.31491 14.8031C3.8066 15.4319 4.39472 15.8452 4.91452 16.0286C5.44348 16.2152 5.79586 16.1376 5.98678 15.9886C6.17756 15.8394 6.33808 15.5159 6.28463 14.9574C6.23201 14.4087 5.97256 13.7385 5.48092 13.1097C4.98934 12.481 4.40201 12.0676 3.88229 11.8841ZM11.9194 14.7786C12.0925 14.5637 12.4074 14.5295 12.6225 14.7025C12.661 14.7334 12.7453 14.7848 12.8491 14.8284C12.8979 14.849 12.9439 14.8642 12.9819 14.8743C13.0173 14.8837 13.0359 14.8858 13.0395 14.8861C13.3156 14.8914 13.5361 15.1197 13.5307 15.3958C13.5253 15.6718 13.2969 15.8912 13.021 15.8861C12.823 15.8822 12.6163 15.8145 12.4614 15.7493C12.2997 15.6813 12.1271 15.5876 11.9956 15.4818C11.7806 15.3087 11.7465 14.9937 11.9194 14.7786ZM3.50924 12.1361L3.57955 12.1497L3.6733 12.1878C3.88055 12.2932 3.99317 12.533 3.92916 12.7659C3.90752 12.8445 3.8653 12.9118 3.81393 12.9681C3.81694 12.9884 3.81922 13.0124 3.82467 13.0384C3.87694 13.2878 4.02405 13.6447 4.28561 13.9671C4.45941 14.1814 4.42741 14.4972 4.21334 14.6712C3.99906 14.8451 3.68332 14.812 3.50924 14.598C3.14412 14.1481 2.92999 13.6431 2.84616 13.2425C2.80618 13.0513 2.78536 12.8362 2.82565 12.6468C2.84499 12.5561 2.89269 12.4054 3.02291 12.2845C3.16403 12.1538 3.34353 12.1108 3.50924 12.1361ZM11.0669 11.7454C11.2971 11.7138 11.5265 11.8478 11.6069 12.0755C11.6924 12.3193 11.576 12.5828 11.3471 12.6908C11.3428 12.696 11.3346 12.705 11.3256 12.721C11.2978 12.7706 11.2672 12.8577 11.2592 12.9779C11.244 13.2098 11.3168 13.5618 11.6518 13.9544C11.831 14.1644 11.806 14.4802 11.5962 14.6595C11.3862 14.8387 11.0704 14.8145 10.8911 14.6048C10.4014 14.0312 10.2273 13.4258 10.2612 12.9115C10.2779 12.6589 10.3462 12.4249 10.4546 12.2318C10.5444 12.0719 10.6842 11.9039 10.8803 11.807L10.9672 11.7699L11.0669 11.7454ZM5.92428 5.79427C5.92428 5.2357 6.35593 5.23572 6.35592 5.79427C6.35593 6.35264 6.57034 6.69727 6.64401 6.78743C6.71763 6.87749 6.84534 7.15945 7.30416 7.1595C7.76238 7.15988 7.76238 7.65527 7.30416 7.65559C6.84534 7.65559 6.77205 7.872 6.64401 8.02864C6.51604 8.18525 6.35794 8.41728 6.35788 8.96126C6.35788 9.50588 5.92623 9.51978 5.92623 8.96126C5.92613 8.40366 5.65582 8.067 5.62448 8.02864C5.59372 7.99102 5.27579 7.65582 4.91647 7.65559C4.557 7.65559 4.56049 7.1595 4.91647 7.1595C5.27178 7.15932 5.50406 6.87024 5.57174 6.78743C5.63909 6.70504 5.92426 6.3528 5.92428 5.79427Z" 50 - fill="currentColor" 51 - /> 52 - </svg> 53 - ); 54 - }; 55 - 56 - export const ReaderRead = (props: Props) => { 57 - return ( 58 - <svg 59 - width="24" 60 - height="24" 61 - viewBox="0 0 24 24" 62 - fill="none" 63 - xmlns="http://www.w3.org/2000/svg" 64 - {...props} 65 - > 66 - <path 67 - d="M5.3939 5.10098C6.94949 3.45228 9.5127 3.07248 11.1166 4.58535C11.8822 5.30769 12.2281 6.27357 12.2132 7.26504C12.4456 7.20657 12.6982 7.16362 12.9515 7.15469C13.2516 7.14413 13.6091 7.17916 13.9379 7.34707C14.6404 7.7064 15.0081 8.33088 15.142 8.9418C16.5998 8.30589 18.3571 8.43563 19.7631 9.42032C21.8907 10.9106 22.4521 13.8268 20.9711 15.9418C20.5099 16.6002 19.9071 17.0981 19.2328 17.4281C19.2725 17.8704 19.3144 18.3372 19.389 18.8285C19.4945 19.5226 19.6505 20.0808 19.8754 20.4223C20.103 20.7681 20.0071 21.2335 19.6615 21.4613C19.3156 21.6891 18.8503 21.5932 18.6224 21.2475C18.2076 20.6174 18.0171 19.7803 17.9066 19.0531C17.8466 18.658 17.8029 18.2404 17.7679 17.8647C16.6347 18.0087 15.441 17.7451 14.4291 17.0365C12.3744 15.5975 11.7805 12.8288 13.0756 10.7358L11.4466 9.68106C11.2965 9.90817 11.1261 10.1258 10.9349 10.3285C9.3795 11.9772 6.81619 12.3575 5.21226 10.8451C4.85829 10.5112 4.59328 10.1249 4.41246 9.70841C4.12505 9.84619 3.7775 10.0195 3.40464 10.2016C3.08808 10.3561 2.76192 10.5118 2.47203 10.6361C2.20246 10.7518 1.89418 10.872 1.63999 10.9145C1.23165 10.9825 0.845053 10.7066 0.776711 10.2982C0.7085 9.88981 0.984507 9.50334 1.39292 9.43497C1.44871 9.42563 1.60626 9.37614 1.8812 9.25821C2.1356 9.14908 2.43339 9.00579 2.74644 8.85294C3.20082 8.63107 3.71706 8.37021 4.11265 8.19278C4.12147 7.09833 4.57394 5.97021 5.3939 5.10098ZM18.9027 10.6488C17.7826 9.86431 16.3636 9.87558 15.3129 10.5473C16.188 11.0878 17.0483 11.6416 17.7211 12.0824C18.1169 12.3418 18.449 12.5624 18.682 12.7182C18.7984 12.796 18.8907 12.8585 18.9535 12.9008C18.9844 12.9216 19.0085 12.9377 19.0248 12.9486C19.0328 12.9541 19.0392 12.9585 19.0433 12.9613C19.0454 12.9627 19.0471 12.9645 19.0482 12.9652H19.0492V12.9662C19.2776 13.1212 19.3377 13.4321 19.183 13.6606C19.0279 13.8888 18.7161 13.9473 18.4877 13.7924L18.4867 13.7934C18.4857 13.7927 18.4847 13.7908 18.4828 13.7895C18.4789 13.7868 18.4729 13.7829 18.4652 13.7777C18.4493 13.767 18.4248 13.7507 18.3939 13.7299C18.3771 13.7186 18.3582 13.7059 18.3373 13.6918C18.3409 13.7007 18.3455 13.7094 18.349 13.7182C18.4391 13.9478 18.4846 14.151 18.514 14.3129C18.5488 14.5038 18.5528 14.5587 18.5697 14.6166C18.6475 14.8814 18.4955 15.1597 18.2308 15.2377C17.9663 15.3152 17.6888 15.1642 17.6107 14.8998C17.5741 14.7753 17.5483 14.5898 17.5306 14.4926C17.5076 14.366 17.4762 14.2309 17.4183 14.0834C17.3064 13.7986 17.0738 13.4138 16.516 12.9633L14.3353 11.5512C13.4914 12.949 13.8764 14.8184 15.2894 15.808C16.7626 16.8395 18.752 16.4941 19.7416 15.0815C20.731 13.6686 20.3757 11.6807 18.9027 10.6488ZM10.0873 5.67715C9.23327 4.87156 7.62151 4.92548 6.48472 6.13028C5.34805 7.33535 5.38756 8.94767 6.24156 9.75333C7.09567 10.5587 8.70742 10.5041 9.8441 9.29923C9.97166 9.16397 10.0826 9.02211 10.181 8.87833C8.43436 8.11224 8.03413 8.1053 7.73863 8.15372C7.49417 8.19384 7.43342 8.19931 7.34215 8.21719C7.27583 8.2302 7.20421 8.24884 7.00426 8.31387C6.74185 8.39904 6.45989 8.25486 6.37437 7.99258C6.28923 7.73019 6.43242 7.44723 6.69469 7.36172C6.90022 7.29488 7.02051 7.26011 7.14976 7.23477C7.25177 7.2148 7.38857 7.1961 7.5648 7.16739L7.50523 7.1293C7.27353 6.9792 7.20675 6.66966 7.3568 6.43789C7.50691 6.20642 7.81653 6.14058 8.0482 6.29043L10.6127 7.95059C10.8517 7.07384 10.6453 6.20394 10.0873 5.67715ZM13.0043 8.65372C12.9388 8.65604 12.868 8.66411 12.7933 8.67618L13.6478 9.14883C13.5794 8.94601 13.4513 8.7833 13.2552 8.68301C13.2369 8.67373 13.1626 8.64818 13.0043 8.65372Z" 32 + d="M12.379 1.43198C13.4689 1.41079 14.1368 1.86924 14.4992 2.30308C15.0424 2.95384 15.1168 3.72838 15.0753 4.3021C17.3983 5.25881 18.4631 7.59734 18.9484 10.3275C19.1445 11.0495 19.539 11.3859 20.0538 11.7269C20.5738 12.0711 21.4604 12.5232 21.9367 13.4847C22.7585 15.1448 22.219 17.3154 20.3732 18.472C19.6405 18.9311 18.7598 19.4667 17.4933 19.8783C16.8876 20.0751 16.2004 20.2389 15.4054 20.3587C15.0286 21.64 13.8299 22.5687 12.422 22.5687C11.0175 22.5684 9.8201 21.6443 9.44056 20.3675C8.62793 20.2471 7.93094 20.079 7.31849 19.8773C6.0667 19.465 5.20053 18.928 4.47279 18.472C2.62669 17.3153 2.08725 15.1449 2.90931 13.4847C3.38819 12.5179 4.18479 12.1099 4.72669 11.7923C5.25122 11.485 5.62837 11.2249 5.88294 10.6478C6.04112 9.31375 6.33609 8.10299 6.82923 7.0814C7.44748 5.8009 8.37993 4.81743 9.70521 4.27671C9.68221 3.73659 9.78903 3.07493 10.2296 2.4769C10.3087 2.36964 10.5055 2.10112 10.8654 1.86558C11.2304 1.62673 11.7262 1.44477 12.379 1.43198ZM13.963 20.515C13.4751 20.5481 12.9528 20.5667 12.3917 20.5667C11.8542 20.5667 11.3526 20.5494 10.8839 20.5189C11.2161 20.9991 11.7775 21.3185 12.422 21.3187C13.0684 21.3187 13.6311 20.9974 13.963 20.515ZM12.4035 2.93198C12.0278 2.93931 11.807 3.03915 11.6827 3.12046C11.5541 3.20485 11.4861 3.29613 11.4357 3.3646C11.2306 3.64338 11.1781 3.98084 11.2101 4.30503C11.233 4.53653 11.3069 4.74354 11.3361 4.8021C11.5745 5.09978 11.5517 5.53674 11.2716 5.80894C10.9739 6.09788 10.498 6.09183 10.2101 5.79526C10.1865 5.77095 10.1678 5.74218 10.1466 5.71519C9.27113 6.10725 8.63618 6.78165 8.17884 7.72886C7.627 8.87197 7.33861 10.4065 7.25599 12.2748C7.24499 12.7149 7.1279 13.2121 6.98646 13.6273C6.84551 14.0408 6.6358 14.3817 6.31166 14.7435C6.03482 15.0524 5.56101 15.0794 5.25306 14.8041C4.9451 14.5283 4.91961 14.0536 5.19642 13.7445C5.40901 13.5071 5.50261 13.3391 5.56751 13.1488C5.58207 13.1061 5.59391 13.0608 5.60658 13.013C5.56502 13.0379 5.52488 13.0636 5.48451 13.0873C4.86861 13.4481 4.50111 13.6519 4.25404 14.1507C3.78545 15.0972 4.05431 16.439 5.26966 17.2005C5.98211 17.6469 6.71946 18.1005 7.78822 18.4525C8.85905 18.8051 10.298 19.0667 12.3917 19.0667C14.4855 19.0667 15.9412 18.8051 17.0294 18.4515C18.1149 18.0987 18.8688 17.6438 19.5763 17.2005C20.7913 16.4391 21.0603 15.0971 20.5919 14.1507C20.3558 13.674 19.9814 13.4704 19.338 13.0501C19.3917 13.2186 19.4731 13.4853 19.6447 13.5697C20.0162 13.7509 20.17 14.2002 19.9874 14.5726C19.8047 14.9447 19.3551 15.0992 18.9835 14.9183C18.5687 14.716 18.3022 14.3715 18.1329 14.0541C17.9627 13.7344 17.8602 13.3869 17.796 13.0882C17.7309 12.7851 17.6995 12.5025 17.6837 12.2982C17.639 11.7172 17.5904 11.1217 17.4591 10.5531L17.465 10.5511C17.0282 8.11865 16.1654 6.46451 14.6427 5.75327C14.4333 5.97064 14.1064 6.05064 13.8126 5.9271C13.4133 5.75888 13.2861 5.33204 13.4152 4.94468C13.4307 4.90565 13.536 4.62297 13.5734 4.29331C13.6186 3.89372 13.5628 3.5267 13.3458 3.26694C13.2493 3.15138 13.0022 2.92054 12.4035 2.93198ZM10.2716 16.0873C11.742 16.2881 12.8605 16.3032 13.716 16.2064C14.1272 16.1602 14.4984 16.4563 14.5451 16.8675C14.5915 17.279 14.2954 17.651 13.8839 17.6976C12.8712 17.8122 11.6232 17.786 10.0685 17.5736C9.65818 17.5175 9.37096 17.1392 9.42689 16.7289C9.48296 16.3185 9.86121 16.0312 10.2716 16.0873ZM16.09 15.7962C16.4415 15.5778 16.9034 15.6852 17.1222 16.0365C17.3407 16.3881 17.2334 16.8509 16.882 17.0697C16.6801 17.1952 16.385 17.321 16.1437 17.388C15.7448 17.4987 15.3318 17.2644 15.2208 16.8656C15.1102 16.4666 15.3434 16.0537 15.7423 15.9427C15.8619 15.9095 16.0227 15.8381 16.09 15.7962Z" 68 33 fill="currentColor" 69 34 /> 70 35 </svg>
+18
components/Icons/RSSTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + export const RSSTiny = (props: Props) => { 3 + return ( 4 + <svg 5 + width="16" 6 + height="16" 7 + viewBox="0 0 16 16" 8 + fill="none" 9 + xmlns="http://www.w3.org/2000/svg" 10 + {...props} 11 + > 12 + <path 13 + d="M2.82098 5.7636C6.84291 5.76364 10.2363 8.92669 10.2364 13.179C10.2364 13.8688 9.67713 14.428 8.98738 14.428C8.29764 14.428 7.73841 13.8688 7.73837 13.179C7.7383 10.3543 5.5118 8.26167 2.82098 8.26163C2.13119 8.26163 1.572 7.7024 1.57196 7.01262C1.57196 6.32281 2.13116 5.7636 2.82098 5.7636ZM2.82098 1.57196C9.12441 1.572 14.428 6.52137 14.428 13.179C14.428 13.8688 13.8688 14.428 13.179 14.428C12.4892 14.428 11.93 13.8688 11.93 13.179C11.93 7.94901 7.7933 4.07003 2.82098 4.06999C2.13116 4.06999 1.57196 3.51079 1.57196 2.82098C1.57196 2.13116 2.13116 1.57196 2.82098 1.57196ZM3.93094 10.6066C4.82318 10.6067 5.54639 11.3299 5.54649 12.2221C5.54649 13.1145 4.82325 13.8382 3.93094 13.8383C3.03853 13.8383 2.31478 13.1145 2.31478 12.2221C2.31489 11.3298 3.03859 10.6066 3.93094 10.6066Z" 14 + fill="currentColor" 15 + /> 16 + </svg> 17 + ); 18 + };
+4 -3
components/Icons/ReaderSmall.tsx
··· 3 3 export const ReaderUnreadSmall = (props: Props) => { 4 4 return ( 5 5 <svg 6 - width="25" 6 + width="24" 7 7 height="24" 8 - viewBox="0 0 25 24" 8 + viewBox="0 0 24 24" 9 9 fill="none" 10 10 xmlns="http://www.w3.org/2000/svg" 11 + {...props} 11 12 > 12 13 <path 13 - d="M2.82968 18.0472C2.82987 17.3641 3.41641 17.3642 3.41659 18.0472C3.41658 18.7307 3.7081 19.1528 3.80819 19.263C3.90827 19.3732 4.08109 19.7179 4.70468 19.7181C5.32851 19.7182 5.32851 20.3255 4.70468 20.3255C4.08111 20.3256 3.98225 20.5899 3.80819 20.7816C3.63417 20.9732 3.41859 21.2572 3.41854 21.9232C3.41854 22.5896 2.83163 22.6066 2.83163 21.9232C2.83155 21.2398 2.4633 20.8276 2.42147 20.7816C2.37905 20.735 1.94693 20.3255 1.45858 20.3255C0.97001 20.3254 0.974762 19.7182 1.45858 19.7181C1.9419 19.7181 2.25811 19.3644 2.35018 19.263C2.44176 19.1622 2.82968 18.7307 2.82968 18.0472ZM18.8189 13.7962C18.8191 13.1543 19.3158 13.1543 19.316 13.7962C19.316 14.4386 19.5624 14.8352 19.6471 14.9388C19.7317 15.0425 19.8778 15.3663 20.4049 15.3665C20.9325 15.3666 20.9325 15.9378 20.4049 15.9378C19.8775 15.9379 19.7943 16.1864 19.6471 16.3665C19.4999 16.5466 19.3171 16.8131 19.317 17.4388C19.317 18.0651 18.8209 18.0811 18.8209 17.4388C18.8207 16.7986 18.5111 16.4117 18.4742 16.3665C18.4388 16.3233 18.073 15.9381 17.6598 15.9378C17.2464 15.9378 17.2504 15.3665 17.6598 15.3665C18.0686 15.3663 18.336 15.0338 18.4137 14.9388C18.4911 14.844 18.8189 14.4386 18.8189 13.7962ZM6.10897 1.79134C6.74065 1.54967 7.31924 1.76772 7.73007 2.04427C8.13472 2.31686 8.48886 2.71494 8.77304 3.07649C9.05968 3.44125 9.32671 3.83827 9.52304 4.11653C9.76174 4.45493 9.68165 4.92361 9.34335 5.16243C9.0051 5.40111 8.53735 5.31974 8.29843 4.98177C8.07551 4.66583 7.84739 4.32673 7.59237 4.00227C7.33511 3.67506 7.09903 3.42822 6.89315 3.28938C6.79455 3.22296 6.72689 3.19837 6.69101 3.19075C6.66479 3.18528 6.65529 3.18881 6.64511 3.1927C6.60428 3.20859 6.40334 3.32114 6.13925 3.88899C6.01867 4.14843 5.71704 5.01402 5.35214 6.10677C5.27851 6.12987 5.22515 6.14661 5.21835 6.15462C5.21489 6.15884 5.21125 6.16214 5.20858 6.16536C4.91664 6.19308 4.63293 6.31734 4.41464 6.54427C4.17686 6.79162 4.06902 7.10768 4.06796 7.40462C4.067 7.70171 4.17276 8.02065 4.41268 8.27083C4.46886 8.32937 4.52964 8.38044 4.59335 8.42513C4.37591 9.09776 4.17095 9.74532 3.99765 10.2884C4.2707 10.3144 4.54179 10.3779 4.80038 10.4691C5.5814 10.7446 6.34697 11.3005 6.96933 12.0501C7.13971 11.9646 7.3231 11.9158 7.50936 11.8929C7.9461 11.8392 8.39353 11.9251 8.81893 12.1146C8.94078 11.5922 9.19242 11.1032 9.59433 10.6868C11.081 9.14683 13.6312 9.62241 15.2428 11.1781C15.3753 11.306 15.4999 11.4413 15.6178 11.5814C15.7723 11.0931 15.9613 10.4983 16.1588 9.87044C16.6539 8.29641 17.2076 6.52428 17.4498 5.71028C17.7531 4.69126 18.1848 3.91964 18.8023 3.49056C19.4769 3.02218 20.2194 3.06179 20.859 3.34017C21.4671 3.60494 22.0253 4.09649 22.5025 4.63802C22.9884 5.18942 23.4343 5.84393 23.8062 6.50032C24.0101 6.86052 23.884 7.31757 23.524 7.52181C23.1638 7.72592 22.7059 7.59954 22.5016 7.23958C22.1686 6.6521 21.7805 6.08653 21.3775 5.62923C20.9661 5.16239 20.5798 4.85528 20.2603 4.71614C19.9724 4.5908 19.8033 4.62119 19.6568 4.72298C19.4532 4.86476 19.1524 5.24367 18.8863 6.13802C18.6401 6.9656 18.0833 8.74963 17.5894 10.3197C17.3421 11.1061 17.1093 11.8418 16.9391 12.3802C16.854 12.6494 16.7853 12.87 16.7369 13.0228C16.7128 13.0988 16.6941 13.1581 16.6812 13.1986C16.6748 13.2188 16.6689 13.235 16.6656 13.2454C16.6641 13.2501 16.6625 13.2536 16.6617 13.2562V13.2591L16.6607 13.2601C16.6469 13.3038 16.6277 13.3449 16.607 13.3841C16.9766 14.5982 16.8251 15.8801 15.9322 16.805C14.4454 18.345 11.8953 17.8696 10.2838 16.3138C9.60418 15.6576 9.11137 14.8276 8.87948 13.972C8.47459 13.5525 8.06917 13.397 7.80429 13.3831C8.01717 13.8598 8.15284 14.345 8.19784 14.8138C8.28056 15.6775 8.06391 16.5969 7.33065 17.1702C6.59744 17.7433 5.65367 17.7322 4.83554 17.4437C4.00748 17.1515 3.19408 16.5464 2.55331 15.7269C1.91258 14.9074 1.52188 13.972 1.43808 13.098C1.35693 12.25 1.56474 11.3498 2.2662 10.7747C2.51811 9.97534 3.00684 8.4405 3.48983 6.96224C4.01662 5.34994 4.56587 3.71844 4.78085 3.25618C5.10348 2.56265 5.52293 2.01575 6.10897 1.79134ZM14.2008 12.2572C12.9082 11.0094 11.3496 11.0284 10.6734 11.7288C9.99762 12.4294 10.0335 13.987 11.3258 15.2347C12.6181 16.4823 14.1767 16.464 14.8531 15.764C15.5292 15.0636 15.4931 13.505 14.2008 12.2572ZM4.30233 11.8841C3.77284 11.6973 3.41998 11.7759 3.22909 11.9251C3.03847 12.0746 2.87871 12.3971 2.93222 12.9554C2.98485 13.5041 3.24331 14.1742 3.73495 14.8031C4.22664 15.4319 4.81476 15.8452 5.33456 16.0286C5.86353 16.2152 6.2159 16.1376 6.40683 15.9886C6.59761 15.8394 6.75812 15.5159 6.70468 14.9574C6.65205 14.4087 6.3926 13.7385 5.90097 13.1097C5.40938 12.481 4.82205 12.0676 4.30233 11.8841ZM12.3394 14.7786C12.5125 14.5637 12.8275 14.5295 13.0426 14.7025C13.081 14.7334 13.1653 14.7848 13.2691 14.8284C13.318 14.849 13.3639 14.8642 13.4019 14.8743C13.4373 14.8837 13.456 14.8858 13.4596 14.8861C13.7357 14.8914 13.9561 15.1197 13.9508 15.3958C13.9454 15.6718 13.7169 15.8912 13.441 15.8861C13.243 15.8822 13.0364 15.8145 12.8814 15.7493C12.7197 15.6813 12.5471 15.5876 12.4156 15.4818C12.2007 15.3087 12.1665 14.9937 12.3394 14.7786ZM3.92929 12.1361L3.9996 12.1497L4.09335 12.1878C4.3006 12.2932 4.41321 12.533 4.34921 12.7659C4.32756 12.8445 4.28534 12.9118 4.23397 12.9681C4.23698 12.9884 4.23927 13.0124 4.24472 13.0384C4.29698 13.2878 4.4441 13.6447 4.70565 13.9671C4.87945 14.1814 4.84746 14.4972 4.63339 14.6712C4.41911 14.8451 4.10336 14.812 3.92929 14.598C3.56417 14.1481 3.35003 13.6431 3.2662 13.2425C3.22622 13.0513 3.2054 12.8362 3.24569 12.6468C3.26503 12.5561 3.31274 12.4054 3.44296 12.2845C3.58407 12.1538 3.76357 12.1108 3.92929 12.1361ZM11.4869 11.7454C11.7171 11.7138 11.9466 11.8478 12.0269 12.0755C12.1125 12.3193 11.996 12.5828 11.7672 12.6908C11.7629 12.696 11.7547 12.705 11.7457 12.721C11.7178 12.7706 11.6872 12.8577 11.6793 12.9779C11.6641 13.2098 11.7368 13.5618 12.0719 13.9544C12.2511 14.1644 12.226 14.4802 12.0162 14.6595C11.8063 14.8387 11.4905 14.8145 11.3111 14.6048C10.8215 14.0312 10.6473 13.4258 10.6812 12.9115C10.698 12.6589 10.7662 12.4249 10.8746 12.2318C10.9645 12.0719 11.1042 11.9039 11.3004 11.807L11.3873 11.7699L11.4869 11.7454ZM6.34433 5.79427C6.34433 5.2357 6.77597 5.23572 6.77597 5.79427C6.77598 6.35264 6.99039 6.69727 7.06405 6.78743C7.13767 6.87749 7.26539 7.15945 7.72421 7.1595C8.18243 7.15988 8.18243 7.65527 7.72421 7.65559C7.26539 7.65559 7.19209 7.872 7.06405 8.02864C6.93609 8.18525 6.77798 8.41728 6.77792 8.96126C6.77792 9.50588 6.34628 9.51978 6.34628 8.96126C6.34618 8.40366 6.07586 8.067 6.04452 8.02864C6.01377 7.99102 5.69583 7.65582 5.33651 7.65559C4.97705 7.65559 4.98054 7.1595 5.33651 7.1595C5.69183 7.15932 5.92411 6.87024 5.99179 6.78743C6.05914 6.70504 6.3443 6.3528 6.34433 5.79427Z" 14 + d="M2.73574 18.3601C2.73594 17.677 3.32247 17.677 3.32266 18.3601C3.32265 19.0436 3.61416 19.4657 3.71426 19.5759C3.81434 19.6861 3.98716 20.0308 4.61074 20.031C5.23458 20.031 5.23458 20.6384 4.61074 20.6384C3.98718 20.6385 3.88832 20.9028 3.71426 21.0945C3.54023 21.2861 3.32466 21.5701 3.32461 22.2361C3.32461 22.9025 2.7377 22.9195 2.7377 22.2361C2.73762 21.5527 2.36936 21.1405 2.32754 21.0945C2.28512 21.0478 1.853 20.6384 1.36465 20.6384C0.876077 20.6383 0.880828 20.0311 1.36465 20.031C1.84797 20.031 2.16417 19.6773 2.25625 19.5759C2.34783 19.4751 2.73574 19.0436 2.73574 18.3601ZM18.725 14.1091C18.7252 13.4672 19.2219 13.4672 19.2221 14.1091C19.2221 14.7515 19.4685 15.1481 19.5531 15.2517C19.6378 15.3554 19.7838 15.6792 20.3109 15.6794C20.8386 15.6795 20.8386 16.2507 20.3109 16.2507C19.7836 16.2508 19.7003 16.4993 19.5531 16.6794C19.406 16.8595 19.2231 17.126 19.223 17.7517C19.223 18.378 18.727 18.394 18.727 17.7517C18.7268 17.1115 18.4172 16.7246 18.3803 16.6794C18.3449 16.6361 17.9791 16.2509 17.5658 16.2507C17.1524 16.2507 17.1564 15.6794 17.5658 15.6794C17.9747 15.6792 18.2421 15.3467 18.3197 15.2517C18.3972 15.1569 18.725 14.7515 18.725 14.1091ZM6.01504 2.10422C6.64672 1.86255 7.22531 2.08061 7.63613 2.35715C8.04078 2.62974 8.39493 3.02782 8.6791 3.38937C8.96575 3.75413 9.23277 4.15115 9.4291 4.42941C9.66781 4.76781 9.58772 5.23649 9.24942 5.47531C8.91117 5.71399 8.44342 5.63262 8.20449 5.29465C7.98157 4.97871 7.75345 4.63961 7.49844 4.31516C7.24118 3.98794 7.00509 3.7411 6.79922 3.60227C6.70062 3.53584 6.63296 3.51125 6.59707 3.50363C6.57086 3.49816 6.56136 3.5017 6.55117 3.50559C6.51034 3.52147 6.30941 3.63402 6.04531 4.20187C5.92473 4.46131 5.6231 5.3269 5.2582 6.41965C5.18458 6.44275 5.13122 6.45949 5.12442 6.4675C5.12096 6.47173 5.11732 6.47502 5.11465 6.47824C4.82271 6.50596 4.53899 6.63022 4.3207 6.85715C4.08293 7.1045 3.97509 7.42056 3.97403 7.7175C3.97307 8.01459 4.07883 8.33353 4.31875 8.58371C4.37493 8.64225 4.43571 8.69333 4.49942 8.73801C4.28198 9.41064 4.07702 10.0582 3.90371 10.6013C4.17677 10.6273 4.44785 10.6908 4.70645 10.782C5.48747 11.0575 6.25304 11.6133 6.87539 12.363C7.04578 12.2775 7.22917 12.2287 7.41543 12.2058C7.85216 12.1521 8.2996 12.238 8.725 12.4275C8.84684 11.905 9.09849 11.4161 9.50039 10.9997C10.9871 9.45971 13.5373 9.9353 15.1488 11.4909C15.2814 11.6189 15.406 11.7542 15.5238 11.8943C15.6783 11.406 15.8673 10.8112 16.0648 10.1833C16.56 8.6093 17.1137 6.83716 17.3559 6.02316C17.6591 5.00414 18.0908 4.23252 18.7084 3.80344C19.383 3.33506 20.1254 3.37467 20.765 3.65305C21.3731 3.91782 21.9314 4.40937 22.4086 4.9509C22.8945 5.5023 23.3404 6.15682 23.7123 6.8132C23.9161 7.1734 23.7901 7.63045 23.4301 7.83469C23.0699 8.0388 22.612 7.91243 22.4076 7.55246C22.0747 6.96499 21.6866 6.39942 21.2836 5.94211C20.8721 5.47527 20.4858 5.16816 20.1664 5.02902C19.8785 4.90368 19.7094 4.93407 19.5629 5.03586C19.3593 5.17764 19.0585 5.55655 18.7924 6.4509C18.5462 7.27848 17.9894 9.06251 17.4955 10.6325C17.2481 11.419 17.0154 12.1546 16.8451 12.6931C16.76 12.9622 16.6914 13.1829 16.643 13.3357C16.6189 13.4117 16.6001 13.471 16.5873 13.5114C16.5809 13.5317 16.575 13.5479 16.5717 13.5583C16.5702 13.563 16.5686 13.5665 16.5678 13.5691V13.572L16.5668 13.573C16.5529 13.6167 16.5338 13.6578 16.5131 13.697C16.8827 14.9111 16.7312 16.1929 15.8383 17.1179C14.3515 18.6579 11.8014 18.1824 10.1898 16.6267C9.51025 15.9705 9.01744 15.1405 8.78555 14.2849C8.38066 13.8654 7.97524 13.7099 7.71035 13.696C7.92323 14.1726 8.05891 14.6578 8.10391 15.1267C8.18663 15.9904 7.96998 16.9098 7.23672 17.4831C6.5035 18.0562 5.55974 18.0451 4.7416 17.7566C3.91355 17.4644 3.10015 16.8593 2.45938 16.0398C1.81865 15.2203 1.42795 14.2849 1.34414 13.4109C1.263 12.5629 1.47081 11.6627 2.17227 11.0876C2.42418 10.2882 2.91291 8.75338 3.3959 7.27512C3.92269 5.66282 4.47194 4.03132 4.68692 3.56906C5.00955 2.87553 5.42899 2.32864 6.01504 2.10422ZM14.1068 12.57C12.8142 11.3223 11.2556 11.3413 10.5795 12.0417C9.90368 12.7423 9.93958 14.2999 11.2318 15.5476C12.5242 16.7952 14.0828 16.7769 14.7592 16.0769C15.4353 15.3765 15.3992 13.8179 14.1068 12.57ZM4.2084 12.197C3.67891 12.0102 3.32604 12.0888 3.13516 12.238C2.94454 12.3875 2.78478 12.71 2.83828 13.2683C2.89091 13.8169 3.14937 14.4871 3.64102 15.1159C4.13271 15.7448 4.72083 16.1581 5.24063 16.3415C5.76959 16.5281 6.12197 16.4505 6.31289 16.3015C6.50367 16.1522 6.66419 15.8287 6.61074 15.2702C6.55812 14.7216 6.29867 14.0514 5.80703 13.4226C5.31545 12.7939 4.72812 12.3805 4.2084 12.197ZM12.2455 15.0915C12.4186 14.8766 12.7336 14.8424 12.9486 15.0154C12.9871 15.0463 13.0714 15.0977 13.1752 15.1413C13.224 15.1618 13.27 15.1771 13.308 15.1872C13.3434 15.1966 13.362 15.1986 13.3656 15.1989C13.6417 15.2043 13.8622 15.4326 13.8568 15.7087C13.8514 15.9846 13.623 16.2041 13.3471 16.1989C13.1491 16.1951 12.9425 16.1273 12.7875 16.0622C12.6258 15.9942 12.4532 15.9004 12.3217 15.7946C12.1068 15.6215 12.0726 15.3066 12.2455 15.0915ZM3.83535 12.4489L3.90567 12.4626L3.99942 12.5007C4.20667 12.6061 4.31928 12.8459 4.25528 13.0788C4.23363 13.1573 4.19141 13.2246 4.14004 13.281C4.14305 13.3013 4.14533 13.3252 4.15078 13.3513C4.20305 13.6007 4.35016 13.9576 4.61172 14.28C4.78552 14.4943 4.75352 14.8101 4.53945 14.9841C4.32517 15.158 4.00943 15.1249 3.83535 14.9109C3.47023 14.4609 3.2561 13.956 3.17227 13.5554C3.13229 13.3642 3.11147 13.1491 3.15176 12.9597C3.1711 12.8689 3.2188 12.7183 3.34903 12.5974C3.49014 12.4666 3.66964 12.4237 3.83535 12.4489ZM11.393 12.0583C11.6232 12.0267 11.8527 12.1607 11.933 12.3884C12.0186 12.6322 11.9021 12.8956 11.6732 13.0036C11.6689 13.0089 11.6608 13.0179 11.6518 13.0339C11.6239 13.0835 11.5933 13.1706 11.5854 13.2907C11.5701 13.5227 11.6429 13.8747 11.9779 14.2673C12.1572 14.4773 12.1321 14.7931 11.9223 14.9724C11.7123 15.1516 11.3965 15.1274 11.2172 14.9177C10.7276 14.3441 10.5534 13.7387 10.5873 13.2243C10.604 12.9718 10.6723 12.7377 10.7807 12.5446C10.8705 12.3848 11.0103 12.2168 11.2064 12.1198L11.2934 12.0827L11.393 12.0583ZM6.25039 6.10715C6.25039 5.54858 6.68204 5.5486 6.68203 6.10715C6.68205 6.66552 6.89645 7.01015 6.97012 7.10031C7.04374 7.19038 7.17145 7.47233 7.63028 7.47238C8.08849 7.47276 8.08849 7.96815 7.63028 7.96848C7.17145 7.96848 7.09816 8.18488 6.97012 8.34152C6.84216 8.49813 6.68405 8.73016 6.68399 9.27414C6.68399 9.81876 6.25235 9.83266 6.25235 9.27414C6.25224 8.71654 5.98193 8.37988 5.95059 8.34152C5.91984 8.3039 5.6019 7.9687 5.24258 7.96848C4.88311 7.96848 4.8866 7.47238 5.24258 7.47238C5.59789 7.4722 5.83017 7.18312 5.89785 7.10031C5.9652 7.01792 6.25037 6.66568 6.25039 6.10715Z" 14 15 fill="currentColor" 15 16 /> 16 17 </svg>
+19
components/Icons/ShareTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const ShareTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M14.294 2.09457C14.4677 2.02691 14.6645 2.06158 14.8048 2.18441C14.9451 2.30734 15.0054 2.4983 14.961 2.67953L12.8145 11.4481C12.7536 11.6967 12.5144 11.8588 12.2608 11.8241L7.56942 11.1766L5.33211 13.7664C5.20836 13.9096 5.01456 13.9711 4.83114 13.9246C4.6477 13.8781 4.50644 13.7316 4.4659 13.5467L3.68368 9.98324L1.212 8.00863C1.07265 7.89707 1.00353 7.71931 1.03035 7.54281C1.05731 7.36628 1.1765 7.21715 1.34285 7.15218L14.294 2.09457ZM4.70028 9.94417L5.12118 11.867L5.8409 10.2899L5.88094 10.2176C5.89632 10.1948 5.9137 10.1732 5.9327 10.1532L8.08407 7.8807L4.70028 9.94417ZM2.51375 7.76742L4.17391 9.09457L10.7677 5.07503C10.9816 4.9446 11.2595 4.99249 11.4171 5.18734C11.5746 5.38222 11.5631 5.66361 11.3907 5.84554L7.32723 10.1346L11.9493 10.7713L13.7598 3.37582L2.51375 7.76742Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+19
components/Icons/TagSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const TagSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M6.72467 12.065C7.3108 11.6577 8.11602 11.8032 8.5235 12.3893C8.93088 12.9754 8.78635 13.7806 8.20026 14.1881C7.72319 14.5197 7.10087 14.4854 6.66608 14.1451C5.6484 14.4853 4.81236 15.2231 4.31256 16.1021C3.73575 17.1169 3.65902 18.2144 4.12213 19.0484C4.47731 19.6876 5.18264 20.0117 6.20514 20.0758C7.22138 20.1393 8.37845 19.9287 9.3399 19.6812C10.7978 19.3061 11.89 19.606 12.584 20.2672C12.9149 20.5824 13.1265 20.9561 13.2393 21.3121C13.3463 21.6497 13.3853 22.0412 13.2784 22.3863C13.1557 22.7818 12.7354 23.003 12.3399 22.8805C11.9689 22.7654 11.7507 22.3889 11.8262 22.0162L11.8419 21.9518C11.8386 21.9605 11.8509 21.8957 11.8096 21.7652C11.7675 21.6322 11.6837 21.4816 11.5489 21.3531C11.3113 21.127 10.7896 20.8563 9.71295 21.1334C8.70042 21.394 7.3632 21.6522 6.11139 21.5738C4.86592 21.4958 3.52842 21.0674 2.81158 19.7779C2.02826 18.368 2.24712 16.6994 3.00787 15.3609C3.68152 14.176 4.82017 13.1659 6.2403 12.7066C6.32636 12.4555 6.49002 12.2282 6.72467 12.065ZM19.9464 11.7271C20.2297 11.5306 20.6186 11.6012 20.8155 11.8844C21.0123 12.1677 20.9424 12.5565 20.6592 12.7535L13.0342 18.0504C12.5945 18.3558 12.0716 18.5201 11.5362 18.5201H6.94928C6.60423 18.5201 6.32351 18.2401 6.3233 17.8951C6.32356 17.5503 6.60351 17.2704 6.9483 17.2701L11.5362 17.2711C11.8166 17.2711 12.091 17.184 12.3214 17.024L19.9464 11.7271ZM14.669 1.39902C15.4624 0.847523 16.5528 1.04335 17.1046 1.83652L21.3663 7.9664C21.9179 8.75983 21.722 9.85015 20.9288 10.4019L12.252 16.4352C11.8686 16.7016 11.4113 16.8423 10.9444 16.8375L6.69147 16.7935C6.27743 16.7892 5.94511 16.4498 5.94928 16.0357C5.95367 15.6217 6.293 15.2893 6.70709 15.2935L10.96 15.3375C11.1156 15.339 11.2678 15.2915 11.3956 15.2027L20.0723 9.1705C20.1855 9.09164 20.2136 8.93615 20.1348 8.82285L15.8731 2.69296C15.7943 2.57985 15.6387 2.55191 15.5255 2.63046L6.88873 8.63437C6.74865 8.73176 6.64607 8.87505 6.59772 9.03867L5.64459 12.2682C5.52741 12.6654 5.11018 12.893 4.71295 12.776C4.31582 12.6587 4.08894 12.2415 4.20612 11.8443L5.15826 8.61386C5.30323 8.1228 5.6119 7.69523 6.03229 7.40292L14.669 1.39902ZM14.086 5.54355C14.1404 5.5563 14.1874 5.59162 14.2149 5.64023L15.0626 7.14706L16.5167 5.84726C16.5594 5.80926 16.6162 5.79077 16.6729 5.79745C16.7299 5.80432 16.782 5.83608 16.8145 5.88339L17.4844 6.85995C17.5169 6.90725 17.5277 6.96644 17.5137 7.02206C17.4996 7.07758 17.4621 7.12477 17.4112 7.15097L15.6739 8.03964L16.796 9.40097C16.8316 9.44425 16.8477 9.5017 16.8399 9.55722C16.832 9.61254 16.8009 9.66223 16.7549 9.69394L15.9327 10.2574C15.8866 10.289 15.8299 10.3001 15.7755 10.2877C15.7208 10.275 15.6731 10.2398 15.6456 10.191L14.7823 8.65976L13.8008 9.58456C13.7863 9.5982 13.7699 9.60998 13.752 9.61874L12.4571 10.2516L13.5762 11.6109C13.6119 11.6542 13.628 11.7116 13.6202 11.7672C13.6123 11.8225 13.5812 11.8722 13.5352 11.9039L12.713 12.4674C12.6669 12.499 12.6092 12.5101 12.5547 12.4977C12.5007 12.4851 12.4545 12.4501 12.4268 12.4019L11.5499 10.8697L10.1046 12.1646C10.0618 12.203 10.0044 12.2222 9.94733 12.2154C9.89045 12.2085 9.83922 12.1767 9.8067 12.1295L9.1358 11.1529C9.10345 11.1057 9.09259 11.0463 9.10651 10.9908C9.12065 10.9351 9.1588 10.8871 9.21002 10.8609L10.9376 9.97812L9.84381 8.63925C9.80844 8.59592 9.79291 8.53934 9.80084 8.48398C9.80882 8.42869 9.83975 8.37888 9.8858 8.34726L10.7081 7.78378C10.7543 7.75217 10.8117 7.74085 10.8663 7.75351C10.9208 7.76625 10.9677 7.80144 10.9952 7.85019L11.8419 9.35312L12.8985 8.3746C12.9131 8.36104 12.9293 8.34908 12.9473 8.34042L14.1612 7.75742L13.0645 6.43027C13.0288 6.38701 13.0128 6.33052 13.0206 6.27499C13.0284 6.21943 13.0593 6.16905 13.1055 6.1373L13.9278 5.57382C13.9739 5.54231 14.0316 5.53098 14.086 5.54355Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+19
components/Icons/WriterSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const WriterSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M12.8808 22.0391C13.2948 22.0391 13.6304 22.3752 13.6308 22.7891C13.6306 23.203 13.2948 23.5389 12.8808 23.5391H4.92087C4.50702 23.5387 4.17194 23.203 4.17185 22.7891C4.17211 22.3751 4.5078 22.0391 4.92185 22.0391H12.8808ZM16.459 22.0391C16.8728 22.0392 17.2086 22.3752 17.209 22.7891C17.2088 23.2031 16.873 23.539 16.459 23.5391H14.7275C14.3134 23.5391 13.9777 23.2032 13.9775 22.7891C13.9778 22.3753 14.3128 22.0394 14.7265 22.0391H16.459ZM19.3564 22.0391C19.7705 22.0391 20.1071 22.3751 20.1074 22.7891C20.1073 23.2031 19.7714 23.5389 19.3574 23.5391H19.0635C18.6493 23.539 18.3135 23.2032 18.3135 22.7891C18.3137 22.3751 18.6495 22.0392 19.0635 22.0391H19.3564ZM10.1855 0.950222C10.3045 0.626539 10.6635 0.459613 10.9873 0.578152C11.3111 0.697051 11.4779 1.05701 11.3594 1.38089L9.4404 6.60842C11.2712 6.62023 12.534 6.984 13.6426 7.69924C13.9325 7.88633 14.016 8.2735 13.8291 8.5635C13.6419 8.85336 13.2548 8.93714 12.9648 8.75002C12.0834 8.18125 11.0469 7.85792 9.35154 7.85744C8.40737 7.85725 7.83011 8.29872 7.62888 8.71389C7.52952 8.91924 7.5134 9.12534 7.57224 9.31349C7.60658 9.42299 7.67297 9.54419 7.78611 9.66701C7.8081 9.64379 7.82999 9.6196 7.85251 9.59572C7.72688 9.31573 7.82074 8.97928 8.08787 8.81154C9.11751 8.16703 10.4737 8.28506 11.5732 8.68654C12.674 9.08861 13.7516 9.85814 14.2021 10.8174C14.371 11.1772 14.1513 11.6095 13.7685 11.6924C13.7617 11.7923 13.7544 11.8969 13.748 12.0049C14.1962 12.082 14.4968 12.046 14.6826 11.9756C14.8825 11.8997 14.9818 11.7762 15.0293 11.6182C15.1422 11.2404 14.9722 10.5056 14.2744 9.82814C14.0272 9.58772 14.0216 9.19189 14.2617 8.94435C14.5021 8.69712 14.8979 8.6915 15.1455 8.93166C15.3345 9.11519 15.5055 9.31499 15.6552 9.52443L17.8857 6.6133C18.0956 6.33963 18.4878 6.28747 18.7617 6.49709C19.0354 6.70683 19.0881 7.0991 18.8789 7.37307L16.249 10.8028C16.3391 11.2028 16.3391 11.6038 16.2275 11.9766C16.0692 12.5048 15.692 12.9296 15.125 13.1445C14.7132 13.3005 14.2295 13.3348 13.6894 13.2608C13.6825 13.4465 13.6757 13.6333 13.6699 13.8174C13.6519 14.3885 13.6397 14.9235 13.6318 15.3154C13.625 15.654 13.6211 15.8899 13.6201 15.9443C13.6201 16.2235 13.4305 16.4762 13.1601 16.5498C12.7201 16.6232 12.2788 16.6722 11.8427 16.7764C11.1206 16.949 10.2109 17.2517 9.40134 17.7647C7.89664 18.7182 6.97599 19.4365 6.3613 19.9883C5.74196 20.5443 5.43673 20.9247 5.09861 21.2852C4.91911 21.4765 4.6388 21.5354 4.39744 21.4326C4.1565 21.3297 4.00507 21.0869 4.01853 20.8252C4.17895 17.7398 4.63069 14.9266 5.55173 12.1611L5.59568 12.0596C5.61302 12.0278 5.63354 11.9972 5.65623 11.9688C6.10821 11.4021 6.46823 11.0234 6.88767 10.5947C6.89692 10.5853 6.90569 10.575 6.91501 10.5654C6.66279 10.3067 6.48116 10.0099 6.37986 9.68654C6.21786 9.1688 6.2782 8.63513 6.50388 8.16896C6.78976 7.5789 7.32765 7.10847 8.02146 6.84377L10.1855 0.950222ZM11.1445 9.86135C10.5565 9.64663 9.98214 9.56714 9.49802 9.62502C9.47457 9.6687 9.44776 9.71158 9.41306 9.75002C8.68746 10.5525 8.19322 11.0477 7.78122 11.4688C7.39591 11.8626 7.0838 12.1914 6.70212 12.6631C6.27084 13.9767 5.94845 15.307 5.71482 16.6826C6.1386 15.806 6.61502 14.9664 7.22654 14.0108C7.01626 13.3116 7.75399 11.8362 8.61033 11.6953C9.11052 11.6133 9.62805 11.94 9.67185 12.4834C9.72674 13.1687 9.35942 13.6875 8.95505 14.0547C8.71606 14.2717 8.41197 14.4145 8.124 14.4619C7.14963 15.9751 6.53411 17.1724 5.91208 18.7236C6.5649 18.1767 7.45483 17.5176 8.7324 16.708C9.70022 16.0948 10.7551 15.7512 11.5527 15.5606C11.8711 15.4845 12.1537 15.4336 12.3789 15.3975C12.3868 14.9958 12.401 14.4083 12.4209 13.7774C12.4553 12.684 12.512 11.4022 12.6025 10.7354C12.2417 10.3934 11.7319 10.076 11.1445 9.86135Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+1 -1
components/InteractionsPreview.tsx
··· 96 96 ); 97 97 }; 98 98 99 - const TagPopover = (props: { tags: string[] }) => { 99 + export const TagPopover = (props: { tags: string[] }) => { 100 100 return ( 101 101 <Popover 102 102 className="p-2! max-w-xs"
+4 -2
components/LoginButton.tsx
··· 5 5 import { ButtonPrimary } from "./Buttons"; 6 6 import { ActionButton } from "./ActionBar/ActionButton"; 7 7 import { AccountSmall } from "./Icons/AccountSmall"; 8 + import { useIsMobile } from "src/hooks/isMobile"; 8 9 9 10 export function LoginButton() { 10 11 let identityData = useIdentityData(); ··· 26 27 export function LoginActionButton() { 27 28 let identityData = useIdentityData(); 28 29 if (identityData.identity) return null; 30 + let isMobile = useIsMobile(); 29 31 return ( 30 32 <Popover 31 33 asChild 32 - align="start" 33 - side="right" 34 + side={isMobile ? "top" : "right"} 35 + align={isMobile ? "center" : "start"} 34 36 trigger={ 35 37 <ActionButton secondary icon={<AccountSmall />} label="Sign In" /> 36 38 }
+27
components/NavStateTracker.tsx
··· 1 + "use client"; 2 + 3 + import { usePathname } from "next/navigation"; 4 + import { useEffect, useRef } from "react"; 5 + 6 + export function NavStateTracker() { 7 + const pathname = usePathname(); 8 + const lastState = useRef<string | null>(null); 9 + 10 + useEffect(() => { 11 + let state: string | null = null; 12 + if (pathname === "/home") state = "home"; 13 + else if (pathname === "/reader" || pathname.startsWith("/reader/")) 14 + state = "reader"; 15 + 16 + if (state && state !== lastState.current) { 17 + lastState.current = state; 18 + fetch("/api/update-nav-state", { 19 + method: "POST", 20 + headers: { "Content-Type": "application/json" }, 21 + body: JSON.stringify({ state }), 22 + }); 23 + } 24 + }, [pathname]); 25 + 26 + return null; 27 + }
+1 -1
components/PageHeader.tsx
··· 29 29 <div 30 30 className={` 31 31 headerWrapper 32 - sticky top-0 z-10 32 + sticky top-0 z-20 33 33 w-full bg-transparent 34 34 `} 35 35 >
+36 -13
components/PageLayouts/DashboardLayout.tsx
··· 4 4 import { Header } from "../PageHeader"; 5 5 import { Footer } from "components/ActionBar/Footer"; 6 6 import { Sidebar } from "components/ActionBar/Sidebar"; 7 + import { DesktopNavigation } from "components/ActionBar/DesktopNavigation"; 8 + 9 + import { MobileNavigation } from "components/ActionBar/MobileNavigation"; 7 10 import { 8 - DesktopNavigation, 9 - MobileNavigation, 10 11 navPages, 11 12 NotificationButton, 12 - } from "components/ActionBar/Navigation"; 13 + } from "components/ActionBar/NavigationButtons"; 13 14 import { create } from "zustand"; 14 15 import { Popover } from "components/Popover"; 15 16 import { Checkbox } from "components/Checkbox"; ··· 26 27 import { ExternalLinkTiny } from "components/Icons/ExternalLinkTiny"; 27 28 import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 28 29 import { Tab } from "components/Tab"; 30 + import { PubIcon, PublicationButtons } from "components/ActionBar/Publications"; 29 31 30 32 export type DashboardState = { 31 33 display?: "grid" | "list"; ··· 69 71 }, 70 72 })); 71 73 72 - const DashboardIdContext = createContext<string | null>(null); 74 + export const DashboardIdContext = createContext<string | null>(null); 73 75 74 76 export const useDashboardId = () => { 75 77 const id = useContext(DashboardIdContext); ··· 138 140 defaultTab: keyof T; 139 141 currentPage: navPages; 140 142 publication?: string; 141 - actions: React.ReactNode; 143 + profileDid?: string; 144 + actions?: React.ReactNode; 145 + pageTitle?: string; 146 + onTabHover?: (tabName: string) => void; 142 147 }) { 143 148 const searchParams = useSearchParams(); 144 149 const tabParam = searchParams.get("tab"); ··· 165 170 let [headerState, setHeaderState] = useState<"default" | "controls">( 166 171 "default", 167 172 ); 173 + 168 174 return ( 169 175 <DashboardIdContext.Provider value={props.id}> 170 176 <div ··· 184 190 ref={ref} 185 191 id="home-content" 186 192 > 193 + {props.pageTitle && ( 194 + <PageTitle pageTitle={props.pageTitle} actions={props.actions} /> 195 + )} 196 + 187 197 {Object.keys(props.tabs).length <= 1 && !controls ? null : ( 188 198 <> 189 199 <Header> ··· 198 208 name={t} 199 209 selected={t === tab} 200 210 onSelect={() => setTabWithUrl(t)} 211 + onMouseEnter={() => props.onTabHover?.(t)} 212 + onPointerDown={() => props.onTabHover?.(t)} 201 213 /> 202 214 ); 203 215 })} ··· 240 252 <Footer> 241 253 <MobileNavigation 242 254 currentPage={props.currentPage} 243 - publication={props.publication} 255 + currentPublicationUri={props.publication} 256 + currentProfileDid={props.profileDid} 244 257 /> 245 - {props.actions && ( 246 - <> 247 - <Separator /> 248 - {props.actions} 249 - </> 250 - )} 251 258 </Footer> 252 259 </div> 253 260 </DashboardIdContext.Provider> 254 261 ); 255 262 } 263 + 264 + export const PageTitle = (props: { 265 + pageTitle: string; 266 + actions: React.ReactNode; 267 + }) => { 268 + return ( 269 + <MediaContents 270 + mobile={true} 271 + className="flex justify-between items-center px-1 mt-1 -mb-1 w-full " 272 + > 273 + <h4 className="grow truncate">{props.pageTitle}</h4> 274 + <div className="flex flex-row-reverse! gap-1">{props.actions}</div> 275 + {/* <div className="shrink-0 h-6">{props.controls}</div> */} 276 + </MediaContents> 277 + ); 278 + }; 256 279 257 280 export const HomeDashboardControls = (props: { 258 281 searchValue: string; ··· 447 470 className={`dashboardSearchInput 448 471 appearance-none! outline-hidden! 449 472 w-full min-w-0 text-primary relative pl-7 pr-1 -my-px 450 - border rounded-md border-transparent focus-within:border-border 473 + border rounded-md border-border-light focus-within:border-border 451 474 bg-transparent ${props.hasBackgroundImage ? "focus-within:bg-bg-page" : "focus-within:bg-bg-leaflet"} `} 452 475 type="text" 453 476 id="pubName"
+5
components/Pages/useHasBackgroundImage.ts
··· 1 + import { useHasBackgroundImageContext } from "components/ThemeManager/ThemeProvider"; 2 + 3 + export function useHasBackgroundImage(entityID?: string | null) { 4 + return useHasBackgroundImageContext(); 5 + }
+1 -1
components/Popover/index.tsx
··· 42 42 <NestedCardThemeProvider> 43 43 <RadixPopover.Content 44 44 className={` 45 - z-20 bg-bg-page 45 + z-20 relative bg-bg-page 46 46 px-3 py-2 text-primary 47 47 max-w-(--radix-popover-content-available-width) 48 48 max-h-(--radix-popover-content-available-height)
+220 -74
components/PostListing.tsx
··· 1 1 "use client"; 2 2 import { AtUri } from "@atproto/api"; 3 3 import { PubIcon } from "components/ActionBar/Publications"; 4 - import { CommentTiny } from "components/Icons/CommentTiny"; 5 - import { QuoteTiny } from "components/Icons/QuoteTiny"; 6 - import { Separator } from "components/Layout"; 7 4 import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 8 5 import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; 9 - import { useSmoker } from "components/Toast"; 10 6 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 7 import type { 12 8 NormalizedDocument, 13 9 NormalizedPublication, 14 10 } from "src/utils/normalizeRecords"; 11 + import { hasLeafletContent } from "lexicons/src/normalize"; 15 12 import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 16 13 17 14 import Link from "next/link"; 18 - import { InteractionPreview } from "./InteractionsPreview"; 15 + import { useEffect, useRef, useState } from "react"; 16 + import { InteractionPreview, TagPopover } from "./InteractionsPreview"; 19 17 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 18 + import { useSmoker } from "./Toast"; 19 + import { Separator } from "./Layout"; 20 + import { CommentTiny } from "./Icons/CommentTiny"; 21 + import { QuoteTiny } from "./Icons/QuoteTiny"; 22 + import { ShareTiny } from "./Icons/ShareTiny"; 23 + import { useSelectedPostListing } from "src/useSelectedPostState"; 20 24 import { mergePreferences } from "src/utils/mergePreferences"; 25 + import { ExternalLinkTiny } from "./Icons/ExternalLinkTiny"; 21 26 import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 27 + import { RecommendButton } from "./RecommendButton"; 22 28 23 29 export const PostListing = (props: Post) => { 24 30 let pubRecord = props.publication?.pubRecord as ··· 38 44 let isStandalone = !pubRecord; 39 45 let theme = usePubTheme(pubRecord?.theme || postRecord?.theme, isStandalone); 40 46 let themeRecord = pubRecord?.theme || postRecord?.theme; 47 + let elRef = useRef<HTMLDivElement>(null); 48 + let [hasBackgroundImage, setHasBackgroundImage] = useState(false); 49 + 50 + useEffect(() => { 51 + if (!themeRecord?.backgroundImage?.image || !elRef.current) { 52 + setHasBackgroundImage(false); 53 + return; 54 + } 55 + let alpha = Number( 56 + window 57 + .getComputedStyle(elRef.current) 58 + .getPropertyValue("--bg-page-alpha"), 59 + ); 60 + setHasBackgroundImage(alpha < 0.7); 61 + }, [themeRecord?.backgroundImage?.image]); 62 + 41 63 let backgroundImage = 42 64 themeRecord?.backgroundImage?.image?.ref && uri 43 65 ? blobRefToSrc(themeRecord.backgroundImage.image.ref, new AtUri(uri).host) ··· 50 72 ? pubRecord?.theme?.showPageBackground 51 73 : postRecord.theme?.showPageBackground ?? true; 52 74 53 - let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); 75 + let mergedPrefs = mergePreferences( 76 + postRecord?.preferences, 77 + pubRecord?.preferences, 78 + ); 54 79 55 - let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 80 + let quotes = 81 + props.documents.mentionsCount ?? 82 + props.documents.document_mentions_in_bsky?.[0]?.count ?? 83 + 0; 56 84 let comments = 57 85 mergedPrefs.showComments === false 58 86 ? 0 ··· 61 89 let tags = (postRecord?.tags as string[] | undefined) || []; 62 90 63 91 // For standalone posts, link directly to the document 64 - let postHref = getDocumentURL(postRecord, props.documents.uri, pubRecord); 92 + let postUrl = getDocumentURL(postRecord, props.documents.uri, pubRecord); 65 93 66 94 return ( 67 - <BaseThemeProvider {...theme} local> 68 - <div 69 - style={{ 70 - backgroundImage: backgroundImage 71 - ? `url(${backgroundImage})` 72 - : undefined, 73 - backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 74 - backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 75 - }} 76 - className={`no-underline! flex flex-row gap-2 w-full relative 77 - bg-bg-leaflet 78 - border border-border-light rounded-lg 79 - sm:p-2 p-2 selected-outline 80 - hover:outline-accent-contrast hover:border-accent-contrast 81 - `} 82 - > 83 - <Link className="h-full w-full absolute top-0 left-0" href={postHref} /> 95 + <div className="postListing flex flex-col gap-1"> 96 + <BaseThemeProvider {...theme} local> 84 97 <div 85 - className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`} 86 - style={{ 87 - backgroundColor: showPageBackground 88 - ? "rgba(var(--bg-page), var(--bg-page-alpha))" 89 - : "transparent", 90 - }} 98 + ref={elRef} 99 + id={`post-listing-${postUri}`} 100 + className={` 101 + relative 102 + flex flex-col overflow-hidden 103 + selected-outline border-border-light rounded-lg w-full hover:outline-accent-contrast 104 + hover:border-accent-contrast 105 + ${showPageBackground ? "bg-bg-page " : "bg-bg-leaflet"} `} 106 + style={ 107 + hasBackgroundImage 108 + ? { 109 + backgroundImage: backgroundImage 110 + ? `url(${backgroundImage})` 111 + : undefined, 112 + backgroundRepeat: backgroundImageRepeat 113 + ? "repeat" 114 + : "no-repeat", 115 + backgroundSize: backgroundImageRepeat 116 + ? `${backgroundImageSize}px` 117 + : "cover", 118 + } 119 + : {} 120 + } 91 121 > 92 - <h3 className="text-primary truncate">{postRecord.title}</h3> 93 - 94 - <p className="text-secondary italic line-clamp-3"> 95 - {postRecord.description} 96 - </p> 97 - <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full"> 98 - {props.publication && pubRecord && ( 99 - <PubInfo 100 - href={props.publication.href} 101 - pubRecord={pubRecord} 102 - uri={props.publication.uri} 103 - /> 104 - )} 105 - <div className="flex flex-row justify-between gap-2 items-center w-full"> 106 - <PostInfo publishedAt={postRecord.publishedAt} /> 107 - <InteractionPreview 108 - postUrl={postHref} 109 - quotesCount={quotes} 110 - commentsCount={comments} 111 - recommendsCount={recommends} 112 - documentUri={props.documents.uri} 113 - tags={tags} 114 - showComments={mergedPrefs.showComments !== false} 115 - showMentions={mergedPrefs.showMentions !== false} 116 - showRecommends={mergedPrefs.showRecommends !== false} 117 - share 122 + <Link 123 + className="h-full w-full absolute top-0 left-0" 124 + href={postUrl} 125 + /> 126 + {postRecord.coverImage && ( 127 + <div className="postListingImage"> 128 + <img 129 + src={blobRefToSrc(postRecord.coverImage.ref, postUri.host)} 130 + alt={postRecord.title || ""} 131 + className="w-full h-auto aspect-video object-cover object-top-left rounded" 118 132 /> 119 133 </div> 134 + )} 135 + <div className="postListingInfo px-3 py-2"> 136 + <h3 className="postListingTitle text-primary line-clamp-2 sm:text-lg text-base"> 137 + {postRecord.title} 138 + </h3> 139 + 140 + <p className="postListingDescription text-secondary line-clamp-3 sm:text-base text-sm"> 141 + {postRecord.description} 142 + </p> 143 + <div className="flex flex-col-reverse gap-2 text-sm text-tertiary items-center justify-start pt-1.5 w-full"> 144 + {props.publication && pubRecord && ( 145 + <PubInfo 146 + href={props.publication.href} 147 + pubRecord={pubRecord} 148 + uri={props.publication.uri} 149 + postRecord={postRecord} 150 + /> 151 + )} 152 + <div className="flex flex-row justify-between gap-2 text-xs items-center w-full"> 153 + <PostDate publishedAt={postRecord.publishedAt} /> 154 + {tags.length === 0 ? null : <TagPopover tags={tags!} />} 155 + </div> 156 + </div> 120 157 </div> 121 158 </div> 159 + </BaseThemeProvider> 160 + <div className="text-sm flex justify-between text-tertiary"> 161 + <Interactions 162 + postUrl={postUrl} 163 + quotesCount={quotes} 164 + commentsCount={comments} 165 + recommendsCount={recommends} 166 + tags={tags} 167 + showComments={mergedPrefs.showComments !== false} 168 + showMentions={mergedPrefs.showMentions !== false} 169 + documentUri={props.documents.uri} 170 + document={postRecord} 171 + /> 172 + <Share postUrl={postUrl} /> 122 173 </div> 123 - </BaseThemeProvider> 174 + </div> 124 175 ); 125 176 }; 126 177 ··· 128 179 href: string; 129 180 pubRecord: NormalizedPublication; 130 181 uri: string; 182 + postRecord: NormalizedDocument; 131 183 }) => { 184 + let isLeaflet = hasLeafletContent(props.postRecord); 185 + let cleanUrl = props.pubRecord.url 186 + ?.replace(/^https?:\/\//, "") 187 + .replace(/^www\./, ""); 188 + 132 189 return ( 133 - <div className="flex flex-col md:w-auto shrink-0 w-full"> 134 - <hr className="md:hidden block border-border-light mb-2" /> 135 - <Link 136 - href={props.href} 137 - className="text-accent-contrast font-bold no-underline text-sm flex gap-1 items-center md:w-fit relative shrink-0" 138 - > 139 - <PubIcon small record={props.pubRecord} uri={props.uri} /> 140 - {props.pubRecord.name} 141 - </Link> 190 + <div className="flex flex-col shrink-0 w-full"> 191 + <hr className=" block border-border-light mb-1" /> 192 + <div className="flex justify-between gap-4 w-full "> 193 + <Link 194 + href={props.href} 195 + className="text-accent-contrast font-bold no-underline text-sm flex gap-[6px] items-center relative grow w-max shrink-0 min-w-0" 196 + > 197 + <PubIcon tiny record={props.pubRecord} uri={props.uri} /> 198 + <div className="w-max min-w-0">{props.pubRecord.name}</div> 199 + </Link> 200 + {!isLeaflet && ( 201 + <div className="text-sm flex flex-row items-center text-tertiary gap-1 min-w-0"> 202 + <div className="truncate min-w-0">{cleanUrl}</div> 203 + <ExternalLinkTiny className="shrink-0" /> 204 + </div> 205 + )} 206 + </div> 142 207 </div> 143 208 ); 144 209 }; 145 210 146 - const PostInfo = (props: { publishedAt: string | undefined }) => { 211 + const PostDate = (props: { publishedAt: string | undefined }) => { 147 212 let localizedDate = useLocalizedDate(props.publishedAt || "", { 148 213 year: "numeric", 149 214 month: "short", 150 215 day: "numeric", 151 216 }); 217 + if (props.publishedAt) { 218 + return <div className="shrink-0 sm:text-sm text-xs">{localizedDate}</div>; 219 + } else return null; 220 + }; 221 + 222 + const Interactions = (props: { 223 + quotesCount: number; 224 + commentsCount: number; 225 + recommendsCount: number; 226 + tags?: string[]; 227 + postUrl: string; 228 + showComments: boolean; 229 + showMentions: boolean; 230 + documentUri: string; 231 + document: NormalizedDocument; 232 + }) => { 233 + let setSelectedPostListing = useSelectedPostListing( 234 + (s) => s.setSelectedPostListing, 235 + ); 236 + let selectPostListing = (drawer: "quotes" | "comments") => { 237 + setSelectedPostListing({ 238 + document_uri: props.documentUri, 239 + document: props.document, 240 + drawer, 241 + }); 242 + }; 243 + 152 244 return ( 153 - <div className="flex gap-2 items-center shrink-0 self-start"> 154 - {props.publishedAt && ( 155 - <> 156 - <div className="shrink-0">{localizedDate}</div> 157 - </> 158 - )} 245 + <div 246 + className={`flex gap-2 text-tertiary text-sm items-center justify-between px-1`} 247 + > 248 + <div className="postListingsInteractions flex gap-3"> 249 + <RecommendButton 250 + documentUri={props.documentUri} 251 + recommendsCount={props.recommendsCount} 252 + /> 253 + {!props.showMentions || props.quotesCount === 0 ? null : ( 254 + <button 255 + aria-label="Post quotes" 256 + onClick={() => selectPostListing("quotes")} 257 + className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast text-tertiary" 258 + > 259 + <QuoteTiny /> {props.quotesCount} 260 + </button> 261 + )} 262 + {!props.showComments || props.commentsCount === 0 ? null : ( 263 + <button 264 + aria-label="Post comments" 265 + onClick={() => selectPostListing("comments")} 266 + className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast text-tertiary" 267 + > 268 + <CommentTiny /> {props.commentsCount} 269 + </button> 270 + )} 271 + </div> 159 272 </div> 160 273 ); 161 274 }; 275 + 276 + const Share = (props: { postUrl: string }) => { 277 + let smoker = useSmoker(); 278 + return ( 279 + <button 280 + id={`copy-post-link-${props.postUrl}`} 281 + className="flex gap-1 items-center hover:text-accent-contrast relative font-bold" 282 + onClick={(e) => { 283 + e.stopPropagation(); 284 + e.preventDefault(); 285 + let mouseX = e.clientX; 286 + let mouseY = e.clientY; 287 + 288 + if (!props.postUrl) return; 289 + navigator.clipboard.writeText( 290 + props.postUrl.includes("http") 291 + ? props.postUrl 292 + : `leaflet.pub/${props.postUrl}`, 293 + ); 294 + 295 + smoker({ 296 + text: <strong>Copied Link!</strong>, 297 + position: { 298 + y: mouseY, 299 + x: mouseX, 300 + }, 301 + }); 302 + }} 303 + > 304 + Share <ShareTiny /> 305 + </button> 306 + ); 307 + };
+4
components/Tab.tsx
··· 4 4 name: string; 5 5 selected: boolean; 6 6 onSelect: () => void; 7 + onMouseEnter?: () => void; 8 + onPointerDown?: () => void; 7 9 href?: string; 8 10 }) => { 9 11 return ( 10 12 <div 11 13 className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`} 12 14 onClick={() => props.onSelect()} 15 + onMouseEnter={props.onMouseEnter} 16 + onPointerDown={props.onPointerDown} 13 17 > 14 18 {props.name} 15 19 {props.href && <ExternalLinkTiny />}
-1
components/Tags.tsx
··· 109 109 } 110 110 111 111 function selectTag(tag: string) { 112 - console.log("selected " + tag); 113 112 props.setSelectedTags([...props.selectedTags, tag]); 114 113 clearTagInput(); 115 114 }
+25 -16
components/ThemeManager/ThemeProvider.tsx
··· 8 8 export function useCardBorderHiddenContext() { 9 9 return useContext(CardBorderHiddenContext); 10 10 } 11 + 12 + // Context for hasBackgroundImage 13 + export const HasBackgroundImageContext = createContext<boolean>(false); 14 + 15 + export function useHasBackgroundImageContext() { 16 + return useContext(HasBackgroundImageContext); 17 + } 11 18 import { 12 19 colorToString, 13 20 useColorAttribute, ··· 79 86 80 87 return ( 81 88 <CardBorderHiddenContext.Provider value={!!cardBorderHiddenValue}> 82 - <BaseThemeProvider 83 - local={props.local} 84 - bgLeaflet={bgLeaflet} 85 - bgPage={bgPage} 86 - primary={primary} 87 - highlight2={highlight2} 88 - highlight3={highlight3} 89 - highlight1={highlight1?.data.value} 90 - accent1={accent1} 91 - accent2={accent2} 92 - showPageBackground={showPageBackground} 93 - pageWidth={pageWidth?.data.value} 94 - hasBackgroundImage={hasBackgroundImage} 95 - > 96 - {props.children} 97 - </BaseThemeProvider> 89 + <HasBackgroundImageContext.Provider value={hasBackgroundImage}> 90 + <BaseThemeProvider 91 + local={props.local} 92 + bgLeaflet={bgLeaflet} 93 + bgPage={bgPage} 94 + primary={primary} 95 + highlight2={highlight2} 96 + highlight3={highlight3} 97 + highlight1={highlight1?.data.value} 98 + accent1={accent1} 99 + accent2={accent2} 100 + showPageBackground={showPageBackground} 101 + pageWidth={pageWidth?.data.value} 102 + hasBackgroundImage={hasBackgroundImage} 103 + > 104 + {props.children} 105 + </BaseThemeProvider> 106 + </HasBackgroundImageContext.Provider> 98 107 </CardBorderHiddenContext.Provider> 99 108 ); 100 109 }
-1
src/replicache/mutations.ts
··· 666 666 } | null; 667 667 }> = async (args, ctx) => { 668 668 await ctx.runOnServer(async (serverCtx) => { 669 - console.log("updating"); 670 669 const updates: { 671 670 description?: string; 672 671 title?: string;
+16
src/useSelectedPostState.ts
··· 1 + import { create } from "zustand"; 2 + import type { NormalizedDocument } from "src/utils/normalizeRecords"; 3 + 4 + export type SelectedPostListing = { 5 + document_uri: string; 6 + document: NormalizedDocument; 7 + drawer: "quotes" | "comments"; 8 + }; 9 + 10 + export const useSelectedPostListing = create<{ 11 + selectedPostListing: SelectedPostListing | null; 12 + setSelectedPostListing: (post: SelectedPostListing | null) => void; 13 + }>((set) => ({ 14 + selectedPostListing: null, 15 + setSelectedPostListing: (post) => set({ selectedPostListing: post }), 16 + }));
+12
src/utils/useRecordFromDid.ts
··· 1 + import { callRPC } from "app/api/rpc/client"; 2 + import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 3 + import useSWR from "swr"; 4 + 5 + export function useRecordFromDid(did: string | undefined | null) { 6 + return useSWR(did ? ["profile-data", did] : null, async () => { 7 + const response = await callRPC("get_profile_data", { 8 + didOrHandle: did!, 9 + }); 10 + return response.result.profile as ProfileViewDetailed | undefined; 11 + }); 12 + }