From 9da45ff6a3b369f7f4fb422902653992dbd3a2b8 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 1 May 2026 01:04:23 -0400 Subject: [PATCH] use route based navigation --- app/(home-pages)/(writer)/WriterShell.tsx | 47 +++++ .../{ => (writer)}/home/Actions/Actions.tsx | 0 .../home/Actions/CreateNewButton.tsx | 0 .../home/Actions/HomeThemeSetter.tsx | 0 .../home/HomeEmpty/DiscoverIllo.tsx | 0 .../home/HomeEmpty/HomeEmpty.tsx | 0 .../home/HomeEmpty/WelcomeToLeafletIllo.tsx | 0 .../{ => (writer)}/home/HomeLayout.tsx | 67 +++--- .../{ => (writer)}/home/IdentitySetter.tsx | 0 .../home/LeafletList/LeafletContent.tsx | 0 .../home/LeafletList/LeafletInfo.tsx | 0 .../home/LeafletList/LeafletListItem.tsx | 0 .../home/LeafletList/LeafletOptions.tsx | 0 .../LeafletList/LeafletPreview.module.css | 0 .../home/LeafletList/LeafletPreview.tsx | 0 app/(home-pages)/{ => (writer)}/home/icon.tsx | 0 app/(home-pages)/{ => (writer)}/home/page.tsx | 4 +- .../{ => (writer)}/home/storage.ts | 0 app/(home-pages)/(writer)/layout.tsx | 5 + .../looseleafs/LooseleafsLayout.tsx | 45 ++-- .../{ => (writer)}/looseleafs/page.tsx | 4 +- .../BskyPostEmbedNotification.tsx | 0 .../CommentMentionNotification.tsx | 0 .../notifications/CommentNotication.tsx | 0 .../notifications/FollowNotification.tsx | 0 .../notifications/MentionNotification.tsx | 0 .../notifications/Notification.tsx | 0 .../notifications/NotificationList.tsx | 0 .../notifications/QuoteNotification.tsx | 0 .../notifications/RecommendNotification.tsx | 0 .../notifications/ReplyNotification.tsx | 0 .../notifications/getNotifications.ts | 0 .../(writer)/notifications/page.tsx | 29 +++ app/(home-pages)/notifications/page.tsx | 51 ----- app/(home-pages)/p/[didOrHandle]/layout.tsx | 45 ++-- app/(home-pages)/page.tsx | 31 +-- app/(home-pages)/reader/InboxContent.tsx | 2 +- app/(home-pages)/reader/ReaderLayout.tsx | 47 ----- app/(home-pages)/reader/hot/page.tsx | 8 +- app/(home-pages)/reader/layout.tsx | 23 +++ app/(home-pages)/reader/loading.tsx | 4 - app/(home-pages)/reader/new/page.tsx | 18 +- app/(home-pages)/reader/page.tsx | 32 ++- app/(home-pages)/reader/trending/page.tsx | 19 ++ app/(home-pages)/tag/[tag]/layout.tsx | 24 +++ app/(home-pages)/tag/[tag]/page.tsx | 30 +-- app/[leaflet_id]/publish/PublishPost.tsx | 2 +- .../dashboard/DashboardChrome.tsx | 66 ++++++ .../[publication]/dashboard/DraftList.tsx | 2 +- .../[publication]/dashboard/DraftsTab.tsx | 51 +++++ .../dashboard/PublicationDashboard.tsx | 160 -------------- .../dashboard/PublishedPostsLists.tsx | 2 +- .../dashboard/analytics/page.tsx | 39 ++++ .../[did]/[publication]/dashboard/layout.tsx | 105 ++++++++++ .../[did]/[publication]/dashboard/page.tsx | 109 ++-------- .../[publication]/dashboard/posts/page.tsx | 42 ++++ .../[publication]/dashboard/settings/page.tsx | 28 +++ .../[publication]/dashboard/subs/page.tsx | 28 +++ .../theme-settings/ThemeSettingsContent.tsx | 2 +- components/ActionBar/DesktopNavigation.tsx | 78 +++---- components/ActionBar/NavigationButtons.tsx | 39 ++-- components/ActionBar/ProfileButton.tsx | 1 + .../ActionBar/PublicationNavigation.tsx | 142 ------------- components/ActionBar/Publications.tsx | 6 +- components/IdentityProvider.tsx | 2 +- components/LoginButton.tsx | 2 +- components/PageLayouts/DashboardLayout.tsx | 195 ------------------ components/PageLayouts/DashboardShell.tsx | 57 +++++ components/PageLayouts/PageSearch.tsx | 2 +- components/PageLayouts/dashboardState.ts | 103 +++++++++ components/utils/AddLeafletToHomepage.tsx | 2 +- 71 files changed, 867 insertions(+), 933 deletions(-) create mode 100644 app/(home-pages)/(writer)/WriterShell.tsx rename app/(home-pages)/{ => (writer)}/home/Actions/Actions.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/Actions/CreateNewButton.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/Actions/HomeThemeSetter.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/HomeEmpty/DiscoverIllo.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/HomeEmpty/HomeEmpty.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/HomeEmpty/WelcomeToLeafletIllo.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/HomeLayout.tsx (85%) rename app/(home-pages)/{ => (writer)}/home/IdentitySetter.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletContent.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletInfo.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletListItem.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletOptions.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletPreview.module.css (100%) rename app/(home-pages)/{ => (writer)}/home/LeafletList/LeafletPreview.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/icon.tsx (100%) rename app/(home-pages)/{ => (writer)}/home/page.tsx (95%) rename app/(home-pages)/{ => (writer)}/home/storage.ts (100%) create mode 100644 app/(home-pages)/(writer)/layout.tsx rename app/(home-pages)/{ => (writer)}/looseleafs/LooseleafsLayout.tsx (71%) rename app/(home-pages)/{ => (writer)}/looseleafs/page.tsx (94%) rename app/(home-pages)/{ => (writer)}/notifications/BskyPostEmbedNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/CommentMentionNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/CommentNotication.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/FollowNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/MentionNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/Notification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/NotificationList.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/QuoteNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/RecommendNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/ReplyNotification.tsx (100%) rename app/(home-pages)/{ => (writer)}/notifications/getNotifications.ts (100%) create mode 100644 app/(home-pages)/(writer)/notifications/page.tsx delete mode 100644 app/(home-pages)/notifications/page.tsx delete mode 100644 app/(home-pages)/reader/ReaderLayout.tsx create mode 100644 app/(home-pages)/reader/layout.tsx delete mode 100644 app/(home-pages)/reader/loading.tsx create mode 100644 app/(home-pages)/reader/trending/page.tsx create mode 100644 app/(home-pages)/tag/[tag]/layout.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/DraftsTab.tsx delete mode 100644 app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/analytics/page.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/layout.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/posts/page.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/settings/page.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/subs/page.tsx delete mode 100644 components/ActionBar/PublicationNavigation.tsx delete mode 100644 components/PageLayouts/DashboardLayout.tsx create mode 100644 components/PageLayouts/DashboardShell.tsx create mode 100644 components/PageLayouts/dashboardState.ts diff --git a/app/(home-pages)/(writer)/WriterShell.tsx b/app/(home-pages)/(writer)/WriterShell.tsx new file mode 100644 index 00000000..b939d6ab --- /dev/null +++ b/app/(home-pages)/(writer)/WriterShell.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { DashboardShell } from "components/PageLayouts/DashboardShell"; +import { PageTitle } from "components/ActionBar/DesktopNavigation"; +import { HomeSmall } from "components/Icons/HomeSmall"; +import { LooseleafTiny } from "components/Icons/LooseleafTiny"; +import { NotificationsUnreadSmall } from "components/Icons/NotificationSmall"; +import { Actions } from "./home/Actions/Actions"; + +const PAGE_META = [ + { + prefix: "/home", + id: "home", + title: "Home", + icon: , + }, + { + prefix: "/looseleafs", + id: "looseleafs", + title: "Looseleafs", + icon: , + }, + { + prefix: "/notifications", + id: "notifications", + title: "Notifications", + icon: , + }, +]; + +export function WriterShell(props: { children: React.ReactNode }) { + const pathname = usePathname(); + const meta = PAGE_META.find((m) => pathname.startsWith(m.prefix)); + + return ( + + } + actions={} + > + {props.children} + + ); +} diff --git a/app/(home-pages)/home/Actions/Actions.tsx b/app/(home-pages)/(writer)/home/Actions/Actions.tsx similarity index 100% rename from app/(home-pages)/home/Actions/Actions.tsx rename to app/(home-pages)/(writer)/home/Actions/Actions.tsx diff --git a/app/(home-pages)/home/Actions/CreateNewButton.tsx b/app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx similarity index 100% rename from app/(home-pages)/home/Actions/CreateNewButton.tsx rename to app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx diff --git a/app/(home-pages)/home/Actions/HomeThemeSetter.tsx b/app/(home-pages)/(writer)/home/Actions/HomeThemeSetter.tsx similarity index 100% rename from app/(home-pages)/home/Actions/HomeThemeSetter.tsx rename to app/(home-pages)/(writer)/home/Actions/HomeThemeSetter.tsx diff --git a/app/(home-pages)/home/HomeEmpty/DiscoverIllo.tsx b/app/(home-pages)/(writer)/home/HomeEmpty/DiscoverIllo.tsx similarity index 100% rename from app/(home-pages)/home/HomeEmpty/DiscoverIllo.tsx rename to app/(home-pages)/(writer)/home/HomeEmpty/DiscoverIllo.tsx diff --git a/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx b/app/(home-pages)/(writer)/home/HomeEmpty/HomeEmpty.tsx similarity index 100% rename from app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx rename to app/(home-pages)/(writer)/home/HomeEmpty/HomeEmpty.tsx diff --git a/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx b/app/(home-pages)/(writer)/home/HomeEmpty/WelcomeToLeafletIllo.tsx similarity index 100% rename from app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx rename to app/(home-pages)/(writer)/home/HomeEmpty/WelcomeToLeafletIllo.tsx diff --git a/app/(home-pages)/home/HomeLayout.tsx b/app/(home-pages)/(writer)/home/HomeLayout.tsx similarity index 85% rename from app/(home-pages)/home/HomeLayout.tsx rename to app/(home-pages)/(writer)/home/HomeLayout.tsx index 7ed879c6..4dc5dd4d 100644 --- a/app/(home-pages)/home/HomeLayout.tsx +++ b/app/(home-pages)/(writer)/home/HomeLayout.tsx @@ -14,19 +14,15 @@ import type { Attribute } from "src/replicache/attributes"; import { callRPC } from "app/api/rpc/client"; import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; import { - DashboardLayout, - DashboardState, + type DashboardState, useDashboardState, -} from "components/PageLayouts/DashboardLayout"; +} from "components/PageLayouts/dashboardState"; import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { PageSearch } from "components/PageLayouts/PageSearch"; -import { Actions } from "./Actions/Actions"; import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; import { useState } from "react"; import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; import { HomeEmptyState } from "./HomeEmpty/HomeEmpty"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { HomeSmall } from "components/Icons/HomeSmall"; import { CreateNewLeafletButton } from "./Actions/CreateNewButton"; export type Leaflet = { @@ -44,7 +40,7 @@ export type Leaflet = { }; }; -export const HomeLayout = (props: { +export const HomeContent = (props: { entityID: string | null; titles: { [root_entity: string]: string }; initialFacts: { @@ -77,41 +73,28 @@ export const HomeLayout = (props: { ).length > 0; return ( - } - pageTitle={} pageTitle="Home" />} - tabs={{ - home: { - content: ( - } - search={ - - } - showHeader={true} - > - - - ), - }, - }} - /> + } + search={ + + } + showHeader={true} + > + + ); }; diff --git a/app/(home-pages)/home/IdentitySetter.tsx b/app/(home-pages)/(writer)/home/IdentitySetter.tsx similarity index 100% rename from app/(home-pages)/home/IdentitySetter.tsx rename to app/(home-pages)/(writer)/home/IdentitySetter.tsx diff --git a/app/(home-pages)/home/LeafletList/LeafletContent.tsx b/app/(home-pages)/(writer)/home/LeafletList/LeafletContent.tsx similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletContent.tsx rename to app/(home-pages)/(writer)/home/LeafletList/LeafletContent.tsx diff --git a/app/(home-pages)/home/LeafletList/LeafletInfo.tsx b/app/(home-pages)/(writer)/home/LeafletList/LeafletInfo.tsx similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletInfo.tsx rename to app/(home-pages)/(writer)/home/LeafletList/LeafletInfo.tsx diff --git a/app/(home-pages)/home/LeafletList/LeafletListItem.tsx b/app/(home-pages)/(writer)/home/LeafletList/LeafletListItem.tsx similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletListItem.tsx rename to app/(home-pages)/(writer)/home/LeafletList/LeafletListItem.tsx diff --git a/app/(home-pages)/home/LeafletList/LeafletOptions.tsx b/app/(home-pages)/(writer)/home/LeafletList/LeafletOptions.tsx similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletOptions.tsx rename to app/(home-pages)/(writer)/home/LeafletList/LeafletOptions.tsx diff --git a/app/(home-pages)/home/LeafletList/LeafletPreview.module.css b/app/(home-pages)/(writer)/home/LeafletList/LeafletPreview.module.css similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletPreview.module.css rename to app/(home-pages)/(writer)/home/LeafletList/LeafletPreview.module.css diff --git a/app/(home-pages)/home/LeafletList/LeafletPreview.tsx b/app/(home-pages)/(writer)/home/LeafletList/LeafletPreview.tsx similarity index 100% rename from app/(home-pages)/home/LeafletList/LeafletPreview.tsx rename to app/(home-pages)/(writer)/home/LeafletList/LeafletPreview.tsx diff --git a/app/(home-pages)/home/icon.tsx b/app/(home-pages)/(writer)/home/icon.tsx similarity index 100% rename from app/(home-pages)/home/icon.tsx rename to app/(home-pages)/(writer)/home/icon.tsx diff --git a/app/(home-pages)/home/page.tsx b/app/(home-pages)/(writer)/home/page.tsx similarity index 95% rename from app/(home-pages)/home/page.tsx rename to app/(home-pages)/(writer)/home/page.tsx index 5408ab37..45163cd9 100644 --- a/app/(home-pages)/home/page.tsx +++ b/app/(home-pages)/(writer)/home/page.tsx @@ -2,7 +2,7 @@ import { getIdentityData } from "actions/getIdentityData"; import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; import { supabaseServerClient } from "supabase/serverClient"; -import { HomeLayout } from "./HomeLayout"; +import { HomeContent } from "./HomeLayout"; export default async function Home() { let auth_res = await getIdentityData(); @@ -23,7 +23,7 @@ export default async function Home() { let home_docs_initialFacts = allLeafletFacts?.result || {}; return ( - {props.children}; +} diff --git a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx b/app/(home-pages)/(writer)/looseleafs/LooseleafsLayout.tsx similarity index 71% rename from app/(home-pages)/looseleafs/LooseleafsLayout.tsx rename to app/(home-pages)/(writer)/looseleafs/LooseleafsLayout.tsx index 47015094..523e48eb 100644 --- a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx +++ b/app/(home-pages)/(writer)/looseleafs/LooseleafsLayout.tsx @@ -1,20 +1,16 @@ "use client"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { useState } from "react"; import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; import { Fact, PermissionToken } from "src/replicache"; import { Attribute } from "src/replicache/attributes"; -import { Actions } from "../home/Actions/Actions"; import { callRPC } from "app/api/rpc/client"; import { useIdentityData } from "components/IdentityProvider"; import useSWR from "swr"; import { Leaflet, LeafletList } from "../home/HomeLayout"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { LooseleafTiny } from "components/Icons/LooseleafTiny"; import { CreateNewLeafletButton } from "../home/Actions/CreateNewButton"; -export const LooseleafsLayout = (props: { +export const LooseleafsContent = (props: { entityID: string | null; titles: { [root_entity: string]: string }; initialFacts: { @@ -33,33 +29,18 @@ export const LooseleafsLayout = (props: { ); return ( - } - pageTitle={ - } pageTitle={"Looseleafs"} /> - } - tabs={{ - home: { - content: ( - } - showHeader={false} - > - - - ), - }, - }} - /> + } + showHeader={false} + > + + ); }; diff --git a/app/(home-pages)/looseleafs/page.tsx b/app/(home-pages)/(writer)/looseleafs/page.tsx similarity index 94% rename from app/(home-pages)/looseleafs/page.tsx rename to app/(home-pages)/(writer)/looseleafs/page.tsx index 49e61614..462e8e99 100644 --- a/app/(home-pages)/looseleafs/page.tsx +++ b/app/(home-pages)/(writer)/looseleafs/page.tsx @@ -1,7 +1,7 @@ import { getIdentityData } from "actions/getIdentityData"; import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; import { supabaseServerClient } from "supabase/serverClient"; -import { LooseleafsLayout } from "./LooseleafsLayout"; +import { LooseleafsContent } from "./LooseleafsLayout"; export default async function Home() { let auth_res = await getIdentityData(); @@ -22,7 +22,7 @@ export default async function Home() { let home_docs_initialFacts = allLeafletFacts?.result || {}; return ( - + + + ); +} + +const NotificationContent = async () => { + let identity = await getIdentityData(); + if (!identity?.atp_did) return redirect("/home"); + let { data } = await supabaseServerClient + .from("notifications") + .select("*") + .eq("recipient", identity.atp_did); + let notifications = await hydrateNotifications(data || []); + return ; +}; diff --git a/app/(home-pages)/notifications/page.tsx b/app/(home-pages)/notifications/page.tsx deleted file mode 100644 index 77174d97..00000000 --- a/app/(home-pages)/notifications/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { getIdentityData } from "actions/getIdentityData"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; -import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; -import { redirect } from "next/navigation"; -import { hydrateNotifications } from "src/notifications"; -import { supabaseServerClient } from "supabase/serverClient"; -import { CommentNotification } from "./CommentNotication"; -import { NotificationList } from "./NotificationList"; -import { NotificationsUnreadSmall } from "components/Icons/NotificationSmall"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; - -export default async function Notifications() { - return ( - } - pageTitle={"Notifications"} - /> - } - tabs={{ - default: { - content: ( - - - - ), - }, - }} - /> - ); -} - -const NotificationContent = async () => { - let identity = await getIdentityData(); - if (!identity?.atp_did) return redirect("/home"); - let { data } = await supabaseServerClient - .from("notifications") - .select("*") - .eq("recipient", identity.atp_did); - let notifications = await hydrateNotifications(data || []); - return ; -}; diff --git a/app/(home-pages)/p/[didOrHandle]/layout.tsx b/app/(home-pages)/p/[didOrHandle]/layout.tsx index 49de62b2..b3592802 100644 --- a/app/(home-pages)/p/[didOrHandle]/layout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/layout.tsx @@ -4,7 +4,7 @@ import { supabaseServerClient } from "supabase/serverClient"; import { Json } from "supabase/database.types"; import { ProfileHeader } from "./ProfileHeader"; import { ProfileTabs } from "./ProfileTabs"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; +import { DashboardShell } from "components/PageLayouts/DashboardShell"; import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { ProfileLayout } from "./ProfileLayout"; import { Agent } from "@atproto/api"; @@ -81,12 +81,8 @@ export default async function ProfilePageLayout(props: { if (!profile) return null; return ( - } tabs={{ - default: { - content: ( - - - - - <>{props.children} - - - ), - }, + Inbox: { href: "/reader/inbox" }, + Trending: { href: "/reader/trending" }, }} - /> + > + + + + + <>{props.children} + + + ); } diff --git a/app/(home-pages)/page.tsx b/app/(home-pages)/page.tsx index 7d8df18c..5b1539da 100644 --- a/app/(home-pages)/page.tsx +++ b/app/(home-pages)/page.tsx @@ -1,36 +1,15 @@ import { cookies } from "next/headers"; -import HomePage from "./home/page"; -import { getReaderFeed } from "./reader/getReaderFeed"; -import { getHotFeed } from "./reader/getHotFeed"; -import { ReaderLayout } from "./reader/ReaderLayout"; +import { redirect } from "next/navigation"; export default async function RootPage() { const cookieStore = await cookies(); const hasAuth = cookieStore.has("auth_token") || cookieStore.has("external_auth_token"); - // if the user isn't logged in, leaflet.pub redirects to reader - if (!hasAuth) { - return ( - - ); - } + if (!hasAuth) redirect("/reader"); const navState = cookieStore.get("nav-state")?.value; - // if the user IS logged in and was last on reader, leaflet.pub redirects to reader - if (navState === "reader") { - return ( - - ); - } - // else redirect to writer home - return ; + if (navState === "reader") redirect("/reader"); + + redirect("/home"); } diff --git a/app/(home-pages)/reader/InboxContent.tsx b/app/(home-pages)/reader/InboxContent.tsx index 8d774410..5ec4f604 100644 --- a/app/(home-pages)/reader/InboxContent.tsx +++ b/app/(home-pages)/reader/InboxContent.tsx @@ -127,7 +127,7 @@ export const ReaderEmpty = () => {
Nothing to read yet…
Subscribe to publications and find their posts here! - + See what posts people are reading! diff --git a/app/(home-pages)/reader/ReaderLayout.tsx b/app/(home-pages)/reader/ReaderLayout.tsx deleted file mode 100644 index c8be5af8..00000000 --- a/app/(home-pages)/reader/ReaderLayout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; -import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; -import { ReaderUnreadSmall } from "components/Icons/ReaderSmall"; -import { GlobalContent } from "./GlobalContent"; -import { InboxContent } from "./InboxContent"; -import type { Cursor, Post } from "./getReaderFeed"; - -export function ReaderLayout(props: { - readerFeedPromise: Promise<{ posts: Post[]; nextCursor: Cursor | null }>; - hotFeedPromise: Promise<{ posts: Post[] }>; - defaultTab: "Inbox" | "Trending"; -}) { - return ( - } pageTitle="Reader" />} - tabs={{ - Inbox: { - content: ( - - - - ), - }, - Trending: { - content: ( - - - - ), - }, - }} - /> - ); -} diff --git a/app/(home-pages)/reader/hot/page.tsx b/app/(home-pages)/reader/hot/page.tsx index 3e0430b2..5a4e6d6c 100644 --- a/app/(home-pages)/reader/hot/page.tsx +++ b/app/(home-pages)/reader/hot/page.tsx @@ -1,7 +1,5 @@ -import { getHotFeed } from "../getHotFeed"; -import { GlobalContent } from "../GlobalContent"; +import { redirect } from "next/navigation"; -export default async function HotPage() { - const feedPromise = getHotFeed(); - return ; +export default function HotPage() { + redirect("/reader/trending"); } diff --git a/app/(home-pages)/reader/layout.tsx b/app/(home-pages)/reader/layout.tsx new file mode 100644 index 00000000..7f299d24 --- /dev/null +++ b/app/(home-pages)/reader/layout.tsx @@ -0,0 +1,23 @@ +import { getIdentityData } from "actions/getIdentityData"; +import { PageTitle } from "components/ActionBar/DesktopNavigation"; +import { DashboardShell } from "components/PageLayouts/DashboardShell"; +import { ReaderUnreadSmall } from "components/Icons/ReaderSmall"; + +export default async function ReaderLayout(props: { + children: React.ReactNode; +}) { + const identity = await getIdentityData(); + const tabs: { [name: string]: { href: string } } = {}; + if (identity?.atp_did) tabs.Inbox = { href: "/reader" }; + tabs.Trending = { href: "/reader/trending" }; + + return ( + } pageTitle="Reader" />} + tabs={tabs} + > + {props.children} + + ); +} diff --git a/app/(home-pages)/reader/loading.tsx b/app/(home-pages)/reader/loading.tsx deleted file mode 100644 index 06773957..00000000 --- a/app/(home-pages)/reader/loading.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { FeedSkeleton } from "./FeedSkeleton"; -export default function Loading() { - return ; -} diff --git a/app/(home-pages)/reader/new/page.tsx b/app/(home-pages)/reader/new/page.tsx index c857832f..002710d0 100644 --- a/app/(home-pages)/reader/new/page.tsx +++ b/app/(home-pages)/reader/new/page.tsx @@ -1,7 +1,19 @@ +import { Suspense } from "react"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { getNewFeed } from "../getNewFeed"; import { NewContent } from "../NewContent"; +import { FeedSkeleton } from "../FeedSkeleton"; -export default async function NewPage() { - const feedPromise = getNewFeed(); - return ; +export default function NewPage() { + return ( + + }> + + + + ); } diff --git a/app/(home-pages)/reader/page.tsx b/app/(home-pages)/reader/page.tsx index 9abef7e4..ea4c247a 100644 --- a/app/(home-pages)/reader/page.tsx +++ b/app/(home-pages)/reader/page.tsx @@ -1,17 +1,29 @@ +import { Suspense } from "react"; +import { redirect } from "next/navigation"; import { getIdentityData } from "actions/getIdentityData"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { getReaderFeed } from "./getReaderFeed"; -import { getHotFeed } from "./getHotFeed"; import { InboxContent } from "./InboxContent"; -import { GlobalContent } from "./GlobalContent"; -import { ReaderLayout } from "./ReaderLayout"; +import { FeedSkeleton } from "./FeedSkeleton"; + +export default async function ReaderPage(props: { + searchParams: Promise<{ tab?: string }>; +}) { + const { tab } = await props.searchParams; + if (tab === "Trending") redirect("/reader/trending"); + + const identity = await getIdentityData(); + if (!identity?.atp_did) redirect("/reader/trending"); -export default async function Reader() { - let identityData = await getIdentityData(); return ( - + + }> + + + ); } diff --git a/app/(home-pages)/reader/trending/page.tsx b/app/(home-pages)/reader/trending/page.tsx new file mode 100644 index 00000000..cf1c8fdd --- /dev/null +++ b/app/(home-pages)/reader/trending/page.tsx @@ -0,0 +1,19 @@ +import { Suspense } from "react"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { getHotFeed } from "../getHotFeed"; +import { GlobalContent } from "../GlobalContent"; +import { FeedSkeleton } from "../FeedSkeleton"; + +export default function ReaderTrendingPage() { + return ( + + }> + + + + ); +} diff --git a/app/(home-pages)/tag/[tag]/layout.tsx b/app/(home-pages)/tag/[tag]/layout.tsx new file mode 100644 index 00000000..2c33995f --- /dev/null +++ b/app/(home-pages)/tag/[tag]/layout.tsx @@ -0,0 +1,24 @@ +import { DashboardShell } from "components/PageLayouts/DashboardShell"; +import { PageTitle } from "components/ActionBar/DesktopNavigation"; +import { TagTiny } from "components/Icons/TagTiny"; + +export default async function TagLayout(props: { + children: React.ReactNode; + params: Promise<{ tag: string }>; +}) { + const params = await props.params; + const decodedTag = decodeURIComponent(params.tag); + + return ( + } pageTitle={decodedTag} />} + tabs={{ + Inbox: { href: "/reader/inbox" }, + Trending: { href: "/reader/trending" }, + }} + > + {props.children} + + ); +} diff --git a/app/(home-pages)/tag/[tag]/page.tsx b/app/(home-pages)/tag/[tag]/page.tsx index 043b3774..099c1349 100644 --- a/app/(home-pages)/tag/[tag]/page.tsx +++ b/app/(home-pages)/tag/[tag]/page.tsx @@ -1,10 +1,7 @@ -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; import { PostListing } from "components/PostListing"; import { getDocumentsByTag } from "./getDocumentsByTag"; import { Metadata } from "next"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { TagTiny } from "components/Icons/TagTiny"; export async function generateMetadata(props: { params: Promise<{ tag: string }>; @@ -22,26 +19,13 @@ export default async function TagPage(props: { const { posts } = await getDocumentsByTag(decodedTag); return ( - } pageTitle={decodedTag} /> - tabs={{ - default: { - content: ( - - - - ), - }, - }} - /> + + + ); } diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index 574c6435..90eececd 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -34,7 +34,7 @@ import { PublicationBackgroundProvider, } from "components/ThemeManager/PublicationThemeProvider"; import { useEntity } from "src/replicache"; -import { LeafletContent } from "app/(home-pages)/home/LeafletList/LeafletContent"; +import { LeafletContent } from "app/(home-pages)/(writer)/home/LeafletList/LeafletContent"; type Props = { title: string; diff --git a/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx b/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx new file mode 100644 index 00000000..05fe46f0 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Actions } from "./Actions"; +import { DashboardShell } from "components/PageLayouts/DashboardShell"; +import { type NormalizedPublication } from "src/utils/normalizeRecords"; +import { useCanSeePro } from "src/hooks/useEntitlement"; +import { PubIcon } from "components/ActionBar/Publications"; +import { PageTitle } from "components/ActionBar/DesktopNavigation"; +import { SettingsSmall } from "components/Icons/SettingsSmall"; +import { + PublicationSWRDataProvider, +} from "./PublicationSWRProvider"; +import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; +import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; + +export function DashboardChrome(props: { + publication_did: string; + publication_rkey: string; + publication_data: GetPublicationDataReturnType["result"]; + pubUri: string; + record: NormalizedPublication; + routeDid: string; + routePublication: string; + children: React.ReactNode; +}) { + let canSeePro = useCanSeePro(); + let baseHref = `/lish/${props.routeDid}/${props.routePublication}/dashboard`; + + return ( + + + + } + /> + } + actions={} + tabs={{ + Drafts: { href: baseHref }, + Posts: { href: `${baseHref}/posts` }, + Subs: { href: `${baseHref}/subs` }, + ...(canSeePro + ? { Analytics: { href: `${baseHref}/analytics` } } + : {}), + Settings: { + href: `${baseHref}/settings`, + icon: , + }, + }} + > + {props.children} + + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/DraftList.tsx b/app/lish/[did]/[publication]/dashboard/DraftList.tsx index 49ff4514..ddce3c10 100644 --- a/app/lish/[did]/[publication]/dashboard/DraftList.tsx +++ b/app/lish/[did]/[publication]/dashboard/DraftList.tsx @@ -5,7 +5,7 @@ import { usePublicationData, useNormalizedPublicationRecord, } from "./PublicationSWRProvider"; -import { LeafletList } from "app/(home-pages)/home/HomeLayout"; +import { LeafletList } from "app/(home-pages)/(writer)/home/HomeLayout"; export function DraftList(props: { searchValue: string; diff --git a/app/lish/[did]/[publication]/dashboard/DraftsTab.tsx b/app/lish/[did]/[publication]/dashboard/DraftsTab.tsx new file mode 100644 index 00000000..6f98b57b --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/DraftsTab.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useState } from "react"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { DraftList } from "./DraftList"; +import { NewDraftActionButton } from "./NewDraftButton"; +import { PageSearch } from "components/PageLayouts/PageSearch"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "./PublicationSWRProvider"; + +export function DraftsTab() { + let { data } = usePublicationData(); + let record = useNormalizedPublicationRecord(); + let pubUri = data?.publication?.uri || ""; + + let [searchValue, setSearchValue] = useState(""); + let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); + useDebouncedEffect( + () => setDebouncedSearchValue(searchValue), + 200, + [searchValue], + ); + + const showPageBackground = !!record?.theme?.showPageBackground; + + return ( + } + search={ + + } + publication={pubUri} + showHeader={true} + > + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx deleted file mode 100644 index 7b2fbd4e..00000000 --- a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -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 { PublicationSubscribers } from "./PublicationSubscribers"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; -import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; -import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; -import { type NormalizedPublication } from "src/utils/normalizeRecords"; -import { PublicationAnalytics } from "./PublicationAnalytics"; -import { useCanSeePro } from "src/hooks/useEntitlement"; -import { SettingsContent } from "./settings/SettingsContent"; -import { PageSearch } from "components/PageLayouts/PageSearch"; -import { PubIcon } from "components/ActionBar/Publications"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { NewDraftActionButton } from "./NewDraftButton"; -import { SettingsSmall } from "components/Icons/SettingsSmall"; - -export default function PublicationDashboard({ - publication, - record, -}: { - record: NormalizedPublication; - publication: Exclude< - GetPublicationDataReturnType["result"]["publication"], - null - >; -}) { - let canSeePro = useCanSeePro(); - let [searchValue, setSearchValue] = useState(""); - let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); - - useDebouncedEffect( - () => { - setDebouncedSearchValue(searchValue); - }, - 200, - [searchValue], - ); - - const pubUri = publication.uri; - const showPageBackground = !!record.theme?.showPageBackground; - - return ( - } - currentPage="pub" - publication={pubUri} - pageTitle={ - } - /> - } - tabs={{ - Drafts: { - content: ( - - } - search={ - - } - publication={pubUri} - showHeader={true} - > - - - ), - }, - Posts: { - content: ( - - } - publication={pubUri} - showHeader={false} - > - - - ), - }, - Subs: { - content: ( - - } - publication={pubUri} - showHeader={false} - > - - - ), - }, - ...(canSeePro - ? { - Analytics: { - content: ( - - } - publication={pubUri} - showHeader={false} - > - - - ), - }, - } - : {}), - Settings: { - icon: , - content: ( - - } - publication={pubUri} - showHeader={false} - > - - - ), - }, - }} - /> - ); -} diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx index cbc9e4f9..fe4ab6f1 100644 --- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -17,7 +17,7 @@ import { import { SpeedyLink } from "components/SpeedyLink"; import { InteractionPreview } from "components/InteractionsPreview"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; -import { LeafletOptions } from "app/(home-pages)/home/LeafletList/LeafletOptions"; +import { LeafletOptions } from "app/(home-pages)/(writer)/home/LeafletList/LeafletOptions"; import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; export function PublishedPostsList(props: { diff --git a/app/lish/[did]/[publication]/dashboard/analytics/page.tsx b/app/lish/[did]/[publication]/dashboard/analytics/page.tsx new file mode 100644 index 00000000..491c56ea --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/analytics/page.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { PublicationAnalytics } from "../PublicationAnalytics"; +import { NewDraftActionButton } from "../NewDraftButton"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; +import { useCanSeePro } from "src/hooks/useEntitlement"; + +export default function AnalyticsPage() { + let canSeePro = useCanSeePro(); + let router = useRouter(); + let { data } = usePublicationData(); + let record = useNormalizedPublicationRecord(); + let pubUri = data?.publication?.uri || ""; + const showPageBackground = !!record?.theme?.showPageBackground; + + useEffect(() => { + if (canSeePro === false) router.replace("../"); + }, [canSeePro, router]); + + if (!canSeePro) return null; + + return ( + } + publication={pubUri} + showHeader={false} + > + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/layout.tsx b/app/lish/[did]/[publication]/dashboard/layout.tsx new file mode 100644 index 00000000..46aea374 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/layout.tsx @@ -0,0 +1,105 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import { Metadata } from "next"; +import { getIdentityData } from "actions/getIdentityData"; +import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; +import { normalizePublicationRecord } from "src/utils/normalizeRecords"; +import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; +import { LoginModal } from "components/LoginButton"; +import { AtUri } from "@atproto/syntax"; +import { DashboardChrome } from "./DashboardChrome"; + +export async function generateMetadata(props: { + params: Promise<{ publication: string; did: string }>; +}): Promise { + let did = decodeURIComponent((await props.params).did); + if (!did) return { title: "Publication 404" }; + + let { result: publication_data } = await get_publication_data.handler( + { + did, + publication_name: decodeURIComponent((await props.params).publication), + }, + { supabase: supabaseServerClient }, + ); + let { publication } = publication_data; + const record = normalizePublicationRecord(publication?.record); + if (!publication) return { title: "404 Publication" }; + return { title: record?.name || "Untitled Publication" }; +} + +export default async function PublicationDashboardLayout(props: { + children: React.ReactNode; + params: Promise<{ publication: string; did: string }>; +}) { + let params = await props.params; + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return ( + +

+ Looks like you're not logged in.{" "} + Log in here

+ } + /> + ! +

+

+ If the issue persists please{" "} + send us a note. +

+ + ); + } + + let did = decodeURIComponent(params.did); + if (!did) return ; + + let { result: publication_data } = await get_publication_data.handler( + { + did, + publication_name: decodeURIComponent(params.publication), + }, + { supabase: supabaseServerClient }, + ); + let { publication } = publication_data; + const record = normalizePublicationRecord(publication?.record); + + if ( + !publication || + identity.atp_did !== publication.identity_did || + !record + ) { + return ; + } + + let uri = new AtUri(publication.uri); + + return ( + + {props.children} + + ); +} + +const PubNotFound = () => { + return ( + +

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]/dashboard/page.tsx b/app/lish/[did]/[publication]/dashboard/page.tsx index 4e9bfe4a..ca858b1f 100644 --- a/app/lish/[did]/[publication]/dashboard/page.tsx +++ b/app/lish/[did]/[publication]/dashboard/page.tsx @@ -1,100 +1,19 @@ -import { supabaseServerClient } from "supabase/serverClient"; -import { Metadata } from "next"; -import { getIdentityData } from "actions/getIdentityData"; -import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; -import { PublicationSWRDataProvider } from "./PublicationSWRProvider"; -import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; -import { AtUri } from "@atproto/syntax"; -import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; -import PublicationDashboard from "./PublicationDashboard"; -import { normalizePublicationRecord } from "src/utils/normalizeRecords"; -import { LoginModal } from "components/LoginButton"; +import { redirect } from "next/navigation"; +import { DraftsTab } from "./DraftsTab"; -export async function generateMetadata(props: { - params: Promise<{ publication: string; did: string }>; -}): Promise { - let did = decodeURIComponent((await props.params).did); - if (!did) return { title: "Publication 404" }; - - let { result: publication_data } = await get_publication_data.handler( - { - did, - publication_name: decodeURIComponent((await props.params).publication), - }, - { supabase: supabaseServerClient }, - ); - let { publication } = publication_data; - const record = normalizePublicationRecord(publication?.record); - if (!publication) return { title: "404 Publication" }; - return { title: record?.name || "Untitled Publication" }; -} - -//This is the admin dashboard of the publication -export default async function Publication(props: { +export default async function DashboardIndex(props: { params: Promise<{ publication: string; did: string }>; + searchParams: Promise<{ tab?: string }>; }) { - let params = await props.params; - let identity = await getIdentityData(); - if (!identity || !identity.atp_did) - return ( - -

- Looks like you're not logged in.{" "} - Log in here - } - /> - ! -

-

- If the issue persists please{" "} - send us a note. -

-
- ); - let did = decodeURIComponent(params.did); - if (!did) return ; - 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; - const record = normalizePublicationRecord(publication?.record); - - if (!publication || identity.atp_did !== publication.identity_did || !record) - return ; - let uri = new AtUri(publication.uri); - - try { - return ( - - - - - - ); - } catch (e) { - return
{JSON.stringify(e, undefined, 2)}
; + const { tab } = await props.searchParams; + if (tab && tab !== "Drafts") { + const params = await props.params; + const base = `/lish/${params.did}/${params.publication}/dashboard`; + if (tab === "Posts") redirect(`${base}/posts`); + if (tab === "Subs") redirect(`${base}/subs`); + if (tab === "Analytics") redirect(`${base}/analytics`); + if (tab === "Settings") redirect(`${base}/settings`); } -} -const PubNotFound = () => { - return ( - -

Sorry, we can't find this publication!

-

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

-
- ); -}; + return ; +} diff --git a/app/lish/[did]/[publication]/dashboard/posts/page.tsx b/app/lish/[did]/[publication]/dashboard/posts/page.tsx new file mode 100644 index 00000000..3bec1be2 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/posts/page.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState } from "react"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { PublishedPostsList } from "../PublishedPostsLists"; +import { NewDraftActionButton } from "../NewDraftButton"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; + +export default function PostsPage() { + let { data } = usePublicationData(); + let record = useNormalizedPublicationRecord(); + let pubUri = data?.publication?.uri || ""; + + let [searchValue, setSearchValue] = useState(""); + let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); + useDebouncedEffect( + () => setDebouncedSearchValue(searchValue), + 200, + [searchValue], + ); + + const showPageBackground = !!record?.theme?.showPageBackground; + + return ( + } + publication={pubUri} + showHeader={false} + > + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/settings/page.tsx b/app/lish/[did]/[publication]/dashboard/settings/page.tsx new file mode 100644 index 00000000..1ab016ae --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/settings/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { SettingsContent } from "./SettingsContent"; +import { NewDraftActionButton } from "../NewDraftButton"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; + +export default function SettingsPage() { + let { data } = usePublicationData(); + let record = useNormalizedPublicationRecord(); + let pubUri = data?.publication?.uri || ""; + const showPageBackground = !!record?.theme?.showPageBackground; + + return ( + } + publication={pubUri} + showHeader={false} + > + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/subs/page.tsx b/app/lish/[did]/[publication]/dashboard/subs/page.tsx new file mode 100644 index 00000000..7ffc442d --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/subs/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { DashboardPageLayout } from "components/PageLayouts/DashboardPageLayout"; +import { PublicationSubscribers } from "../PublicationSubscribers"; +import { NewDraftActionButton } from "../NewDraftButton"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; + +export default function SubsPage() { + let { data } = usePublicationData(); + let record = useNormalizedPublicationRecord(); + let pubUri = data?.publication?.uri || ""; + const showPageBackground = !!record?.theme?.showPageBackground; + + return ( + } + publication={pubUri} + showHeader={false} + > + + + ); +} diff --git a/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx b/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx index 51a284a7..047a9200 100644 --- a/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx +++ b/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx @@ -46,7 +46,7 @@ export function ThemeSettingsContent() { changes, } = state; - let settingsHref = `/lish/${params.did}/${params.publication}/dashboard?tab=Settings`; + let settingsHref = `/lish/${params.did}/${params.publication}/dashboard/settings`; let hasUnsavedChanges = changes || diff --git a/components/ActionBar/DesktopNavigation.tsx b/components/ActionBar/DesktopNavigation.tsx index 28af90ce..00b00ab9 100644 --- a/components/ActionBar/DesktopNavigation.tsx +++ b/components/ActionBar/DesktopNavigation.tsx @@ -1,6 +1,7 @@ +"use client"; +import { usePathname } from "next/navigation"; import { useIdentityData } from "components/IdentityProvider"; import { - navPages, NotificationButton, ReaderButton, WriterButton, @@ -12,25 +13,36 @@ import { ProfileButton } from "./ProfileButton"; import { ActionButton } from "./ActionButton"; import { AccountSmall } from "components/Icons/AccountSmall"; import { TabsSmall } from "components/Icons/TabsSmall"; +import { SpeedyLink } from "components/SpeedyLink"; type NavigationProps = { pageTitle: React.ReactNode; - currentPage: navPages; publication?: string; actions?: React.ReactNode; - tabs?: { [name: string]: { icon?: React.ReactNode } }; - currentTab?: string; - onTabClick?: (tab: string) => void; - onTabHover?: (tab: string) => void; + tabs?: { [name: string]: { href: string; icon?: React.ReactNode } }; }; +function pickActiveTabHref( + pathname: string, + tabs: { [name: string]: { href: string } }, +): string | null { + let best: string | null = null; + for (const { href } of Object.values(tabs)) { + if (pathname === href || pathname.startsWith(href + "/")) { + if (!best || href.length > best.length) best = href; + } + } + return best; +} + export const NavigationContent = (props: NavigationProps) => { let { identity } = useIdentityData(); - - let isWriterPage = - props.currentPage === "home" || - props.currentPage === "looseleafs" || - props.currentPage === "pub"; + let pathname = usePathname(); + let activeTabHref = props.tabs ? pickActiveTabHref(pathname, props.tabs) : null; + let onWriterPage = + pathname.startsWith("/home") || + pathname.startsWith("/looseleafs") || + pathname.startsWith("/notifications"); return ( <> @@ -43,48 +55,40 @@ export const NavigationContent = (props: NavigationProps) => {
)} - {props.tabs && Object.keys(props.tabs).length > 1 && ( - <> - {Object.keys(props.tabs).map((t) => ( + + {props.tabs && + Object.entries(props.tabs).map(([name, { href, icon }]) => ( + } - label={t} + icon={icon ?? } + label={name} className={ - t === props.currentTab ? "bg-bg-page! border-border-light!" : "" + href === activeTabHref ? "bg-bg-page! border-border-light!" : "" } - onClick={() => props.onTabClick?.(t)} - onMouseEnter={() => props.onTabHover?.(t)} - onPointerDown={() => props.onTabHover?.(t)} /> - ))} - - )} + + ))} - {props.currentPage === "home" && ( + {onWriterPage && ( <>
PUBLICATIONS
- + )}
- {isWriterPage && ( - - )} - {identity?.atp_did && ( - - )} + + {identity?.atp_did && } {identity ? ( <>
diff --git a/components/ActionBar/NavigationButtons.tsx b/components/ActionBar/NavigationButtons.tsx index 987b8d6d..6e78a2b3 100644 --- a/components/ActionBar/NavigationButtons.tsx +++ b/components/ActionBar/NavigationButtons.tsx @@ -1,14 +1,14 @@ +"use client"; +import { usePathname } from "next/navigation"; import { HomeSmall } from "components/Icons/HomeSmall"; import { ActionButton } from "./ActionButton"; import { useIdentityData } from "components/IdentityProvider"; -import { PublicationButtons } from "./Publications"; import { ReaderUnreadSmall } from "components/Icons/ReaderSmall"; import { NotificationsReadSmall, NotificationsUnreadSmall, } from "components/Icons/NotificationSmall"; import { SpeedyLink } from "components/SpeedyLink"; -import { Popover } from "components/Popover"; import { WriterSmall } from "components/Icons/WriterSmall"; export type navPages = | "home" @@ -20,25 +20,25 @@ export type navPages = | "profile" | "discover"; -export const HomeButton = (props: { - current?: boolean; - className?: string; -}) => { +function useIsActive(href: string) { + let pathname = usePathname(); + return pathname === href || pathname.startsWith(href + "/"); +} + +export const HomeButton = (props: { className?: string }) => { + let current = useIsActive("/home"); return ( } label="Home" - className={`${props.current ? "bg-bg-page! border-border-light!" : ""} w-full! ${props.className}`} + className={`${current ? "bg-bg-page! border-border-light!" : ""} w-full! ${props.className}`} /> ); }; -export const WriterButton = (props: { - currentPubUri?: string; - compactOnMobile?: boolean; -}) => { +export const WriterButton = () => { return ( } label="Write" /> @@ -46,20 +46,23 @@ export const WriterButton = (props: { ); }; -export const ReaderButton = (props: { - subs: boolean; - compactOnMobile?: boolean; -}) => { +export const ReaderButton = (props: { subs: boolean }) => { + let current = useIsActive("/reader"); return ( - } label="Read" /> + } + label="Read" + className={current ? "bg-bg-page! border-border-light!" : ""} + /> ); }; -export function NotificationButton(props: { current?: boolean }) { +export function NotificationButton() { let { identity } = useIdentityData(); let unreads = identity?.notifications[0]?.count; + let current = useIsActive("/notifications"); return ( @@ -73,7 +76,7 @@ export function NotificationButton(props: { current?: boolean }) { ) } label="Notifications" - className={`${props.current ? "bg-bg-page! border-border-light!" : ""} ${unreads ? "text-accent-contrast!" : ""}`} + className={`${current ? "bg-bg-page! border-border-light!" : ""} ${unreads ? "text-accent-contrast!" : ""}`} /> ); diff --git a/components/ActionBar/ProfileButton.tsx b/components/ActionBar/ProfileButton.tsx index 2a75ad3c..7095c51c 100644 --- a/components/ActionBar/ProfileButton.tsx +++ b/components/ActionBar/ProfileButton.tsx @@ -1,3 +1,4 @@ +"use client"; import { Avatar } from "components/Avatar"; import { ActionButton } from "./ActionButton"; import { useIdentityData } from "components/IdentityProvider"; diff --git a/components/ActionBar/PublicationNavigation.tsx b/components/ActionBar/PublicationNavigation.tsx deleted file mode 100644 index 9eb7f088..00000000 --- a/components/ActionBar/PublicationNavigation.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; -import { useIdentityData } from "components/IdentityProvider"; -import { getBasePublicationURL } from "app/lish/createPub/getPublicationURL"; -import { - normalizePublicationRecord, - type NormalizedPublication, -} from "src/utils/normalizeRecords"; -import { SpeedyLink } from "components/SpeedyLink"; -import { Popover } from "components/Popover"; -import { ButtonPrimary } from "components/Buttons"; -import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; -import { HomeButton, type navPages } from "./NavigationButtons"; -import { HomeSmall } from "components/Icons/HomeSmall"; -import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; -import { PubIcon, PublicationButtons } from "./Publications"; -import { HomeTiny } from "components/Icons/HomeTiny"; -import { LooseleafTiny } from "components/Icons/LooseleafTiny"; -import { Separator } from "components/Layout"; -import { Menu, MenuItem } from "components/Menu"; -import { AddTiny } from "components/Icons/AddTiny"; - -export const PublicationNavigation = (props: { currentPage: navPages }) => { - let { identity } = useIdentityData(); - - if (!identity) return; - - let hasLooseleafs = !!identity?.permission_token_on_homepage.find( - (f) => - f.permission_tokens.leaflets_to_documents && - f.permission_tokens.leaflets_to_documents[0]?.document, - ); - - let pubCount = identity?.publications.length ?? 0; - let onlyOnePub = pubCount === 1 && hasLooseleafs; - let onlyLooseleafs = pubCount === 0 && hasLooseleafs; - let className = - "font-bold text-secondary flex gap-2 items-center grow min-w-0 text-sm h-[34px] px-2 accent-container"; - - // if not publications or looseleafs - if (!identity.publications && !hasLooseleafs) { - return ( - - - Create a Publication! - - - ); - } - - switch (props.currentPage) { - case "looseleafs": - case "pub": - if (onlyLooseleafs || onlyOnePub) - return ( - <> - - - Home - - - ); - break; - case "home": { - if (onlyLooseleafs || onlyOnePub) { - let pub = identity.publications[0]; - return ( -
- }> - - {}}> - - Create New Publication - - - - - {onlyLooseleafs ? ( - - Looseleafs - - ) : ( - - -
{pub.name}
-
- )} -
- ); - } - break; - } - } - - return ( - - ({ - record: normalizePublicationRecord(pub.record), - uri: pub.uri, - }))} - />{" "} - Publications -
- } - className="pt-1 px-2!" - > - -
- - - ); -}; - -function PubIcons(props: { - publications: { record: NormalizedPublication | null; uri: string }[]; -}) { - if (props.publications.length < 1) return null; - return ( -
- {props.publications.map((pub, index) => { - if (index <= 2) - return ( -
- -
- ); - })} -
- ); -} diff --git a/components/ActionBar/Publications.tsx b/components/ActionBar/Publications.tsx index 821355a8..18c150d7 100644 --- a/components/ActionBar/Publications.tsx +++ b/components/ActionBar/Publications.tsx @@ -18,13 +18,9 @@ import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { useIsInitialRender, useIsMobile } from "src/hooks/isMobile"; import { useState } from "react"; import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; -import { type navPages } from "./NavigationButtons"; import { LoginModal } from "components/LoginButton"; -export const PublicationButtons = (props: { - currentPage: navPages; - className?: string; -}) => { +export const PublicationButtons = (props: { className?: string }) => { let { identity } = useIdentityData(); let hasLooseleafs = !!identity?.permission_token_on_homepage.find( (f) => diff --git a/components/IdentityProvider.tsx b/components/IdentityProvider.tsx index b71e2313..85f159dc 100644 --- a/components/IdentityProvider.tsx +++ b/components/IdentityProvider.tsx @@ -2,7 +2,7 @@ import { getIdentityData } from "actions/getIdentityData"; import { createContext, useContext, useEffect } from "react"; import useSWR, { KeyedMutator, mutate } from "swr"; -import { DashboardState } from "./PageLayouts/DashboardLayout"; +import type { DashboardState } from "./PageLayouts/dashboardState"; import { supabaseBrowserClient } from "supabase/browserClient"; import { produce, Draft } from "immer"; diff --git a/components/LoginButton.tsx b/components/LoginButton.tsx index 308ff8ea..23ca1107 100644 --- a/components/LoginButton.tsx +++ b/components/LoginButton.tsx @@ -19,7 +19,7 @@ import { confirmEmailAuthToken, } from "actions/emailAuth"; import { loginWithEmailToken } from "actions/login"; -import { getHomeDocs } from "app/(home-pages)/home/storage"; +import { getHomeDocs } from "app/(home-pages)/(writer)/home/storage"; import { mutate } from "swr"; export const LoginModal = (props: { diff --git a/components/PageLayouts/DashboardLayout.tsx b/components/PageLayouts/DashboardLayout.tsx deleted file mode 100644 index f440da1e..00000000 --- a/components/PageLayouts/DashboardLayout.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; -import { useState, createContext, useContext } from "react"; -import { useSearchParams } from "next/navigation"; -import * as Dialog from "@radix-ui/react-dialog"; -import { navPages } from "components/ActionBar/NavigationButtons"; -import { - DesktopNavigation, - NavigationContent, -} from "components/ActionBar/DesktopNavigation"; -import { Sidebar, useSidebarStore } from "components/ActionBar/Sidebar"; -import { create } from "zustand"; -import { InterfaceState, useIdentityData } from "components/IdentityProvider"; -import { updateIdentityInterfaceState } from "actions/updateIdentityInterfaceState"; - -export type DashboardState = { - display?: "grid" | "list"; - sort?: "created" | "alphabetical"; - filter: { - drafts: boolean; - published: boolean; - docs: boolean; - archived: boolean; - }; -}; - -type DashboardStore = { - dashboards: { [id: string]: DashboardState }; - setDashboard: (id: string, partial: Partial) => void; -}; - -const defaultDashboardState: DashboardState = { - display: undefined, - sort: undefined, - filter: { - drafts: false, - published: false, - docs: false, - archived: false, - }, -}; - -export const useDashboardStore = create((set, get) => ({ - dashboards: {}, - setDashboard: (id: string, partial: Partial) => { - set((state) => ({ - dashboards: { - ...state.dashboards, - [id]: { - ...(state.dashboards[id] || defaultDashboardState), - ...partial, - }, - }, - })); - }, -})); - -export const DashboardIdContext = createContext(null); - -export const useDashboardId = () => { - const id = useContext(DashboardIdContext); - if (!id) { - throw new Error("useDashboardId must be used within a DashboardLayout"); - } - return id; -}; - -export const useDashboardState = () => { - const id = useDashboardId(); - let { identity } = useIdentityData(); - let localState = useDashboardStore( - (state) => state.dashboards[id] || defaultDashboardState, - ); - if (!identity) return localState; - let metadata = identity.interface_state as InterfaceState; - return metadata?.dashboards?.[id] || defaultDashboardState; -}; - -export const useSetDashboardState = () => { - const id = useDashboardId(); - let { identity, mutate } = useIdentityData(); - const setDashboard = useDashboardStore((state) => state.setDashboard); - return async (partial: Partial) => { - if (!identity) return setDashboard(id, partial); - - let interface_state = (identity.interface_state as InterfaceState) || {}; - let newDashboardState = { - ...defaultDashboardState, - ...interface_state.dashboards?.[id], - ...partial, - }; - mutate( - { - ...identity, - interface_state: { - ...interface_state, - dashboards: { - ...interface_state.dashboards, - [id]: newDashboardState, - }, - }, - }, - { revalidate: false }, - ); - await updateIdentityInterfaceState({ - ...interface_state, - dashboards: { - [id]: newDashboardState, - }, - }); - }; -}; - -export function DashboardLayout< - T extends { - [name: string]: { - content: React.ReactNode; - icon?: React.ReactNode; - }; - }, ->(props: { - id: string; - tabs: T; - defaultTab: keyof T; - currentPage: navPages; - publication?: string; - profileDid?: string; - actions?: React.ReactNode; - onTabHover?: (tabName: string) => void; - pageTitle: React.ReactNode; -}) { - const searchParams = useSearchParams(); - const tabParam = searchParams.get("tab"); - - const initialTab = - tabParam && props.tabs[tabParam] ? tabParam : props.defaultTab; - let [tab, setTab] = useState(initialTab); - - const setTabWithUrl = (newTab: keyof T) => { - setTab(newTab); - const params = new URLSearchParams(searchParams.toString()); - params.set("tab", newTab as string); - const newUrl = `${window.location.pathname}?${params.toString()}`; - window.history.replaceState(null, "", newUrl); - }; - - let { content } = props.tabs[tab]; - - let navigationProps = { - currentPage: props.currentPage, - publication: props.publication, - actions: props.actions, - tabs: props.tabs, - currentTab: tab as string, - onTabClick: (t: string) => setTabWithUrl(t as keyof T), - onTabHover: props.onTabHover, - pageTitle: props.pageTitle, - }; - - let { open, setOpen } = useSidebarStore(); - - return ( - -
- - - - - { - const target = e.target as HTMLElement; - const interactive = target.closest("a, button"); - if (!interactive) return; - if (interactive.getAttribute("aria-haspopup")) return; - setOpen(false); - }} - > - Navigation - - - - - - - {content} -
-
- ); -} diff --git a/components/PageLayouts/DashboardShell.tsx b/components/PageLayouts/DashboardShell.tsx new file mode 100644 index 00000000..68d17ee9 --- /dev/null +++ b/components/PageLayouts/DashboardShell.tsx @@ -0,0 +1,57 @@ +"use client"; +import * as Dialog from "@radix-ui/react-dialog"; +import { + DesktopNavigation, + NavigationContent, +} from "components/ActionBar/DesktopNavigation"; +import { Sidebar, useSidebarStore } from "components/ActionBar/Sidebar"; +import { DashboardIdContext } from "./dashboardState"; + +export type DashboardShellProps = { + id: string; + pageTitle: React.ReactNode; + publication?: string; + actions?: React.ReactNode; + tabs?: { [name: string]: { href: string; icon?: React.ReactNode } }; + children: React.ReactNode; +}; + +export function DashboardShell(props: DashboardShellProps) { + let { id, children, ...navigationProps } = props; + let { open, setOpen } = useSidebarStore(); + + return ( + +
+ + + + + { + const target = e.target as HTMLElement; + const interactive = target.closest("a, button"); + if (!interactive) return; + if (interactive.getAttribute("aria-haspopup")) return; + setOpen(false); + }} + > + Navigation + + + + + + + {children} +
+
+ ); +} diff --git a/components/PageLayouts/PageSearch.tsx b/components/PageLayouts/PageSearch.tsx index 7b8a652f..9d71291e 100644 --- a/components/PageLayouts/PageSearch.tsx +++ b/components/PageLayouts/PageSearch.tsx @@ -11,7 +11,7 @@ import { DashboardState, useDashboardState, useSetDashboardState, -} from "./DashboardLayout"; +} from "./dashboardState"; export const PageSearch = (props: { searchValue: string; diff --git a/components/PageLayouts/dashboardState.ts b/components/PageLayouts/dashboardState.ts new file mode 100644 index 00000000..b4c76eba --- /dev/null +++ b/components/PageLayouts/dashboardState.ts @@ -0,0 +1,103 @@ +"use client"; +import { createContext, useContext } from "react"; +import { create } from "zustand"; +import { InterfaceState, useIdentityData } from "components/IdentityProvider"; +import { updateIdentityInterfaceState } from "actions/updateIdentityInterfaceState"; + +export type DashboardState = { + display?: "grid" | "list"; + sort?: "created" | "alphabetical"; + filter: { + drafts: boolean; + published: boolean; + docs: boolean; + archived: boolean; + }; +}; + +type DashboardStore = { + dashboards: { [id: string]: DashboardState }; + setDashboard: (id: string, partial: Partial) => void; +}; + +const defaultDashboardState: DashboardState = { + display: undefined, + sort: undefined, + filter: { + drafts: false, + published: false, + docs: false, + archived: false, + }, +}; + +export const useDashboardStore = create((set) => ({ + dashboards: {}, + setDashboard: (id: string, partial: Partial) => { + set((state) => ({ + dashboards: { + ...state.dashboards, + [id]: { + ...(state.dashboards[id] || defaultDashboardState), + ...partial, + }, + }, + })); + }, +})); + +export const DashboardIdContext = createContext(null); + +export const useDashboardId = () => { + const id = useContext(DashboardIdContext); + if (!id) { + throw new Error("useDashboardId must be used within a DashboardLayout"); + } + return id; +}; + +export const useDashboardState = () => { + const id = useDashboardId(); + let { identity } = useIdentityData(); + let localState = useDashboardStore( + (state) => state.dashboards[id] || defaultDashboardState, + ); + if (!identity) return localState; + let metadata = identity.interface_state as InterfaceState; + return metadata?.dashboards?.[id] || defaultDashboardState; +}; + +export const useSetDashboardState = () => { + const id = useDashboardId(); + let { identity, mutate } = useIdentityData(); + const setDashboard = useDashboardStore((state) => state.setDashboard); + return async (partial: Partial) => { + if (!identity) return setDashboard(id, partial); + + let interface_state = (identity.interface_state as InterfaceState) || {}; + let newDashboardState = { + ...defaultDashboardState, + ...interface_state.dashboards?.[id], + ...partial, + }; + mutate( + { + ...identity, + interface_state: { + ...interface_state, + dashboards: { + ...interface_state.dashboards, + [id]: newDashboardState, + }, + }, + }, + { revalidate: false }, + ); + await updateIdentityInterfaceState({ + ...interface_state, + dashboards: { + [id]: newDashboardState, + }, + }); + }; +}; diff --git a/components/utils/AddLeafletToHomepage.tsx b/components/utils/AddLeafletToHomepage.tsx index 54ec4350..e437b971 100644 --- a/components/utils/AddLeafletToHomepage.tsx +++ b/components/utils/AddLeafletToHomepage.tsx @@ -1,6 +1,6 @@ "use client"; -import { addDocToHome } from "app/(home-pages)/home/storage"; +import { addDocToHome } from "app/(home-pages)/(writer)/home/storage"; import { useIdentityData } from "components/IdentityProvider"; import { useEffect } from "react"; import { useReplicache } from "src/replicache"; -- 2.51.2