a tool for shared writing and social publishing

started the reader

+340 -45
+1 -1
app/discover/page.tsx
··· 40 40 <div className="w-full h-full mx-auto bg-[#FDFCFA]"> 41 41 <DashboardLayout 42 42 id="discover" 43 - hasBackgroundImage={false} 43 + cardBorderHidden={false} 44 44 currentPage="discover" 45 45 defaultTab="default" 46 46 actions={null}
+2 -2
app/home/HomeEmpty/HomeEmpty.tsx
··· 88 88 export const DiscoverBanner = (props: { small?: boolean }) => { 89 89 return ( 90 90 <div 91 - className={`accent-container flex sm:py-2 gap-4 items-center ${props.small ? "items-start p-2 text-sm font-normal" : "items-center p-4"}`} 91 + className={`accent-container flex sm:py-2 gap-2 items-center ${props.small ? "items-start p-2 text-sm font-normal" : "items-center p-4"}`} 92 92 > 93 93 {props.small ? ( 94 - <DiscoverSmall className="shrink-0" /> 94 + <DiscoverSmall className="shrink-0 text-accent-contrast" /> 95 95 ) : ( 96 96 <div className="w-[64px] mx-auto"> 97 97 <DiscoverIllo />
+1 -2
app/home/HomeLayout.tsx
··· 95 95 return ( 96 96 <DashboardLayout 97 97 id="home" 98 - hasBackgroundImage={hasBackgroundImage} 98 + cardBorderHidden={cardBorderHidden} 99 99 currentPage="home" 100 100 defaultTab="home" 101 101 actions={<Actions />} ··· 192 192 {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === 193 193 0 && <PublicationBanner small />} 194 194 <DiscoverBanner small /> 195 - <div className="spacer h-8 w-full bg-transparent shrink-0 " /> 196 195 </> 197 196 ); 198 197 }
+1 -1
app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
··· 39 39 return ( 40 40 <DashboardLayout 41 41 id={publication.uri} 42 - hasBackgroundImage={!!record?.theme?.backgroundImage} 42 + cardBorderHidden={!!record.theme?.showPageBackground} 43 43 defaultTab="Drafts" 44 44 tabs={{ 45 45 Drafts: {
+92
app/reader/ReaderContent.tsx
··· 1 + "use client"; 2 + import { ShareSmall } from "components/Icons/ShareSmall"; 3 + import { Separator } from "components/Layout"; 4 + import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 5 + import Link from "next/link"; 6 + 7 + export const ReaderContent = (props: { root_entity: string }) => { 8 + let cardBorderHidden = useCardBorderHidden(props.root_entity); 9 + return ( 10 + <div className="flex flex-col gap-3"> 11 + {dummyPosts.map((p) => ( 12 + <Post {...p} cardBorderHidden={true} /> 13 + ))} 14 + </div> 15 + ); 16 + }; 17 + 18 + const Post = (props: { 19 + title: string; 20 + description: string; 21 + date: string; 22 + read: boolean; 23 + author: string; 24 + pub: string; 25 + cardBorderHidden: boolean; 26 + }) => { 27 + return ( 28 + <div 29 + className={`flex flex-col gap-0 ${props.cardBorderHidden ? "bg-bg-page" : "bg-bg-leaflet"} p-3 rounded-lg border border-border-light`} 30 + > 31 + <div 32 + className={`${props.cardBorderHidden ? "bg-transparent" : "bg-bg-page px-3 py-2"} rounded-md`} 33 + > 34 + <div className="flex justify-between gap-2"> 35 + <Link 36 + href={"/"} 37 + className="text-accent-contrast font-bold no-underline text-sm " 38 + > 39 + {props.pub} 40 + </Link> 41 + <button className="text-tertiary">{/*<ShareSmall />*/}</button> 42 + </div> 43 + <h3 className="truncate">{props.title}</h3> 44 + 45 + <p className="text-secondary">{props.description}</p> 46 + <div className="flex gap-2 text-sm text-tertiary items-center pt-3"> 47 + <div className="flex gap-[6px] items-center"> 48 + <div className="bg-test rounded-full h-4 w-4" /> 49 + {props.author} 50 + </div> 51 + <Separator classname="h-4 !min-h-0" /> 52 + {props.date} 53 + </div> 54 + </div> 55 + </div> 56 + ); 57 + }; 58 + 59 + let dummyPosts: { 60 + title: string; 61 + description: string; 62 + date: string; 63 + read: boolean; 64 + author: string; 65 + pub: string; 66 + }[] = [ 67 + { 68 + title: "First Post", 69 + description: "this is a description", 70 + date: "Oct 2", 71 + read: false, 72 + author: "jared", 73 + pub: "a warm space", 74 + }, 75 + { 76 + title: "This is a second Tost", 77 + description: "It has another description, as you can see", 78 + date: "Oct 2", 79 + read: false, 80 + author: "celine", 81 + pub: "Celine's Super Soliloquy", 82 + }, 83 + { 84 + title: "A Third Post, A Burnt Toast", 85 + description: 86 + "If the first post is bread, the second is toast, and inevitably the third is a plate of charcoal.", 87 + date: "Oct 2", 88 + read: false, 89 + author: "brendan", 90 + pub: "Scraps", 91 + }, 92 + ];
+3
app/reader/SubscriptionsContent.tsx
··· 1 + export const SubscriptionsContent = () => { 2 + return <div>subs here</div>; 3 + };
+147
app/reader/page.tsx
··· 1 + import { cookies } from "next/headers"; 2 + import { Fact, ReplicacheProvider, useEntity } from "src/replicache"; 3 + import type { Attribute } from "src/replicache/attributes"; 4 + import { 5 + ThemeBackgroundProvider, 6 + ThemeProvider, 7 + } from "components/ThemeManager/ThemeProvider"; 8 + import { EntitySetProvider } from "components/EntitySetProvider"; 9 + import { createIdentity } from "actions/createIdentity"; 10 + import { drizzle } from "drizzle-orm/node-postgres"; 11 + import { IdentitySetter } from "app/home/IdentitySetter"; 12 + import { getIdentityData } from "actions/getIdentityData"; 13 + import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; 14 + import { supabaseServerClient } from "supabase/serverClient"; 15 + import { pool } from "supabase/pool"; 16 + 17 + import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 18 + import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 19 + import { ReaderContent } from "./ReaderContent"; 20 + import { SubscriptionsContent } from "./SubscriptionsContent"; 21 + 22 + export default async function Reader(props: {}) { 23 + let cookieStore = await cookies(); 24 + let auth_res = await getIdentityData(); 25 + let identity: string | undefined; 26 + if (auth_res) identity = auth_res.id; 27 + else identity = cookieStore.get("identity")?.value; 28 + let needstosetcookie = false; 29 + if (!identity) { 30 + const client = await pool.connect(); 31 + const db = drizzle(client); 32 + let newIdentity = await createIdentity(db); 33 + client.release(); 34 + identity = newIdentity.id; 35 + needstosetcookie = true; 36 + } 37 + 38 + async function setCookie() { 39 + "use server"; 40 + 41 + (await cookies()).set("identity", identity as string, { 42 + sameSite: "strict", 43 + }); 44 + } 45 + 46 + let permission_token = auth_res?.home_leaflet; 47 + if (!permission_token) { 48 + let res = await supabaseServerClient 49 + .from("identities") 50 + .select( 51 + `*, 52 + permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)) 53 + `, 54 + ) 55 + .eq("id", identity) 56 + .single(); 57 + permission_token = res.data?.permission_tokens; 58 + } 59 + 60 + if (!permission_token) 61 + return ( 62 + <NotFoundLayout> 63 + <p className="font-bold">Sorry, we can't find this page!</p> 64 + <p> 65 + This may be a glitch on our end. If the issue persists please{" "} 66 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 67 + </p> 68 + </NotFoundLayout> 69 + ); 70 + let [homeLeafletFacts, allLeafletFacts] = await Promise.all([ 71 + supabaseServerClient.rpc("get_facts", { 72 + root: permission_token.root_entity, 73 + }), 74 + auth_res 75 + ? getFactsFromHomeLeaflets.handler( 76 + { 77 + tokens: auth_res.permission_token_on_homepage.map( 78 + (r) => r.permission_tokens.root_entity, 79 + ), 80 + }, 81 + { supabase: supabaseServerClient }, 82 + ) 83 + : undefined, 84 + ]); 85 + let initialFacts = 86 + (homeLeafletFacts.data as unknown as Fact<Attribute>[]) || []; 87 + let root_entity = permission_token.root_entity; 88 + 89 + if (!auth_res?.atp_did) return; 90 + let { data: publications } = await supabaseServerClient 91 + .from("publication_subscriptions") 92 + .select(`publications(*, documents_in_publications(documents(*)))`) 93 + .eq("identity", auth_res?.atp_did); 94 + 95 + let subbedPubs = publications?.map((pub) => { 96 + let subbedPubsArr: string[] = []; 97 + pub.publications?.name && subbedPubsArr.push(pub.publications?.name); 98 + return subbedPubsArr; 99 + }); 100 + 101 + console.log(subbedPubs); 102 + 103 + return ( 104 + <ReplicacheProvider 105 + rootEntity={root_entity} 106 + token={permission_token} 107 + name={root_entity} 108 + initialFacts={initialFacts} 109 + > 110 + <IdentitySetter cb={setCookie} call={needstosetcookie} /> 111 + <EntitySetProvider 112 + set={permission_token.permission_token_rights[0].entity_set} 113 + > 114 + <ThemeProvider entityID={root_entity}> 115 + <ThemeBackgroundProvider entityID={root_entity}> 116 + <DashboardLayout 117 + id="reader" 118 + cardBorderHidden={false} 119 + currentPage="discover" 120 + defaultTab="reader" 121 + actions={null} 122 + tabs={{ 123 + reader: { 124 + controls: <FocusToggle />, 125 + content: <ReaderContent root_entity={root_entity} />, 126 + }, 127 + subs: { 128 + controls: null, 129 + content: <SubscriptionsContent />, 130 + }, 131 + discover: { 132 + controls: null, 133 + content: <></>, 134 + href: "/discover", 135 + }, 136 + }} 137 + /> 138 + </ThemeBackgroundProvider> 139 + </ThemeProvider> 140 + </EntitySetProvider> 141 + </ReplicacheProvider> 142 + ); 143 + } 144 + 145 + const FocusToggle = () => { 146 + return <div className="grow flex justify-end">focus</div>; 147 + };
+19 -13
components/ActionBar/Navigation.tsx
··· 7 7 import { PublicationButtons } from "./Publications"; 8 8 import { Popover } from "components/Popover"; 9 9 import { MenuSmall } from "components/Icons/MenuSmall"; 10 + import { 11 + ReaderReadSmall, 12 + ReaderUnreadSmall, 13 + } from "components/Icons/ReaderSmall"; 10 14 11 15 export type navPages = "home" | "reader" | "pub" | "discover"; 12 16 ··· 110 114 }; 111 115 112 116 const ReaderButton = (props: { current?: boolean }) => { 113 - // let readerUnreads = true; 114 - // let subs = false; 117 + let readerUnreads = true; 118 + let subs = true; 115 119 116 - // if (subs) 117 - // return ( 118 - // <ActionButton 119 - // nav 120 - // icon={readerUnreads ? <ReaderUnreadSmall /> : <ReaderReadSmall />} 121 - // label="Reader" 122 - // className={` 123 - // ${readerUnreads ? "text-accent-contrast! border-accent-contrast" : props.current ? "bg-border-light! border-border" : ""} 124 - // `} 125 - // /> 126 - // ); 120 + if (subs) 121 + return ( 122 + <Link href={"/reader"} className="hover:no-underline!"> 123 + <ActionButton 124 + nav 125 + icon={readerUnreads ? <ReaderUnreadSmall /> : <ReaderReadSmall />} 126 + label="Reader" 127 + className={` 128 + ${readerUnreads ? "text-accent-contrast! border-accent-contrast" : props.current ? "bg-border-light! border-border" : ""} 129 + `} 130 + /> 131 + </Link> 132 + ); 127 133 return ( 128 134 <Link href={"/discover"} className="hover:no-underline!"> 129 135 <ActionButton
-1
components/ActionBar/Publications.tsx
··· 24 24 {identity.publications?.map((d) => { 25 25 // console.log("thisURI : " + d.uri); 26 26 // console.log("currentURI : " + props.currentPubUri); 27 - console.log(d.uri === props.currentPubUri); 28 27 29 28 return ( 30 29 <PublicationOption
+19
components/Icons/ExternalLinkTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const ExternalLinkTiny = (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="M4.94336 2.26074C5.22793 2.3192 5.44238 2.57118 5.44238 2.87305C5.44227 3.17482 5.22788 3.42693 4.94336 3.48535L4.81738 3.49805H4.37305C3.61366 3.49805 2.99805 4.11366 2.99805 4.87305V11.627C2.99818 12.3862 3.61374 13.002 4.37305 13.002H11.127C11.8863 13.002 12.5018 12.3862 12.502 11.627V11.1055C12.5021 10.7605 12.7819 10.4805 13.127 10.4805C13.472 10.4805 13.7518 10.7605 13.752 11.1055V11.627C13.7518 13.0766 12.5766 14.252 11.127 14.252H4.37305C2.92338 14.252 1.74818 13.0766 1.74805 11.627V4.87305C1.74805 3.4233 2.9233 2.24805 4.37305 2.24805H4.81738L4.94336 2.26074ZM13.127 1.74805C13.7482 1.74809 14.252 2.25176 14.252 2.87305V8.04199C14.2518 8.66317 13.7482 9.16694 13.127 9.16699C12.5058 9.16693 12.0021 8.66316 12.002 8.04199V5.58887L7.25488 10.3359L7.16895 10.4141C6.7271 10.774 6.07483 10.7476 5.66309 10.3359C5.22426 9.89664 5.22413 9.18433 5.66309 8.74512L10.4102 3.99805H7.95703C7.33606 3.99774 6.83216 3.49406 6.83203 2.87305C6.83203 2.25192 7.33597 1.74836 7.95703 1.74805H13.127Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+11 -10
components/PageHeader.tsx
··· 3 3 4 4 export const Header = (props: { 5 5 children: React.ReactNode; 6 - hasBackgroundImage: boolean; 6 + cardBorderHidden: boolean; 7 7 }) => { 8 8 let [scrollPos, setScrollPos] = useState(0); 9 + console.log(props.cardBorderHidden); 9 10 10 11 useEffect(() => { 11 12 const homeContent = document.getElementById("home-content"); ··· 22 23 } 23 24 }, []); 24 25 25 - let headerBGColor = props.hasBackgroundImage 26 - ? "var(--bg-page)" 27 - : "var(--bg-leaflet)"; 26 + let headerBGColor = props.cardBorderHidden 27 + ? "var(--bg-leaflet)" 28 + : "var(--bg-page)"; 28 29 29 30 return ( 30 31 <div ··· 55 56 scrollPos < 20 56 57 ? { 57 58 backgroundColor: `rgba(${headerBGColor}, ${scrollPos / 60 + 0.75})`, 58 - paddingLeft: props.hasBackgroundImage 59 - ? "8px" 60 - : `calc(${scrollPos / 20}*8px)`, 61 - paddingRight: props.hasBackgroundImage 62 - ? "8px" 63 - : `calc(${scrollPos / 20}*8px)`, 59 + paddingLeft: props.cardBorderHidden 60 + ? `calc(${scrollPos / 20}*8px)` 61 + : "8px", 62 + paddingRight: props.cardBorderHidden 63 + ? `calc(${scrollPos / 20}*8px)` 64 + : "8px", 64 65 } 65 66 : { 66 67 backgroundColor: `rgb(${headerBGColor})`,
+43 -14
components/PageLayouts/DashboardLayout.tsx
··· 20 20 import { SearchTiny } from "components/Icons/SearchTiny"; 21 21 import { InterfaceState, useIdentityData } from "components/IdentityProvider"; 22 22 import { updateIdentityInterfaceState } from "actions/updateIdentityInterfaceState"; 23 + import Link from "next/link"; 24 + import { ExternalLinkTiny } from "components/Icons/ExternalLinkTiny"; 23 25 24 26 export type DashboardState = { 25 27 display?: "grid" | "list"; ··· 117 119 118 120 export function DashboardLayout< 119 121 T extends { 120 - [name: string]: { content: React.ReactNode; controls: React.ReactNode }; 122 + [name: string]: { 123 + content: React.ReactNode; 124 + controls: React.ReactNode; 125 + href?: string; 126 + }; 121 127 }, 122 128 >(props: { 123 129 id: string; 124 - hasBackgroundImage: boolean; 130 + cardBorderHidden: boolean; 125 131 tabs: T; 126 132 defaultTab: keyof T; 127 133 currentPage: navPages; ··· 129 135 actions: React.ReactNode; 130 136 }) { 131 137 let [tab, setTab] = useState(props.defaultTab); 132 - let { content, controls } = props.tabs[tab]; 138 + let { content, controls, href } = props.tabs[tab]; 133 139 134 140 let [headerState, setHeaderState] = useState<"default" | "controls">( 135 141 "default", ··· 154 160 > 155 161 {Object.keys(props.tabs).length <= 1 && !controls ? null : ( 156 162 <> 157 - <Header hasBackgroundImage={props.hasBackgroundImage}> 163 + <Header cardBorderHidden={props.cardBorderHidden}> 158 164 {headerState === "default" ? ( 159 165 <> 160 166 {Object.keys(props.tabs).length > 1 && ( 161 167 <div className="pubDashTabs flex flex-row gap-1"> 162 - {Object.keys(props.tabs).map((t) => ( 163 - <Tab 164 - key={t} 165 - name={t} 166 - selected={t === tab} 167 - onSelect={() => setTab(t)} 168 - /> 169 - ))} 168 + {Object.keys(props.tabs).map((t) => { 169 + if (props.tabs[t].href) 170 + return ( 171 + <Link 172 + key={t} 173 + href={props.tabs[t].href} 174 + className="no-underline" 175 + > 176 + <Tab 177 + name={t} 178 + selected={t === tab} 179 + href={props.tabs[t].href} 180 + onSelect={() => setTab(t)} 181 + /> 182 + </Link> 183 + ); 184 + return ( 185 + <Tab 186 + key={t} 187 + name={t} 188 + selected={t === tab} 189 + onSelect={() => setTab(t)} 190 + /> 191 + ); 192 + })} 170 193 </div> 171 194 )} 172 195 {props.publication && ( ··· 326 349 ); 327 350 }; 328 351 329 - function Tab(props: { name: string; selected: boolean; onSelect: () => void }) { 352 + function Tab(props: { 353 + name: string; 354 + selected: boolean; 355 + onSelect: () => void; 356 + href?: string; 357 + }) { 330 358 return ( 331 359 <div 332 - className={`pubTabs px-1 py-0 rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`} 360 + 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"}`} 333 361 onClick={() => props.onSelect()} 334 362 > 335 363 {props.name} 364 + {props.href && <ExternalLinkTiny />} 336 365 </div> 337 366 ); 338 367 }
+1 -1
feeds/index.ts
··· 33 33 34 34 let { data: publications } = await supabaseServerClient 35 35 .from("publication_subscriptions") 36 - .select(`publications(documents_in_publications(documents(*)))`) 36 + .select(`publications(*, documents_in_publications(documents(*)))`) 37 37 .eq("identity", auth); 38 38 39 39 const allPosts = (publications || [])