diff --git a/actions/getIdentityData.ts b/actions/getIdentityData.ts index 97e0fc14..b2968f8a 100644 --- a/actions/getIdentityData.ts +++ b/actions/getIdentityData.ts @@ -25,7 +25,7 @@ export async function getIdentityData() { id, root_entity, permission_token_rights(*), - leaflets_in_publications(*, publications(*)) + leaflets_in_publications(*, publications(*), documents(*)) ) ) )`, diff --git a/actions/updateIdentityInterfaceState.ts b/actions/updateIdentityInterfaceState.ts new file mode 100644 index 00000000..5ed8cbc7 --- /dev/null +++ b/actions/updateIdentityInterfaceState.ts @@ -0,0 +1,16 @@ +"use server"; + +import { InterfaceState } from "components/IdentityProvider"; +import { getIdentityData } from "./getIdentityData"; +import { supabaseServerClient } from "supabase/serverClient"; + +export async function updateIdentityInterfaceState( + interfaceState: InterfaceState, +) { + let identity = await getIdentityData(); + if (!identity) return; + await supabaseServerClient + .from("identities") + .update({ interface_state: interfaceState }) + .eq("id", identity.id); +} diff --git a/app/[leaflet_id]/Actions.tsx b/app/[leaflet_id]/Actions.tsx index 9d6d5df6..ebb6c89f 100644 --- a/app/[leaflet_id]/Actions.tsx +++ b/app/[leaflet_id]/Actions.tsx @@ -27,7 +27,7 @@ export const BackToPubButton = (props: { return ( } diff --git a/app/[leaflet_id]/page.tsx b/app/[leaflet_id]/page.tsx index 4468340b..81376965 100644 --- a/app/[leaflet_id]/page.tsx +++ b/app/[leaflet_id]/page.tsx @@ -12,6 +12,7 @@ import { PageSWRDataProvider } from "components/PageSWRDataProvider"; import { getPollData } from "actions/pollActions"; import { supabaseServerClient } from "supabase/serverClient"; import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; export const preferredRegion = ["sfo1"]; export const dynamic = "force-dynamic"; @@ -29,20 +30,13 @@ export default async function LeafletPage(props: Props) { let rootEntity = res.data?.root_entity; if (!rootEntity || !res.data || res.data.blocked_by_admin) return ( -
-
-
- Hmmm…we couldn't find that Leaflet. -
-
- You can{" "} - - email us - {" "} - for help! -
-
-
+ +

Sorry, we can't find this leaflet!

+

+ This may be a glitch on our end. If the issue persists please{" "} + send us a note. +

+
); let [{ data }, rsvp_data, poll_data] = await Promise.all([ diff --git a/app/[leaflet_id]/publish/PublishIllustration/PublishIllustration.tsx b/app/[leaflet_id]/publish/PublishIllustration/PublishIllustration.tsx index ee414535..df421612 100644 --- a/app/[leaflet_id]/publish/PublishIllustration/PublishIllustration.tsx +++ b/app/[leaflet_id]/publish/PublishIllustration/PublishIllustration.tsx @@ -78,8 +78,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { Math.pow((isPlanet ? y + 14 : y) - (moonPosY + 41), 2), ); if (moonDistance < 60) { - console.log("moon collision!"); - return true; } @@ -90,8 +88,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { Math.pow(y - (isPlanet ? star.y + 14 : star.y), 2), ); if (starDistance < 40) { - console.log("star collision!"); - return true; } } @@ -147,7 +143,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { y: y, rot: Math.random() * 360, }); - console.log("planet : " + type); } // the first, fouth, and seventh stars are large stars else if ( @@ -156,7 +151,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { i === numberOfPlanets + 6 ) { let type = Math.floor(Math.random() * 4); - console.log("big star : " + type); stars.push({ type: type === 0 @@ -172,7 +166,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { }); } else { let type = Math.floor(Math.random() * 3); - console.log("small star : " + type); stars.push({ type: type === 0 ? "S1" : type === 1 ? "S2" : "S3", @@ -181,7 +174,6 @@ export const PublishIllustration = (props: { posts_in_pub: number }) => { rot: Math.random() * 360, }); } - console.log("planetcount : " + JSON.stringify(planetCounter)); } } // animate the last child diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index d1aad4f3..10bd8499 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -133,7 +133,7 @@ const PublishPostForm = (
-
+
Back @@ -200,7 +200,7 @@ const PublishPostSuccess = (props: {

Published!

Back to Dashboard diff --git a/app/[leaflet_id]/publish/page.tsx b/app/[leaflet_id]/publish/page.tsx index fd09533d..578afb69 100644 --- a/app/[leaflet_id]/publish/page.tsx +++ b/app/[leaflet_id]/publish/page.tsx @@ -1,5 +1,4 @@ import { supabaseServerClient } from "supabase/serverClient"; -import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; import { PublishPost } from "./PublishPost"; import { PubLeafletPublication } from "lexicons/api"; import { getIdentityData } from "actions/getIdentityData"; @@ -15,36 +14,42 @@ type Props = { // this is now a token id not leaflet! Should probs rename params: Promise<{ leaflet_id: string }>; }; -export default async function LeafletPage(props: Props) { +export default async function PublishLeafletPage(props: Props) { let leaflet_id = (await props.params).leaflet_id; - let { result: res } = await get_leaflet_data.handler( - { token_id: leaflet_id }, - { supabase: supabaseServerClient }, - ); - let rootEntity = res.data?.root_entity; - if ( - !rootEntity || - !res.data || - res.data.blocked_by_admin || - !res.data.leaflets_in_publications[0] - ) + let { data } = await supabaseServerClient + .from("permission_tokens") + .select( + `*, + permission_token_rights(*), + leaflets_in_publications( + *, + publications( + *, + documents_in_publications(count) + ), + documents(*))`, + ) + .eq("id", leaflet_id) + .single(); + let rootEntity = data?.root_entity; + if (!data || !rootEntity || !data.leaflets_in_publications[0]) return (
missin something -
{JSON.stringify(res.data, undefined, 2)}
+
{JSON.stringify(data, undefined, 2)}
); let identity = await getIdentityData(); if (!identity || !identity.atp_did) return null; - let pub = res.data.leaflets_in_publications[0]; + let pub = data.leaflets_in_publications[0]; let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); let profile = await agent.getProfile({ actor: identity.atp_did }); return ( diff --git a/app/api/rpc/[command]/getFactsFromHomeLeaflets.ts b/app/api/rpc/[command]/getFactsFromHomeLeaflets.ts index f4baacd8..cfb49308 100644 --- a/app/api/rpc/[command]/getFactsFromHomeLeaflets.ts +++ b/app/api/rpc/[command]/getFactsFromHomeLeaflets.ts @@ -3,6 +3,11 @@ import type { Fact } from "src/replicache"; import type { Attribute } from "src/replicache/attributes"; import { makeRoute } from "../lib"; import type { Env } from "./route"; +import { scanIndexLocal } from "src/replicache/utils"; +import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks"; +import * as base64 from "base64-js"; +import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; +import { applyUpdate, Doc } from "yjs"; export const getFactsFromHomeLeaflets = makeRoute({ route: "getFactsFromHomeLeaflets", @@ -16,15 +21,39 @@ export const getFactsFromHomeLeaflets = makeRoute({ }); if (all_facts.data) { + let titles = {} as { [key: string]: string }; + + let facts = all_facts.data.reduce( + (acc, fact) => { + if (!acc[fact.root_id]) acc[fact.root_id] = []; + acc[fact.root_id].push(fact as unknown as Fact); + return acc; + }, + {} as { [key: string]: Fact[] }, + ); + for (let token of tokens) { + let scan = scanIndexLocal(facts[token]); + let [root] = scan.eav(token, "root/page"); + let rootEntity = root?.data.value || token; + let [title] = getBlocksWithTypeLocal(facts[token], rootEntity).filter( + (b) => b.type === "text" || b.type === "heading", + ); + if (!title) titles[token] = "Untitled"; + else { + let [content] = scan.eav(title.value, "block/text"); + if (!content) titles[token] = "Untitled"; + else { + let doc = new Doc(); + const update = base64.toByteArray(content.data.value); + applyUpdate(doc, update); + let nodes = doc.getXmlElement("prosemirror").toArray(); + let stringValue = YJSFragmentToString(nodes[0]); + titles[token] = stringValue; + } + } + } return { - result: all_facts.data.reduce( - (acc, fact) => { - if (!acc[fact.root_id]) acc[fact.root_id] = []; - acc[fact.root_id].push(fact as unknown as Fact); - return acc; - }, - {} as { [key: string]: Fact[] }, - ), + result: { facts, titles }, }; } diff --git a/app/api/rpc/[command]/get_leaflet_data.ts b/app/api/rpc/[command]/get_leaflet_data.ts index 874018f5..e8390b9e 100644 --- a/app/api/rpc/[command]/get_leaflet_data.ts +++ b/app/api/rpc/[command]/get_leaflet_data.ts @@ -6,7 +6,7 @@ export type GetLeafletDataReturnType = Awaited< ReturnType<(typeof get_leaflet_data)["handler"]> >; -const leaflets_in_publications_query = `leaflets_in_publications(*, publications(*, documents_in_publications(count)), documents(*))`; +const leaflets_in_publications_query = `leaflets_in_publications(*, publications(*), documents(*))`; export const get_leaflet_data = makeRoute({ route: "get_leaflet_data", input: z.object({ diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index baa775c4..8af7dc5b 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { makeRoute } from "../lib"; import type { Env } from "./route"; import { AtUri } from "@atproto/syntax"; +import { getFactsFromHomeLeaflets } from "./getFactsFromHomeLeaflets"; export type GetPublicationDataReturnType = Awaited< ReturnType<(typeof get_publication_data)["handler"]> @@ -36,6 +37,7 @@ export const get_publication_data = makeRoute({ publication_subscriptions(*, identities(bsky_profiles(*))), publication_domains(*), leaflets_in_publications(*, + documents(*), permission_tokens(*, permission_token_rights(*), custom_domain_routes!custom_domain_routes_edit_permission_token_fkey(*) @@ -46,6 +48,16 @@ export const get_publication_data = makeRoute({ .eq("identity_did", did) .single(); - return { result: publication }; + let leaflet_data = await getFactsFromHomeLeaflets.handler( + { + tokens: + publication?.leaflets_in_publications.map( + (l) => l.permission_tokens?.root_entity!, + ) || [], + }, + { supabase }, + ); + + return { result: { publication, leaflet_data: leaflet_data.result } }; }, }); diff --git a/app/discover/PubListing.tsx b/app/discover/PubListing.tsx index 16132adf..c8c9d741 100644 --- a/app/discover/PubListing.tsx +++ b/app/discover/PubListing.tsx @@ -4,6 +4,7 @@ import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; +import { timeAgo } from "src/utils/timeAgo"; import { Json } from "supabase/database.types"; export const PubListing = (props: { @@ -35,7 +36,7 @@ export const PubListing = (props: { backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, }} - className={`!no-underline flex flex-row gap-2 + className={`no-underline! flex flex-row gap-2 bg-bg-leaflet border border-border-light rounded-lg px-3 py-3 selected-outline @@ -69,26 +70,3 @@ export const PubListing = (props: { ); }; - -export function timeAgo(timestamp: string): string { - const now = new Date(); - const date = new Date(timestamp); - const diffMs = now.getTime() - date.getTime(); - const diffSeconds = Math.floor(diffMs / 1000); - const diffMinutes = Math.floor(diffSeconds / 60); - const diffHours = Math.floor(diffMinutes / 60); - const diffDays = Math.floor(diffHours / 24); - const diffYears = Math.floor(diffDays / 365); - - if (diffYears > 0) { - return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`; - } else if (diffDays > 0) { - return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; - } else if (diffHours > 0) { - return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; - } else if (diffMinutes > 0) { - return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`; - } else { - return "just now"; - } -} diff --git a/app/discover/SortedPublicationList.tsx b/app/discover/SortedPublicationList.tsx index 438dcf01..d2c4ce74 100644 --- a/app/discover/SortedPublicationList.tsx +++ b/app/discover/SortedPublicationList.tsx @@ -26,7 +26,6 @@ export function SortedPublicationList(props: { ?.filter((pub) => pub.documents_in_publications.length > 0) ?.sort((a, b) => { if (order === "popular") { - console.log("sorting by popularity"); return ( b.publication_subscriptions[0].count - a.publication_subscriptions[0].count diff --git a/app/discover/page.tsx b/app/discover/page.tsx index 1b17da40..7ae75c98 100644 --- a/app/discover/page.tsx +++ b/app/discover/page.tsx @@ -2,6 +2,7 @@ import { supabaseServerClient } from "supabase/serverClient"; import Link from "next/link"; import { SortedPublicationList } from "./SortedPublicationList"; import { Metadata } from "next"; +import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; export const dynamic = "force-static"; export const revalidate = 60; @@ -36,17 +37,37 @@ export default async function Discover(props: { let publications = await getPublications(); return ( -
-
-
-

Discover

-

- Explore publications on Leaflet ✨ Or{" "} - make your own! -

-
- -
+
+ , + }, + }} + />
); } + +const DiscoverContent = async (props: { order: string }) => { + let publications = await getPublications(); + + return ( +
+
+

Discover

+

+ Explore publications on Leaflet ✨ Or{" "} + make your own! +

+
+ +
+ ); +}; diff --git a/app/globals.css b/app/globals.css index 264fae81..e617633c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,88 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; + +@theme inline { + --breakpoint-*: initial; + --breakpoint-sm: 640px; + --breakpoint-md: 960px; + --breakpoint-lg: 1280px; + + --radius-*: initial; + --radius-none: 0; + --radius-md: 0.25rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; + + --color-*: initial; + --color-inherit: inherit; + --color-transparent: transparent; + --color-current: currentColor; + --color-primary: rgb(var(--primary)); + --color-secondary: color-mix( + in oklab, + rgb(var(--primary)), + rgb(var(--bg-page)) 25% + ); + --color-tertiary: color-mix( + in oklab, + rgb(var(--primary)), + rgb(var(--bg-page)) 55% + ); + --color-border: color-mix( + in oklab, + rgb(var(--primary)), + rgb(var(--bg-page)) 75% + ); + --color-border-light: color-mix( + in oklab, + rgb(var(--primary)), + rgb(var(--bg-page)) 85% + ); + --color-white: #ffffff; + --color-accent-1: rgb(var(--accent-1)); + --color-accent-2: rgb(var(--accent-2)); + --color-accent-contrast: rgb(var(--accent-contrast)); + --color-bg-leaflet: rgb(var(--bg-leaflet)); + --color-bg-page: rgb(var(--bg-page)); + --color-highlight-1: var(--highlight-1); + --color-highlight-2: rgb(var(--highlight-2)); + --color-highlight-3: rgb(var(--highlight-3)); + --color-test: #e18181; + --color-test-blue: #48d1ef; + + --text-*: initial; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.625rem; + --text-2xl: 2rem; + + --shadow-sm: 0.9px 1.5px 1.7px -1.8px rgba(var(--primary), 0.2), + 4.2px 6.9px 7.8px -3.5px rgba(var(--primary), 0.15); + --shadow-md: 1.2px 2.5px 2.7px -1.8px rgba(var(--primary), 0.1), + 5.6px 11.6px 12.5px -3.5px rgba(var(--primary), 0.15); + + --font-sans: var(--font-quattro); + --font-serif: Garamond; +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} @layer base { :root { @@ -103,10 +185,7 @@ @apply text-accent-contrast; @apply hover:cursor-pointer; @apply no-underline; - } - - a:hover { - @apply underline; + @apply hover:underline; } pre { @@ -127,6 +206,9 @@ /* END GLOBAL STYLING */ } +button:hover { + cursor: pointer; +} blockquote { margin: 0; @@ -165,10 +247,10 @@ input[type="number"] { font-size: 1em; @apply bg-border-light; @apply font-mono; - @apply px-[1px]; - @apply py-[1px]; - @apply -mx-[1px]; - @apply -my-[1px]; + @apply px-px; + @apply py-px; + @apply -mx-px; + @apply -my-px; @apply rounded-[4px]; @apply box-decoration-clone; } @@ -191,10 +273,10 @@ pre.shiki { } .highlight { - @apply px-[1px]; - @apply py-[1px]; - @apply -mx-[1px]; - @apply -my-[1px]; + @apply px-px; + @apply py-px; + @apply -mx-px; + @apply -my-px; @apply rounded-[4px]; @apply box-decoration-clone; } @@ -213,19 +295,19 @@ pre.shiki { } .transparent-outline { - @apply outline; + @apply outline-solid; @apply outline-transparent; } .selected-outline { @apply border; - @apply focus:outline; + @apply focus:outline-solid; @apply focus:outline-2; @apply focus:outline-offset-1; - @apply focus-within:outline; + @apply focus-within:outline-solid; @apply focus-within:outline-2; @apply focus-within:outline-offset-1; - @apply hover:outline; + @apply hover:outline-solid; @apply hover:outline-2; @apply hover:outline-offset-1; } @@ -240,13 +322,13 @@ pre.shiki { @apply hover:border-tertiary; @apply focus:border-tertiary; - @apply focus:outline; + @apply focus:outline-solid; @apply focus:outline-tertiary; @apply focus:outline-2; @apply focus:outline-offset-1; @apply focus-within:border-tertiary; - @apply focus-within:outline; + @apply focus-within:outline-solid; @apply focus-within:outline-tertiary; @apply focus-within:outline-2; @apply focus-within:outline-offset-1; @@ -260,7 +342,7 @@ pre.shiki { @apply border; @apply border-border-light; @apply rounded-lg; - @apply outline; + @apply outline-solid; @apply outline-offset-1; @apply outline-2; @apply outline-transparent; @@ -271,7 +353,7 @@ pre.shiki { @apply border; @apply border-border; @apply rounded-lg; - @apply outline; + @apply outline-solid; @apply outline-offset-1; @apply outline-2; @apply outline-border; diff --git a/app/home/AccountSettings.tsx b/app/home/Actions/AccountSettings.tsx similarity index 100% rename from app/home/AccountSettings.tsx rename to app/home/Actions/AccountSettings.tsx diff --git a/app/home/HomeSidebar.tsx b/app/home/Actions/Actions.tsx similarity index 68% rename from app/home/HomeSidebar.tsx rename to app/home/Actions/Actions.tsx index 1f909819..8e09c1db 100644 --- a/app/home/HomeSidebar.tsx +++ b/app/home/Actions/Actions.tsx @@ -3,24 +3,19 @@ import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { CreateNewLeafletButton } from "./CreateNewButton"; import { HelpPopover } from "components/HelpPopover"; import { AccountSettings } from "./AccountSettings"; -import { Sidebar } from "components/ActionBar/Sidebar"; import { useIdentityData } from "components/IdentityProvider"; import { useReplicache } from "src/replicache"; import { LoginActionButton } from "components/LoginButton"; -import { MyPublicationList } from "./Publications"; -export const HomeSidebar = () => { +export const Actions = () => { let { identity } = useIdentityData(); let { rootEntity } = useReplicache(); - return ( - + <> {identity ? : } - + {/**/} -
- -
+ ); }; diff --git a/app/home/CreateNewButton.tsx b/app/home/Actions/CreateNewButton.tsx similarity index 99% rename from app/home/CreateNewButton.tsx rename to app/home/Actions/CreateNewButton.tsx index 58605c0e..be84e719 100644 --- a/app/home/CreateNewButton.tsx +++ b/app/home/Actions/CreateNewButton.tsx @@ -56,7 +56,7 @@ export const CreateNewLeafletButton = (props: {}) => { id="new-leaflet-button" primary icon= - label="New Doc" + label="New" /> } > diff --git a/app/home/HomeHelp.tsx b/app/home/Actions/HomeHelp.tsx similarity index 100% rename from app/home/HomeHelp.tsx rename to app/home/Actions/HomeHelp.tsx diff --git a/app/home/HomeEmpty/DiscoverIllo.tsx b/app/home/HomeEmpty/DiscoverIllo.tsx new file mode 100644 index 00000000..edb6f40c --- /dev/null +++ b/app/home/HomeEmpty/DiscoverIllo.tsx @@ -0,0 +1,26 @@ +import { theme } from "tailwind.config"; +export const DiscoverIllo = () => { + return ( + + + + + + ); +}; diff --git a/app/home/HomeEmpty/HomeEmpty.tsx b/app/home/HomeEmpty/HomeEmpty.tsx new file mode 100644 index 00000000..1dcab01d --- /dev/null +++ b/app/home/HomeEmpty/HomeEmpty.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { PubListEmptyIllo } from "components/ActionBar/Publications"; +import { ButtonPrimary } from "components/Buttons"; +import { AddSmall } from "components/Icons/AddSmall"; +import { Link } from "react-aria-components"; +import { DiscoverIllo } from "./DiscoverIllo"; +import { WelcomeToLeafletIllo } from "./WelcomeToLeafletIllo"; +import { DiscoverSmall } from "components/Icons/DiscoverSmall"; +import { PublishSmall } from "components/Icons/PublishSmall"; +import { createNewLeaflet } from "actions/createNewLeaflet"; +import { useIsMobile } from "src/hooks/isMobile"; + +export function HomeEmptyState() { + let isMobile = useIsMobile(); + return ( +
+
+
+ +
+
+

Leaflet

+ {/*

A platform for social publishing.

*/} +
+ Write and share delightful documents! +
+ { + let openNewLeaflet = (id: string) => { + if (isMobile) { + window.location.href = `/${id}?focusFirstBlock`; + } else { + window.open(`/${id}?focusFirstBlock`, "_blank"); + } + }; + + let id = await createNewLeaflet({ + pageType: "doc", + redirectUser: false, + }); + openNewLeaflet(id); + }} + > + Write a Doc! + +
+
+
+
+
or
+
+
+ + + +
+ Note: Right now docs and publications are separate. Soon you'll be able + to send docs to pubs! +
+
+ ); +} + +export const PublicationBanner = (props: { small?: boolean }) => { + return ( +
+ {props.small ? ( + + ) : ( +
+ +
+ )} +
+ + Start a Publication + {" "} + and blog on the Atmosphere +
+
+ ); +}; + +export const DiscoverBanner = (props: { small?: boolean }) => { + return ( +
+ {props.small ? ( + + ) : ( +
+ +
+ )} +
+ + Explore Publications + {" "} + already on the network +
+
+ ); +}; diff --git a/app/home/HomeEmpty/WelcomeToLeafletIllo.tsx b/app/home/HomeEmpty/WelcomeToLeafletIllo.tsx new file mode 100644 index 00000000..fc08f443 --- /dev/null +++ b/app/home/HomeEmpty/WelcomeToLeafletIllo.tsx @@ -0,0 +1,24 @@ +import { theme } from "tailwind.config"; + +export const WelcomeToLeafletIllo = () => { + return ( + + + + + ); +}; diff --git a/app/home/HomeFooter.tsx b/app/home/HomeFooter.tsx deleted file mode 100644 index 381e295c..00000000 --- a/app/home/HomeFooter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import { Footer } from "components/ActionBar/Footer"; -import { Media } from "components/Media"; -import { ThemePopover } from "components/ThemeManager/ThemeSetter"; -import { CreateNewLeafletButton } from "./CreateNewButton"; -import { HelpPopover } from "components/HelpPopover"; -import { AccountSettings } from "./AccountSettings"; -import { useIdentityData } from "components/IdentityProvider"; -import { useReplicache } from "src/replicache"; -import { LoginActionButton } from "components/LoginButton"; - -export const HomeFooter = () => { - let { identity } = useIdentityData(); - let { rootEntity } = useReplicache(); - return ( - -
- - {identity ? : } - - -
-
- ); -}; diff --git a/app/home/HomeLayout.tsx b/app/home/HomeLayout.tsx new file mode 100644 index 00000000..47d34a81 --- /dev/null +++ b/app/home/HomeLayout.tsx @@ -0,0 +1,334 @@ +"use client"; + +import { getHomeDocs, HomeDoc } from "./storage"; +import useSWR from "swr"; +import { + Fact, + PermissionToken, + ReplicacheProvider, + useEntity, +} from "src/replicache"; +import { LeafletListItem } from "./LeafletList/LeafletListItem"; +import { useIdentityData } from "components/IdentityProvider"; +import type { Attribute } from "src/replicache/attributes"; +import { callRPC } from "app/api/rpc/client"; +import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; +import { HomeSmall } from "components/Icons/HomeSmall"; +import { + HomeDashboardControls, + DashboardLayout, + DashboardState, + useDashboardState, +} from "components/PageLayouts/DashboardLayout"; +import { Actions } from "./Actions/Actions"; +import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; +import { Json } from "supabase/database.types"; +import { useTemplateState } from "./Actions/CreateNewButton"; +import { CreateNewLeafletButton } from "./Actions/CreateNewButton"; +import { ActionButton } from "components/ActionBar/ActionButton"; +import { AddTiny } from "components/Icons/AddTiny"; +import { + get_leaflet_data, + GetLeafletDataReturnType, +} from "app/api/rpc/[command]/get_leaflet_data"; +import { useEffect, useRef, useState } from "react"; +import { Input } from "components/Input"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; +import { + ButtonPrimary, + ButtonSecondary, + ButtonTertiary, +} from "components/Buttons"; +import { AddSmall } from "components/Icons/AddSmall"; +import { PublishIllustration } from "app/[leaflet_id]/publish/PublishIllustration/PublishIllustration"; +import { PubListEmptyIllo } from "components/ActionBar/Publications"; +import { theme } from "tailwind.config"; +import Link from "next/link"; +import { DiscoverIllo } from "./HomeEmpty/DiscoverIllo"; +import { WelcomeToLeafletIllo } from "./HomeEmpty/WelcomeToLeafletIllo"; +import { + DiscoverBanner, + HomeEmptyState, + PublicationBanner, +} from "./HomeEmpty/HomeEmpty"; + +type Leaflet = { + added_at: string; + token: PermissionToken & { + leaflets_in_publications?: Exclude< + GetLeafletDataReturnType["result"]["data"], + null + >["leaflets_in_publications"]; + }; +}; + +export const HomeLayout = (props: { + entityID: string; + titles: { [root_entity: string]: string }; + initialFacts: { + [root_entity: string]: Fact[]; + }; +}) => { + let hasBackgroundImage = !!useEntity( + props.entityID, + "theme/background-image", + ); + let cardBorderHidden = !!useCardBorderHidden(props.entityID); + + let [searchValue, setSearchValue] = useState(""); + let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); + + useDebouncedEffect( + () => { + setDebouncedSearchValue(searchValue); + }, + 200, + [searchValue], + ); + + let { identity } = useIdentityData(); + + let hasPubs = !identity || identity.publications.length === 0 ? false : true; + let hasTemplates = + useTemplateState((s) => s.templates).length === 0 ? false : true; + + return ( + } + tabs={{ + home: { + controls: ( + + ), + content: ( + + ), + }, + }} + /> + ); +}; + +export function HomeLeafletList(props: { + titles: { [root_entity: string]: string }; + initialFacts: { + [root_entity: string]: Fact[]; + }; + searchValue: string; + cardBorderHidden: boolean; +}) { + let { identity } = useIdentityData(); + let { data: initialFacts } = useSWR( + "home-leaflet-data", + async () => { + if (identity) { + let { result } = await callRPC("getFactsFromHomeLeaflets", { + tokens: identity.permission_token_on_homepage.map( + (ptrh) => ptrh.permission_tokens.root_entity, + ), + }); + let titles = { + ...result.titles, + ...identity.permission_token_on_homepage.reduce( + (acc, tok) => { + let title = + tok.permission_tokens.leaflets_in_publications[0]?.title; + if (title) acc[tok.permission_tokens.root_entity] = title; + return acc; + }, + {} as { [k: string]: string }, + ), + }; + return { ...result, titles }; + } + }, + { fallbackData: { facts: props.initialFacts, titles: props.titles } }, + ); + + let { data: localLeaflets } = useSWR("leaflets", () => getHomeDocs(), { + fallbackData: [], + }); + let leaflets: Leaflet[] = identity + ? identity.permission_token_on_homepage.map((ptoh) => ({ + added_at: ptoh.created_at, + token: ptoh.permission_tokens as PermissionToken, + })) + : localLeaflets + .sort((a, b) => (a.added_at > b.added_at ? -1 : 1)) + .filter((d) => !d.hidden) + .map((ll) => ll); + + return leaflets.length === 0 ? ( + + ) : ( + <> + +
+ + {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === + 0 && } + +
+ + ); +} + +export function LeafletList(props: { + leaflets: Leaflet[]; + titles: { [root_entity: string]: string }; + defaultDisplay: Exclude; + initialFacts: { + [root_entity: string]: Fact[]; + }; + searchValue: string; + cardBorderHidden: boolean; + showPreview?: boolean; +}) { + let { identity } = useIdentityData(); + let { display } = useDashboardState(); + + display = display || props.defaultDisplay; + + let searchedLeaflets = useSearchedLeaflets( + props.leaflets, + props.titles, + props.searchValue, + ); + + return ( +
+ {props.leaflets.map(({ token: leaflet, added_at }, index) => ( + + + l.doc)} + publishedAt={ + leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents + ?.indexed_at + } + leaflet_id={leaflet.root_entity} + loggedIn={!!identity} + display={display} + added_at={added_at} + cardBorderHidden={props.cardBorderHidden} + index={index} + showPreview={props.showPreview} + isHidden={ + !searchedLeaflets.some( + (sl) => sl.token.root_entity === leaflet.root_entity, + ) + } + /> + + + ))} +
+ ); +} + +function useSearchedLeaflets( + leaflets: Leaflet[], + titles: { [root_entity: string]: string }, + searchValue: string, +) { + let { sort, filter } = useDashboardState(); + + let sortedLeaflets = leaflets.sort((a, b) => { + if (sort === "alphabetical") { + if (titles[a.token.root_entity] === titles[b.token.root_entity]) { + return a.added_at > b.added_at ? -1 : 1; + } else { + return titles[a.token.root_entity].toLocaleLowerCase() > + titles[b.token.root_entity].toLocaleLowerCase() + ? 1 + : -1; + } + } else { + return a.added_at === b.added_at + ? a.token.root_entity > b.token.root_entity + ? -1 + : 1 + : a.added_at > b.added_at + ? -1 + : 1; + } + }); + + let allTemplates = useTemplateState((s) => s.templates); + let filteredLeaflets = sortedLeaflets.filter(({ token: leaflet }) => { + let published = !!leaflet.leaflets_in_publications?.find((l) => l.doc); + let drafts = !!leaflet.leaflets_in_publications?.length && !published; + let docs = !leaflet.leaflets_in_publications?.length; + let templates = !!allTemplates.find((t) => t.id === leaflet.id); + // If no filters are active, show all + if ( + !filter.drafts && + !filter.published && + !filter.docs && + !filter.templates + ) + return true; + + return ( + (filter.drafts && drafts) || + (filter.published && published) || + (filter.docs && docs) || + (filter.templates && templates) + ); + }); + if (searchValue === "") return filteredLeaflets; + let searchedLeaflets = filteredLeaflets.filter(({ token: leaflet }) => { + return titles[leaflet.root_entity] + ?.toLowerCase() + .includes(searchValue.toLowerCase()); + }); + + return searchedLeaflets; +} diff --git a/app/home/LeafletList.tsx b/app/home/LeafletList.tsx deleted file mode 100644 index 597eb11f..00000000 --- a/app/home/LeafletList.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { getHomeDocs, HomeDoc } from "./storage"; -import useSWR from "swr"; -import { Fact, PermissionToken, ReplicacheProvider } from "src/replicache"; -import { LeafletPreview } from "./LeafletPreview"; -import { useIdentityData } from "components/IdentityProvider"; -import type { Attribute } from "src/replicache/attributes"; -import { callRPC } from "app/api/rpc/client"; -import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; - -export function LeafletList(props: { - initialFacts: { - [root_entity: string]: Fact[]; - }; -}) { - let { data: localLeaflets } = useSWR("leaflets", () => getHomeDocs(), { - fallbackData: [], - }); - let { identity } = useIdentityData(); - let { data: initialFacts } = useSWR( - "home-leaflet-data", - async () => { - if (identity) { - let { result } = await callRPC("getFactsFromHomeLeaflets", { - tokens: identity.permission_token_on_homepage.map( - (ptrh) => ptrh.permission_tokens.root_entity, - ), - }); - return result; - } - }, - { fallbackData: props.initialFacts }, - ); - let leaflets: Array< - PermissionToken & { - leaflets_in_publications?: Array<{ - doc: string; - description: string; - publication: string; - leaflet: string; - title: string; - publications: null; - documents: null; - }>; - } - > = identity - ? identity.permission_token_on_homepage - .sort((a, b) => - a.created_at === b.created_at - ? a.permission_tokens.root_entity > b.permission_tokens.root_entity - ? -1 - : 1 - : a.created_at > b.created_at - ? -1 - : 1, - ) - .map((ptoh) => ptoh.permission_tokens) - : localLeaflets - .sort((a, b) => (a.added_at > b.added_at ? -1 : 1)) - .filter((d) => !d.hidden) - .map((ll) => ll.token); - - return ( -
-
- {leaflets.map((leaflet, index) => ( - - - l.doc) - } - leaflet_id={leaflet.root_entity} - loggedIn={!!identity} - /> - - - ))} -
-
- ); -} diff --git a/app/home/LeafletList/LeafletContent.tsx b/app/home/LeafletList/LeafletContent.tsx new file mode 100644 index 00000000..e722f3b9 --- /dev/null +++ b/app/home/LeafletList/LeafletContent.tsx @@ -0,0 +1,68 @@ +"use client"; +import { BlockPreview } from "components/Blocks/PageLinkBlock"; +import { useEffect, useRef, useState } from "react"; +import { useBlocks } from "src/hooks/queries/useBlocks"; +import { useEntity } from "src/replicache"; +import { CanvasContent } from "components/Canvas"; +import styles from "./LeafletPreview.module.css"; +import { PublicationMetadataPreview } from "components/Pages/PublicationMetadata"; + +export const LeafletContent = (props: { + entityID: string; + isOnScreen: boolean; +}) => { + let type = useEntity(props.entityID, "page/type")?.data.value || "doc"; + let blocks = useBlocks(props.entityID); + let previewRef = useRef(null); + + if (type === "canvas") + return ( +
+
+ {props.isOnScreen && ( + + )} +
+
+ ); + + return ( +
+
+ + + {props.isOnScreen && + blocks.slice(0, 10).map((b, index, arr) => { + return ( + + ); + })} +
+
+ ); +}; diff --git a/app/home/LeafletList/LeafletInfo.tsx b/app/home/LeafletList/LeafletInfo.tsx new file mode 100644 index 00000000..eceb24da --- /dev/null +++ b/app/home/LeafletList/LeafletInfo.tsx @@ -0,0 +1,88 @@ +"use client"; +import { PermissionToken } from "src/replicache"; +import { LeafletOptions } from "./LeafletOptions"; +import Link from "next/link"; +import { useState } from "react"; +import { theme } from "tailwind.config"; +import { TemplateSmall } from "components/Icons/TemplateSmall"; +import { timeAgo } from "src/utils/timeAgo"; + +export const LeafletInfo = (props: { + title?: string; + draft?: boolean; + published?: boolean; + token: PermissionToken; + leaflet_id: string; + loggedIn: boolean; + isTemplate: boolean; + className?: string; + display: "grid" | "list"; + added_at: string; + publishedAt?: string; +}) => { + let [prefetch, setPrefetch] = useState(false); + let prettyCreatedAt = props.added_at ? timeAgo(props.added_at) : ""; + + let prettyPublishedAt = props.publishedAt ? timeAgo(props.publishedAt) : ""; + + return ( +
+
+ setPrefetch(true)} + onPointerDown={() => setPrefetch(true)} + prefetch={prefetch} + href={`/${props.token.id}`} + className="no-underline sm:hover:no-underline text-primary grow min-w-0" + > +

+ {props.title} +

+ +
+ {props.isTemplate && props.display === "list" ? ( + + ) : null} + +
+
+ setPrefetch(true)} + onPointerDown={() => setPrefetch(true)} + prefetch={prefetch} + href={`/${props.token.id}`} + className="no-underline sm:hover:no-underline text-primary w-full" + > + {props.draft || props.published ? ( +
+ {props.published + ? `Published ${prettyPublishedAt}` + : `Draft ${prettyCreatedAt}`} +
+ ) : ( +
{prettyCreatedAt}
+ )} + + {props.isTemplate && props.display === "grid" ? ( +
+ +
+ ) : null} +
+ ); +}; diff --git a/app/home/LeafletList/LeafletListItem.tsx b/app/home/LeafletList/LeafletListItem.tsx new file mode 100644 index 00000000..b4b10cd3 --- /dev/null +++ b/app/home/LeafletList/LeafletListItem.tsx @@ -0,0 +1,102 @@ +"use client"; +import { PermissionToken } from "src/replicache"; +import { useTemplateState } from "../Actions/CreateNewButton"; +import { LeafletListPreview, LeafletGridPreview } from "./LeafletPreview"; +import { LeafletInfo } from "./LeafletInfo"; +import { useState, useRef, useEffect } from "react"; + +export const LeafletListItem = (props: { + token: PermissionToken; + leaflet_id: string; + loggedIn: boolean; + display: "list" | "grid"; + cardBorderHidden: boolean; + added_at: string; + title: string; + draft?: boolean; + published?: boolean; + publishedAt?: string; + index: number; + isHidden: boolean; + showPreview?: boolean; +}) => { + let isTemplate = useTemplateState( + (s) => !!s.templates.find((t) => t.id === props.token.id), + ); + + let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); + let previewRef = useRef(null); + + useEffect(() => { + if (!previewRef.current) return; + let observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setIsOnScreen(true); + } else { + setIsOnScreen(false); + } + }); + }, + { threshold: 0.1, root: null }, + ); + observer.observe(previewRef.current); + return () => observer.disconnect(); + }, [previewRef]); + + if (props.display === "list") + return ( + <> +
+ {props.showPreview && ( + + )} + +
+ {props.cardBorderHidden && ( +
+ )} + + ); + return ( +
+
+ +
+ +
+ ); +}; diff --git a/app/home/LeafletOptions.tsx b/app/home/LeafletList/LeafletOptions.tsx similarity index 67% rename from app/home/LeafletOptions.tsx rename to app/home/LeafletList/LeafletOptions.tsx index b73fc72f..e61920d3 100644 --- a/app/home/LeafletOptions.tsx +++ b/app/home/LeafletList/LeafletOptions.tsx @@ -1,29 +1,32 @@ "use client"; import { Menu, MenuItem } from "components/Layout"; -import type { PermissionToken } from "src/replicache"; -import { hideDoc } from "./storage"; +import { useReplicache, type PermissionToken } from "src/replicache"; +import { hideDoc } from "../storage"; import { useState } from "react"; import { ButtonPrimary } from "components/Buttons"; -import { useTemplateState } from "./CreateNewButton"; -import { Item } from "@radix-ui/react-dropdown-menu"; -import { useSmoker } from "components/Toast"; +import { useTemplateState } from "../Actions/CreateNewButton"; +import { useSmoker, useToaster } from "components/Toast"; import { removeLeafletFromHome } from "actions/removeLeafletFromHome"; import { useIdentityData } from "components/IdentityProvider"; import { HideSmall } from "components/Icons/HideSmall"; import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; import { TemplateRemoveSmall } from "components/Icons/TemplateRemoveSmall"; import { TemplateSmall } from "components/Icons/TemplateSmall"; +import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; +import { addLeafletToHome } from "actions/addLeafletToHome"; export const LeafletOptions = (props: { leaflet: PermissionToken; isTemplate: boolean; loggedIn: boolean; + added_at: string; }) => { let { mutate: mutateIdentity } = useIdentityData(); let [state, setState] = useState<"normal" | "template">("normal"); let [open, setOpen] = useState(false); let smoker = useSmoker(); + let toaster = useToaster(); return ( <> - +
{ + e.preventDefault; + e.stopPropagation; + }} + > +
} > @@ -93,6 +102,18 @@ export const LeafletOptions = (props: { } else { hideDoc(props.leaflet); } + toaster({ + content: ( +
+ Doc removed!{" "} + +
+ ), + type: "success", + }); }} > @@ -110,6 +131,49 @@ export const LeafletOptions = (props: { ); }; +const UndoRemoveFromHomeButton = (props: { + leaflet: PermissionToken; + added_at: string | undefined; +}) => { + let toaster = useToaster(); + let { mutate } = useIdentityData(); + return ( + + ); +}; + const AddTemplateForm = (props: { leaflet: PermissionToken; close: () => void; @@ -124,7 +188,7 @@ const AddTemplateForm = (props: { value={name} onChange={(e) => setName(e.target.value)} type="text" - className=" text-primary font-normal border border-border rounded-md outline-none px-2 py-1 w-64" + className=" text-primary font-normal border border-border rounded-md outline-hidden px-2 py-1 w-64" /> diff --git a/app/home/LeafletPreview.module.css b/app/home/LeafletList/LeafletPreview.module.css similarity index 100% rename from app/home/LeafletPreview.module.css rename to app/home/LeafletList/LeafletPreview.module.css diff --git a/app/home/LeafletList/LeafletPreview.tsx b/app/home/LeafletList/LeafletPreview.tsx new file mode 100644 index 00000000..e6ff22c3 --- /dev/null +++ b/app/home/LeafletList/LeafletPreview.tsx @@ -0,0 +1,187 @@ +"use client"; +import { + ThemeBackgroundProvider, + ThemeProvider, +} from "components/ThemeManager/ThemeProvider"; +import { + PermissionToken, + useEntity, + useReferenceToEntity, +} from "src/replicache"; +import { useTemplateState } from "../Actions/CreateNewButton"; +import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; +import { LeafletContent } from "./LeafletContent"; +import { Tooltip } from "components/Tooltip"; +import { useState } from "react"; +import Link from "next/link"; +import { SpeedyLink } from "components/SpeedyLink"; + +export const LeafletListPreview = (props: { + draft?: boolean; + published?: boolean; + isVisible: boolean; + token: PermissionToken; + leaflet_id: string; + loggedIn: boolean; +}) => { + let root = + useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || + props.leaflet_id; + let firstPage = useEntity(root, "root/page")[0]; + let page = firstPage?.data.value || root; + + let cardBorderHidden = useCardBorderHidden(root); + let rootBackgroundImage = useEntity(root, "theme/card-background-image"); + let rootBackgroundRepeat = useEntity( + root, + "theme/card-background-image-repeat", + ); + let rootBackgroundOpacity = useEntity( + root, + "theme/card-background-image-opacity", + ); + + return ( + +
+ + +
+
+
+ + +
+
+ } + className="p-1!" + > + + +
+
+ +
+
+
+
+
+ ); +}; + +export const LeafletGridPreview = (props: { + draft?: boolean; + published?: boolean; + token: PermissionToken; + leaflet_id: string; + loggedIn: boolean; + isVisible: boolean; +}) => { + let root = + useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || + props.leaflet_id; + let firstPage = useEntity(root, "root/page")[0]; + let page = firstPage?.data.value || root; + + let cardBorderHidden = useCardBorderHidden(root); + let rootBackgroundImage = useEntity(root, "theme/card-background-image"); + let rootBackgroundRepeat = useEntity( + root, + "theme/card-background-image-repeat", + ); + let rootBackgroundOpacity = useEntity( + root, + "theme/card-background-image-opacity", + ); + return ( + +
+
+ +
+
+ +
+
+
+
+ +
+
+ ); +}; + +const LeafletPreviewLink = (props: { id: string }) => { + return ( + + ); +}; diff --git a/app/home/LeafletPreview.tsx b/app/home/LeafletPreview.tsx deleted file mode 100644 index f1eba178..00000000 --- a/app/home/LeafletPreview.tsx +++ /dev/null @@ -1,255 +0,0 @@ -"use client"; -import { BlockPreview, PagePreview } from "components/Blocks/PageLinkBlock"; -import { - ThemeBackgroundProvider, - ThemeProvider, -} from "components/ThemeManager/ThemeProvider"; -import { useEffect, useRef, useState } from "react"; -import { useBlocks } from "src/hooks/queries/useBlocks"; -import { - PermissionToken, - useEntity, - useReferenceToEntity, - useReplicache, -} from "src/replicache"; -import { deleteLeaflet } from "actions/deleteLeaflet"; -import { removeDocFromHome } from "./storage"; -import { mutate } from "swr"; -import { ButtonPrimary } from "components/Buttons"; -import { LeafletOptions } from "./LeafletOptions"; -import { CanvasContent } from "components/Canvas"; -import { theme } from "tailwind.config"; -import { useTemplateState } from "./CreateNewButton"; -import styles from "./LeafletPreview.module.css"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { TemplateSmall } from "components/Icons/TemplateSmall"; -import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; -import { - PublicationMetadata, - PublicationMetadataPreview, -} from "components/Pages/PublicationMetadata"; -import { SpeedyLink } from "components/SpeedyLink"; - -export const LeafletPreview = (props: { - draft?: boolean; - published?: boolean; - index: number; - token: PermissionToken; - leaflet_id: string; - loggedIn: boolean; -}) => { - let [state, setState] = useState<"normal" | "deleting">("normal"); - let isTemplate = useTemplateState( - (s) => !!s.templates.find((t) => t.id === props.token.id), - ); - let root = - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || - props.leaflet_id; - let firstPage = useEntity(root, "root/page")[0]; - let page = firstPage?.data.value || root; - - let cardBorderHidden = useCardBorderHidden(root); - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); - let rootBackgroundRepeat = useEntity( - root, - "theme/card-background-image-repeat", - ); - let rootBackgroundOpacity = useEntity( - root, - "theme/card-background-image-opacity", - ); - - return ( -
- -
- {state === "normal" ? ( -
- -
-
- -
-
-
- -
- ) : ( - - )} -
-
- {props.draft || props.published ? ( -
- {props.published ? "Published!" : "Draft"} -
- ) : ( -
- )} - -
- - -
- ); -}; - -const LeafletContent = (props: { entityID: string; index: number }) => { - let type = useEntity(props.entityID, "page/type")?.data.value || "doc"; - let blocks = useBlocks(props.entityID); - let previewRef = useRef(null); - let [isVisible, setIsVisible] = useState(props.index > 16 ? false : true); - useEffect(() => { - if (!previewRef.current) return; - let observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setIsVisible(true); - } else { - setIsVisible(false); - } - }); - }, - { threshold: 0.1, root: null }, - ); - observer.observe(previewRef.current); - return () => observer.disconnect(); - }, [previewRef]); - - if (type === "canvas") - return ( -
-
- {isVisible && } -
-
- ); - - return ( -
-
- - - {isVisible && - blocks.slice(0, 10).map((b, index, arr) => { - return ( - - ); - })} -
-
- ); -}; - -const LeafletAreYouSure = (props: { - token: PermissionToken; - setState: (s: "normal" | "deleting") => void; -}) => { - return ( -
-
- Permanently delete this Leaflet? -
-
- { - e.stopPropagation(); - e.preventDefault(); - deleteLeaflet(props.token); - removeDocFromHome(props.token); - mutate("leaflets"); - }} - > - Delete - - -
-
- ); -}; - -const LeafletTemplateIndicator = (props: { isTemplate: boolean }) => { - if (!props.isTemplate) return; - - return ( -
- -
- ); -}; diff --git a/app/home/page.tsx b/app/home/page.tsx index 45ffd500..bcf27599 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -1,5 +1,5 @@ import { cookies } from "next/headers"; -import { Fact, ReplicacheProvider } from "src/replicache"; +import { Fact, ReplicacheProvider, useEntity } from "src/replicache"; import type { Attribute } from "src/replicache/attributes"; import { ThemeBackgroundProvider, @@ -9,16 +9,15 @@ import { EntitySetProvider } from "components/EntitySetProvider"; import { createIdentity } from "actions/createIdentity"; import { drizzle } from "drizzle-orm/node-postgres"; import { IdentitySetter } from "./IdentitySetter"; -import { LeafletList } from "./LeafletList"; + import { getIdentityData } from "actions/getIdentityData"; import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; -import { HomeSidebar } from "./HomeSidebar"; -import { HomeFooter } from "./HomeFooter"; -import { Media } from "components/Media"; -import { MyPublicationList } from "./Publications"; import { supabaseServerClient } from "supabase/serverClient"; import { pool } from "supabase/pool"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; +import { HomeLayout } from "./HomeLayout"; + export default async function Home() { let cookieStore = await cookies(); let auth_res = await getIdentityData(); @@ -59,13 +58,13 @@ export default async function Home() { if (!permission_token) return ( -
-

Sorry, home page not found!

+ +

Sorry, we can't find this home!

This may be a glitch on our end. If the issue persists please{" "} send us a note.

-
+ ); let [homeLeafletFacts, allLeafletFacts] = await Promise.all([ supabaseServerClient.rpc("get_facts", { @@ -87,6 +86,7 @@ export default async function Home() { let root_entity = permission_token.root_entity; let home_docs_initialFacts = allLeafletFacts?.result || {}; + return ( -
- -
- -
- -
- -
-
- -
- -
-
-
+ + { + let title = + tok.permission_tokens.leaflets_in_publications[0]?.title; + if (title) acc[tok.permission_tokens.root_entity] = title; + return acc; + }, + {} as { [k: string]: string }, + ), + }} + entityID={root_entity} + initialFacts={home_docs_initialFacts.facts || {}} + /> +
diff --git a/app/legal/content.tsx b/app/legal/content.tsx index 580062a6..58b7c5d7 100644 --- a/app/legal/content.tsx +++ b/app/legal/content.tsx @@ -36,7 +36,7 @@ export const LegalContent = () => {
{state === "terms" ? : }
diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 718d9815..68f62409 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -43,7 +43,7 @@ export const SubscribeButton = (props: {
{ if (identity?.email) { await subscribeToPublicationWithEmail(props.publication); @@ -151,7 +151,7 @@ const ConfirmCodeInput = (props: { { @@ -160,7 +160,7 @@ const ConfirmCodeInput = (props: { /> { console.log( await confirmEmailAuthToken(props.token, props.codeInputValue), @@ -356,14 +356,14 @@ const SubscribeSuccessModal = ({ - +
         
       
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx index 4e28e2a8..89208240 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx @@ -13,10 +13,10 @@ import { ButtonPrimary } from "components/Buttons"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { Popover } from "components/Popover"; import { AppBskyActorProfile, AtUri } from "@atproto/api"; -import { timeAgo } from "app/discover/PubListing"; import { BlueskyLogin } from "app/login/LoginForm"; import { usePathname } from "next/navigation"; import { QuoteContent } from "../Quotes"; +import { timeAgo } from "src/utils/timeAgo"; export type Comment = { record: Json; @@ -44,7 +44,9 @@ export function Comments(props: { document_uri: string; comments: Comment[] }) { Comments diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx index 94198309..f45627f5 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx @@ -21,10 +21,10 @@ export const InteractionDrawer = (props: { return ( <>
-
+
{currentDrawer === "quotes" ? ( diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx index 802ec3a3..8b3ae0ba 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx @@ -129,7 +129,7 @@ export const QuoteContent = (props: { blocks={content} did={props.did} preview - className="!py-0" + className="py-0!" />
diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index 51da42a0..a9cd3fd4 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -109,7 +109,7 @@ let Block = ({ postBlockWrapper min-h-7 pt-1 pb-2 - ${isList && "isListItem !pb-0 "} + ${isList && "isListItem pb-0! "} ${alignment} `; @@ -137,7 +137,7 @@ let Block = ({ } case PubLeafletBlocksUnorderedList.isMain(b.block): { return ( -
    +
      {b.block.children.map((child, i) => (
      {b.block.description}
      @@ -217,7 +217,7 @@ let Block = ({ alt={b.block.alt} height={b.block.aspectRatio?.height} width={b.block.aspectRatio?.width} - className={`!pt-3 sm:!pt-4 rounded-md ${className}`} + className={`pt-3! sm:pt-4! rounded-md ${className}`} src={blobRefToSrc(b.block.image.ref, did)} /> {b.block.alt && ( @@ -240,7 +240,7 @@ let Block = ({ return ( // highly unfortunate hack so that the border-l on blockquote is the height of just the text rather than the height of the block, which includes padding.
      +
    • Edit Post diff --git a/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx b/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx index 7b78aa2a..d923ae67 100644 --- a/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx +++ b/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx @@ -152,7 +152,7 @@ export const QuoteOptionButtons = (props: { position: string }) => {
      Share via
      +
    • diff --git a/app/lish/[did]/[publication]/[rkey]/page.tsx b/app/lish/[did]/[publication]/[rkey]/page.tsx index 377b97a2..7162364b 100644 --- a/app/lish/[did]/[publication]/[rkey]/page.tsx +++ b/app/lish/[did]/[publication]/[rkey]/page.tsx @@ -20,6 +20,7 @@ import { PostPageContextProvider } from "./PostPageContext"; import { PostPage } from "./PostPage"; import { PageLayout } from "./PageLayout"; import { extractCodeBlocks } from "./extractCodeBlocks"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; export async function generateMetadata(props: { params: Promise<{ publication: string; did: string; rkey: string }>; @@ -51,13 +52,13 @@ export default async function Post(props: { let did = decodeURIComponent((await props.params).did); if (!did) return ( - + ); let agent = new AtpAgent({ service: "https://public.api.bsky.app", @@ -80,17 +81,13 @@ export default async function Post(props: { ]); if (!document?.data || !document.documents_in_publications[0].publications) return ( -
      -
      -
      -

      Sorry, post not found!

      -

      - This may be a glitch on our end. If the issue persists please{" "} - send us a note. -

      -
      -
      -
      + +

      Sorry, we can't find this post!

      +

      + This may be a glitch on our end. If the issue persists please{" "} + send us a note. +

      +
      ); let record = document.data as PubLeafletDocument.Record; let bskyPosts = record.pages.flatMap((p) => { diff --git a/app/lish/[did]/[publication]/dashboard/Actions.tsx b/app/lish/[did]/[publication]/dashboard/Actions.tsx index 61f3f856..96e4452c 100644 --- a/app/lish/[did]/[publication]/dashboard/Actions.tsx +++ b/app/lish/[did]/[publication]/dashboard/Actions.tsx @@ -22,29 +22,10 @@ import { SpeedyLink } from "components/SpeedyLink"; export const Actions = (props: { publication: string }) => { return ( <> - - - } label="Go Home" /> - - -
      - - - } label="Go Home" /> - - ); }; @@ -64,7 +45,6 @@ function PublicationShareButton() { - secondary label="Share" onClick={() => {}} /> @@ -72,7 +52,7 @@ function PublicationShareButton() { > {}}>
      Viewer Mode
      @@ -85,7 +65,7 @@ function PublicationShareButton() { onSelect={(e) => { e.preventDefault(); let rect = (e.currentTarget as Element)?.getBoundingClientRect(); - navigator.clipboard.writeText(getPublicationURL(pub!)); + navigator.clipboard.writeText(getPublicationURL(pub?.publication!)); smoker({ position: { x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, @@ -133,7 +113,7 @@ function PublicationThemeButton() { return ( - - {pub_data.leaflets_in_publications - .filter((d) => !d.doc) - .map((d) => { - return ( - - -
      -
      - ); - })} -
      - ); -} +
      + -function Draft(props: { id: string; title: string; description: string }) { - let [open, setOpen] = useState(false); - return ( -
      - - {props.title ? ( -

      {props.title}

      - ) : ( -

      Untitled

      - )} -
      {props.description}
      -
      - setOpen(o)} - align="end" - asChild - trigger={ - - } - > - <> - - - + !l.documents) + .map((l) => { + return { + token: { + ...l.permission_tokens!, + leaflets_in_publications: [ + { + ...l, + publications: { + ...publication, + }, + }, + ], + }, + added_at: "", + }; + })} + initialFacts={pub_data.leaflet_data.facts || {}} + titles={{ + ...leaflets_in_publications.reduce( + (acc, leaflet) => { + if (leaflet.title && leaflet.permission_tokens) + acc[leaflet.permission_tokens.root_entity] = leaflet.title; + return acc; + }, + {} as { [l: string]: string }, + ), + }} + /> +
      ); } - -export function DeleteDraft(props: { id: string }) { - let { mutate } = usePublicationData(); - let [state, setState] = useState<"normal" | "confirm">("normal"); - - if (state === "normal") { - return ( - { - if (state === "normal") { - e.preventDefault(); - return setState("confirm"); - } - }} - > - Delete Draft - - - ); - } - if (state === "confirm") { - return ( -
      - Are you sure? -
      - This action cannot be undone! -
      - { - await mutate((data) => { - if (!data) return data; - return { - ...data, - leaflets_in_publications: data.leaflets_in_publications.filter( - (d) => d.leaflet !== props.id, - ), - }; - }, false); - await deleteDraft(props.id); - }} - > - Delete - -
      - ); - } -} diff --git a/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx b/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx index 30421a87..aedf8c79 100644 --- a/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx +++ b/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx @@ -17,7 +17,7 @@ export function NewDraftActionButton(props: { publication: string }) { router.push(`/${newLeaflet}`); }} icon= - label="New Draft" + label="New" /> ); } diff --git a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx index c820c89c..4a8c3bf1 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx @@ -1,81 +1,84 @@ "use client"; -import { BlobRef } from "@atproto/lexicon"; -import { useState } from "react"; -import { useIsMobile } from "src/hooks/isMobile"; -import { theme } from "tailwind.config"; -import { usePublicationData } from "./PublicationSWRProvider"; + +import { DraftList } from "./DraftList"; +import { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; +import { Actions } from "./Actions"; +import React, { useState } from "react"; +import { PublishedPostsList } from "./PublishedPostsLists"; import { PubLeafletPublication } from "lexicons/api"; +import { PublicationSubscribers } from "./PublicationSubscribers"; +import { AtUri } from "@atproto/syntax"; +import { + HomeDashboardControls, + DashboardLayout, + PublicationDashboardControls, +} from "components/PageLayouts/DashboardLayout"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; -type Tabs = { [tabName: string]: React.ReactNode }; -export function PublicationDashboard(props: { - name: string; - tabs: T; - defaultTab: keyof T; - icon: BlobRef | null; - did: string; +export default function PublicationDashboard({ + publication, + record, +}: { + record: PubLeafletPublication.Record; + publication: Exclude< + GetPublicationDataReturnType["result"]["publication"], + null + >; }) { - let { data: pub } = usePublicationData(); - let showPageBackground = !!(pub?.record as PubLeafletPublication.Record) - ?.theme?.showPageBackground; - let [tab, setTab] = useState(props.defaultTab); - let content = props.tabs[tab]; + let [searchValue, setSearchValue] = useState(""); + let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); - return ( - <> -
      -
      -
      - {props.icon && ( -
      - )} -
      - {props.name} -
      -
      -
      -
      - {Object.keys(props.tabs).map((t) => ( - setTab(t)} - showPageBackground={showPageBackground} - /> - ))} -
      -
      -
      - {content} -
      - + useDebouncedEffect( + () => { + setDebouncedSearchValue(searchValue); + }, + 200, + [searchValue], ); -} -function Tab(props: { - name: string; - selected: boolean; - showPageBackground: boolean; - onSelect: () => void; -}) { return ( -
      props.onSelect()} - > - {props.name} -
      + + ), + controls: ( + + ), + }, + Published: { + content: ( + + ), + controls: null, + }, + Subscribers: { + content: ( + + ), + controls: null, + }, + }} + actions={} + currentPage="pub" + publication={publication.uri} + /> ); } diff --git a/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx b/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx index af25aa78..17a0b1f0 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx @@ -12,7 +12,7 @@ export function PublicationSWRDataProvider(props: { publication_data: GetPublicationDataReturnType["result"]; children: React.ReactNode; }) { - let key = `publication-data-${props.publication_did}`; + let key = `publication-data-${props.publication_did}-${props.publication_rkey}`; return ( diff --git a/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx b/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx index 6c239856..024e67e1 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationSubscribers.tsx @@ -1,19 +1,60 @@ "use client"; import { AppBskyActorProfile } from "lexicons/api"; import { usePublicationData } from "./PublicationSWRProvider"; -import { blobRefToSrc } from "src/utils/blobRefToSrc"; import { ButtonPrimary } from "components/Buttons"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; import { useSmoker } from "components/Toast"; +import { Menu, MenuItem, Separator } from "components/Layout"; +import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; +import { Checkbox } from "components/Checkbox"; +import { useEffect, useState } from "react"; -export function PublicationSubscribers() { - let { data: publication } = usePublicationData(); +type subscriber = { email: string | undefined; did: string | undefined }; + +// HELLO! THIS FILE HAS CHECKBOXES AND A HEADER COMMENTED OUT +// the checkboxes would let you select users and and the eader provided a count and let you select all and do actions +// I also removed some props from SubscriberListItem around emails since we dont have them yet. +// WHen we get emails in, we can uncomment some of this stuff + +export function PublicationSubscribers(props: { + showPageBackground?: boolean; +}) { let smoker = useSmoker(); + let { data: publication } = usePublicationData(); + // let [checkedSubscribers, setCheckedSubscribers] = useState([]); + // let [checkAll, setCheckAll] = useState(false); if (!publication) return
      null
      ; - if (publication.publication_subscriptions.length === 0) + let subscribers = publication.publication?.publication_subscriptions || []; + + // useEffect(() => { + // const allSubscribersSelected = subscribers.every((subscriber) => + // checkedSubscribers.some( + // (checked) => + // checked.email === "dummyemail@email.com" && + // checked.did === subscriber.identities?.bsky_profiles?.did, + // ), + // ); + + // if (allSubscribersSelected && subscribers.length > 0) { + // setCheckAll(true); + // } else { + // setCheckAll(false); + // } + // }, [checkedSubscribers]); + + if (subscribers.length === 0) return ( -
      +

      No subscribers yet

      Start sharing your publication!

      { e.preventDefault(); let rect = (e.currentTarget as Element)?.getBoundingClientRect(); - navigator.clipboard.writeText(getPublicationURL(publication!)); + navigator.clipboard.writeText( + getPublicationURL(publication.publication!), + ); smoker({ position: { x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, @@ -37,53 +80,158 @@ export function PublicationSubscribers() { ); return ( -
      -

      {publication.publication_subscriptions.length} Subscribers

      -
      - {publication.publication_subscriptions +
      + {/*
      + { + if (checkAll === false) { + const allSubscribers = subscribers.map((subscriber) => ({ + email: "dummyemail@email.com", + did: subscriber.identities?.bsky_profiles?.did, + })); + setCheckedSubscribers(allSubscribers); + } else { + setCheckedSubscribers([]); + } + }} + className="!font-bold text-secondary mb-1" + > + {subscribers.length} Subscriber{subscribers.length !== 1 && "s"} + + {checkedSubscribers.length !== 0 && ( + + )} +
      */} +
      + {subscribers.length} Subscriber{subscribers.length !== 1 && "s"} +
      +
      +
      + {subscribers .sort((a, b) => { return b.created_at.localeCompare(a.created_at); }) .map((subscriber, index) => { if (!subscriber.identities?.bsky_profiles) return null; let handle = subscriber.identities?.bsky_profiles.handle; + let did = subscriber.identities?.bsky_profiles.did; let profile = subscriber.identities?.bsky_profiles ?.record as AppBskyActorProfile.Record | null; if (!profile) return null; return ( -
      - -
      - {profile.avatar && ( - {profile.displayName} - )} -
      -

      {profile.displayName}

      -

      @{handle}

      -
      -
      -
      - {index !== publication.publication_subscriptions.length - 1 && ( -
      - )} -
      + ); })}
      ); } + +const SubscriberListItem = (props: { + handle: string | undefined; + did: string | undefined; + createdAt: string; +}) => { + return ( + // { + // if (props.checked === false) { + // const newCheckedSubscribers = [ + // ...props.checkedSubscribers, + // { + // email: props.email, + // did: props.did, + // }, + // ]; + // props.setCheckedSubscribers(newCheckedSubscribers); + // } else { + // const newCheckedSubscribers = props.checkedSubscribers.filter( + // (subscriber) => + // !( + // subscriber.email === props.email && subscriber.did === props.did + // ), + // ); + // props.setCheckedSubscribers(newCheckedSubscribers); + // } + // console.log(props.checkedSubscribers); + // }} + // > + <> +
      + {/* + {props.email} + + + {props.handle && props.email && ( + + )}*/} + {props.handle && ( + + @{props.handle} + + )} +
      + {new Date(props.createdAt).toLocaleString(undefined, { + year: "2-digit", + month: "2-digit", + day: "2-digit", + })} +
      +
      + + //
      + ); +}; + +const SubscriberOptions = (props: { + checkedSubscribers: subscriber[]; + allSelected: boolean; +}) => { + return ( + + {props.allSelected ? "All" : props.checkedSubscribers.length} Selected{" "} + + + } + > + {}}> + Export {props.allSelected ? "All" : "Selected"} + + {}}> + Remove {props.allSelected ? "All" : "Selected"} + + + ); +}; diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index 581c8f11..f7ea5be0 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -18,9 +18,13 @@ import { SpeedyLink } from "components/SpeedyLink"; import { QuoteTiny } from "components/Icons/QuoteTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; -export function PublishedPostsList() { - let { data: publication } = usePublicationData(); +export function PublishedPostsList(props: { + searchValue: string; + showPageBackground: boolean; +}) { + let { data } = usePublicationData(); let params = useParams(); + let { publication } = data!; if (!publication) return null; if (publication.documents_in_publications.length === 0) return ( @@ -29,7 +33,7 @@ export function PublishedPostsList() {
      ); return ( -
      +
      {publication.documents_in_publications .sort((a, b) => { let aRecord = a.documents?.data! as PubLeafletDocument.Record; @@ -49,18 +53,23 @@ export function PublishedPostsList() { ); let uri = new AtUri(doc.documents.uri); let record = doc.documents.data as PubLeafletDocument.Record; - let quotes = - doc.documents.document_mentions_in_bsky[0]?.count || 0; - let comments = - doc.documents.comments_on_documents[0]?.count || 0; + let quotes = doc.documents.document_mentions_in_bsky[0]?.count || 0; + let comments = doc.documents.comments_on_documents[0]?.count || 0; return (
      -
      + -
      + {!props.showPageBackground && ( +
      + )} ); })} @@ -136,7 +149,7 @@ let Options = (props: { document_uri: string }) => { alignOffset={20} asChild trigger={ - } @@ -149,11 +162,11 @@ let Options = (props: { document_uri: string }) => { }; function OptionsMenu(props: { document_uri: string }) { - let { mutate, data: publication } = usePublicationData(); + let { mutate, data } = usePublicationData(); let [state, setState] = useState<"normal" | "confirm">("normal"); - let postLink = publication - ? `${getPublicationURL(publication)}/${new AtUri(props.document_uri).rkey}` + let postLink = data?.publication + ? `${getPublicationURL(data?.publication)}/${new AtUri(props.document_uri).rkey}` : null; if (state === "normal") { @@ -203,13 +216,17 @@ function OptionsMenu(props: { document_uri: string }) { if (!data) return data; return { ...data, - leaflets_in_publications: data.leaflets_in_publications.filter( - (l) => l.doc !== props.document_uri, - ), - documents_in_publications: - data.documents_in_publications.filter( - (d) => d.documents?.uri !== props.document_uri, - ), + publication: { + ...data.publication!, + leaflets_in_publications: + data.publication?.leaflets_in_publications.filter( + (l) => l.doc !== props.document_uri, + ) || [], + documents_in_publications: + data.publication?.documents_in_publications.filter( + (d) => d.documents?.uri !== props.document_uri, + ) || [], + }, }; }, false); await deletePost(props.document_uri); diff --git a/app/lish/[did]/[publication]/dashboard/page.tsx b/app/lish/[did]/[publication]/dashboard/page.tsx index 95f46896..c35bede4 100644 --- a/app/lish/[did]/[publication]/dashboard/page.tsx +++ b/app/lish/[did]/[publication]/dashboard/page.tsx @@ -1,26 +1,14 @@ import { supabaseServerClient } from "supabase/serverClient"; import { Metadata } from "next"; - -import { Sidebar } from "components/ActionBar/Sidebar"; - -import { Media } from "components/Media"; -import { Footer } from "components/ActionBar/Footer"; -import { PublicationDashboard } from "./PublicationDashboard"; -import { DraftList } from "./DraftList"; import { getIdentityData } from "actions/getIdentityData"; -import { Actions } from "./Actions"; -import React from "react"; import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; import { PublicationSWRDataProvider } from "./PublicationSWRProvider"; -import { PublishedPostsList } from "./PublishedPostsLists"; -import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; -import { PublicationSubscribers } from "./PublicationSubscribers"; -import { - PublicationThemeProvider, - PublicationThemeProviderDashboard, -} from "components/ThemeManager/PublicationThemeProvider"; -import { blobRefToSrc } from "src/utils/blobRefToSrc"; +import { PubLeafletPublication } from "lexicons/api"; +import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; import { AtUri } from "@atproto/syntax"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; +import PublicationDashboard from "./PublicationDashboard"; +import Link from "next/link"; export async function generateMetadata(props: { params: Promise<{ publication: string; did: string }>; @@ -28,13 +16,14 @@ export async function generateMetadata(props: { let did = decodeURIComponent((await props.params).did); if (!did) return { title: "Publication 404" }; - let { result: publication } = await get_publication_data.handler( + let { result: publication_data } = await get_publication_data.handler( { did, publication_name: decodeURIComponent((await props.params).publication), }, { supabase: supabaseServerClient }, ); + let { publication } = publication_data; let record = (publication?.record as PubLeafletPublication.Record) || undefined; if (!publication) return { title: "404 Publication" }; @@ -49,71 +38,39 @@ export default async function Publication(props: { let identity = await getIdentityData(); if (!identity || !identity.atp_did) return ( -
      + ); let did = decodeURIComponent(params.did); if (!did) return ; - let { result: publication } = await get_publication_data.handler( + let { result: publication_data } = await get_publication_data.handler( { did, publication_name: decodeURIComponent((await props.params).publication), }, { supabase: supabaseServerClient }, ); + let { publication, leaflet_data } = publication_data; + let record = publication?.record as PubLeafletPublication.Record | null; - if (!publication || identity.atp_did !== publication.identity_did) + if (!publication || identity.atp_did !== publication.identity_did || !record) return ; - let record = publication?.record as PubLeafletPublication.Record | null; let uri = new AtUri(publication.uri); - let showPageBackground = !!record?.theme?.showPageBackground; - try { return ( -
      -
      -
      -
      - - - -
      -
      -
      - , - Published: , - Subscribers: , - }} - defaultTab={"Drafts"} - /> -
      -
      - -
      -
      -
      +
      ); @@ -125,12 +82,12 @@ export default async function Publication(props: { const PubNotFound = () => { return ( -
      -

      Sorry, publication not found!

      + +

      Sorry, we can't find this publication!

      This may be a glitch on our end. If the issue persists please{" "} send us a note.

      -
      + ); }; diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index e9a27ae5..42096c05 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -7,7 +7,6 @@ import { } from "lexicons/api"; import { createElement } from "react"; import { StaticPostContent } from "./[rkey]/StaticPostContent"; -import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; import { supabaseServerClient } from "supabase/serverClient"; import { NextResponse } from "next/server"; @@ -18,13 +17,25 @@ export async function generateFeed( let renderToReadableStream = await import("react-dom/server").then( (module) => module.renderToReadableStream, ); - let { result: publication } = await get_publication_data.handler( - { - did: did, - publication_name: publication_name, - }, - { supabase: supabaseServerClient }, - ); + let uri; + if (/^(?!\.$|\.\.S)[A-Za-z0-9._:~-]{1,512}$/.test(publication_name)) { + uri = AtUri.make( + did, + "pub.leaflet.publication", + publication_name, + ).toString(); + } + let { data: publication } = await supabaseServerClient + .from("publications") + .select( + `*, + publication_subscriptions(*), + documents_in_publications(documents(*)) + `, + ) + .eq("identity_did", did) + .or(`name.eq."${publication_name}", uri.eq."${uri}"`) + .single(); let pubRecord = publication?.record as PubLeafletPublication.Record; if (!publication || !pubRecord) diff --git a/app/lish/[did]/[publication]/layout.tsx b/app/lish/[did]/[publication]/layout.tsx index faefd07c..3720b79f 100644 --- a/app/lish/[did]/[publication]/layout.tsx +++ b/app/lish/[did]/[publication]/layout.tsx @@ -1,7 +1,7 @@ import { PubLeafletPublication } from "lexicons/api"; -import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; import { supabaseServerClient } from "supabase/serverClient"; import { Metadata } from "next"; +import { AtUri } from "@atproto/syntax"; export default async function PublicationLayout(props: { children: React.ReactNode; @@ -16,14 +16,29 @@ export async function generateMetadata(props: { }>; }): Promise { let params = await props.params; + let did = decodeURIComponent(params.did); if (!params.did || !params.publication) return { title: "Publication 404" }; - let { result: publication } = await get_publication_data.handler( - { - did: decodeURIComponent(params.did), - publication_name: decodeURIComponent(params.publication), - }, - { supabase: supabaseServerClient }, - ); + + let uri; + let publication_name = decodeURIComponent(params.publication); + if (/^(?!\.$|\.\.S)[A-Za-z0-9._:~-]{1,512}$/.test(publication_name)) { + uri = AtUri.make( + did, + "pub.leaflet.publication", + publication_name, + ).toString(); + } + let { data: publication } = await supabaseServerClient + .from("publications") + .select( + `*, + publication_subscriptions(*), + documents_in_publications(documents(*)) + `, + ) + .eq("identity_did", did) + .or(`name.eq."${publication_name}", uri.eq."${uri}"`) + .single(); if (!publication) return { title: "Publication 404" }; let pubRecord = publication?.record as PubLeafletPublication.Record; diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx index 20a37ba8..f1a34c74 100644 --- a/app/lish/[did]/[publication]/page.tsx +++ b/app/lish/[did]/[publication]/page.tsx @@ -10,6 +10,7 @@ import { PublicationBackgroundProvider, PublicationThemeProvider, } from "components/ThemeManager/PublicationThemeProvider"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; import { SpeedyLink } from "components/SpeedyLink"; import { QuoteTiny } from "components/Icons/QuoteTiny"; import { CommentTiny } from "components/Icons/CommentTiny"; @@ -68,7 +69,7 @@ export default async function Publication(props: { className={`pubWrapper flex flex-col sm:py-6 h-full ${showPageBackground ? "max-w-prose mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} >
      {record?.icon && ( @@ -141,7 +142,7 @@ export default async function Publication(props: {

      {doc_record.title}

      @@ -198,12 +199,12 @@ export default async function Publication(props: { const PubNotFound = () => { return ( -

      -

      Sorry, publication not found!

      + +

      Sorry, we can't find this publication!

      This may be a glitch on our end. If the issue persists please{" "} send us a note.

      -
      + ); }; diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx index f69c8d8b..cf39401f 100644 --- a/app/lish/createPub/CreatePubForm.tsx +++ b/app/lish/createPub/CreatePubForm.tsx @@ -207,14 +207,14 @@ function DomainInput(props: { return (
      -