From 303000ca107c0447ae1c8ba98acfdfb604bf5397 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 20 May 2025 18:08:58 +0000 Subject: [PATCH] add subdomain to publications --- middleware.ts | 2 +- actions/emailAuth.ts | 8 ++------ drizzle/relations.ts | 27 ++++++++++++++++++++------- drizzle/schema.ts | 11 +++++++++++ src/auth.ts | 13 +++++++++++++ supabase/database.types.ts | 33 +++++++++++++++++++++++++++++++++ app/[leaflet_id]/Actions.tsx | 7 +++++-- app/home/Publications.tsx | 13 +++++++++++-- app/lish/PostList.tsx | 15 ++++++++++++--- components/Pages/PublicationMetadata.tsx | 7 ++++--- components/ShareOptions/index.tsx | 2 +- src/utils/isProductionDeployment.ts | 7 +++++++ supabase/migrations/20250520190442_add_publication_domains_table.sql | 33 +++++++++++++++++++++++++++++++++ app/lish/createPub/CreatePubForm.tsx | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- app/lish/createPub/createPublication.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++------- app/lish/createPub/getPublicationURL.ts | 17 +++++++++++++++++ app/lish/createPub/page.tsx | 18 +++++++++++------- app/api/oauth/[route]/route.ts | 11 +++-------- app/lish/[did]/[publication]/page.tsx | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[handle]/[publication]/page.tsx | 133 ------------------------------------------------------------------------------------------------------------------------------------- app/lish/[did]/[publication]/[rkey]/page.tsx | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/Actions.tsx | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/DraftList.tsx | 43 +++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx | 44 ++++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx | 41 +++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[did]/[publication]/dashboard/page.tsx | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/lish/[handle]/[publication]/[rkey]/page.tsx | 136 ---------------------------------------------------------------------------------------------------------------------------------------- app/lish/[handle]/[publication]/dashboard/Actions.tsx | 97 ------------------------------------------------------------------------------------------------- app/lish/[handle]/[publication]/dashboard/DraftList.tsx | 43 ------------------------------------------- app/lish/[handle]/[publication]/dashboard/NewDraftButton.tsx | 44 -------------------------------------------- app/lish/[handle]/[publication]/dashboard/PublicationDashboard.tsx | 58 ---------------------------------------------------------- app/lish/[handle]/[publication]/dashboard/PublicationSWRProvider.tsx | 41 ----------------------------------------- app/lish/[handle]/[publication]/dashboard/PublishedPostsLists.tsx | 71 ----------------------------------------------------------------------- app/lish/[handle]/[publication]/dashboard/page.tsx | 107 ----------------------------------------------------------------------------------------------------------- 36 file(s) changed, 1023 insertion(s)(+), 800 deletion(s)(-) diff --git a/middleware.ts b/middleware.ts --- a/middleware.ts +++ b/middleware.ts @@ -25,7 +25,7 @@ if (req.nextUrl.pathname === "/not-found") return; let { data: routes } = await supabase .from("custom_domains") - .select("*, custom_domain_routes(*)") + .select("*, custom_domain_routes(*), publication_domains(*)") .eq("domain", hostname) .single(); if (routes) { diff --git a/actions/emailAuth.ts b/actions/emailAuth.ts --- a/actions/emailAuth.ts +++ b/actions/emailAuth.ts @@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm"; import { cookies } from "next/headers"; import { createIdentity } from "./createIdentity"; +import { setAuthToken } from "src/auth"; async function sendAuthCode(email: string, code: string) { if (process.env.NODE_ENV === "development") { @@ -136,12 +137,7 @@ ) .returning(); - (await cookies()).set("auth_token", confirmedToken.id, { - maxAge: 60 * 60 * 24 * 365, - secure: process.env.NODE_ENV === "production", - httpOnly: true, - sameSite: "lax", - }); + await setAuthToken(confirmedToken.id); client.end(); return confirmedToken; diff --git a/drizzle/relations.ts b/drizzle/relations.ts --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm/relations"; -import { entities, facts, entity_sets, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, poll_votes_on_entity, subscribers_to_publications, publications, permission_token_on_homepage, documents, documents_in_publications, leaflets_in_publications, permission_token_rights } from "./schema"; +import { entities, facts, entity_sets, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, poll_votes_on_entity, publication_domains, publications, subscribers_to_publications, permission_token_on_homepage, documents, documents_in_publications, leaflets_in_publications, permission_token_rights } from "./schema"; export const factsRelations = relations(facts, ({one}) => ({ entity: one(entities, { @@ -107,6 +107,7 @@ fields: [custom_domains.identity], references: [identities.email] }), + publication_domains: many(publication_domains), })); export const poll_votes_on_entityRelations = relations(poll_votes_on_entity, ({one}) => ({ @@ -122,6 +123,24 @@ }), })); +export const publication_domainsRelations = relations(publication_domains, ({one}) => ({ + custom_domain: one(custom_domains, { + fields: [publication_domains.domain], + references: [custom_domains.domain] + }), + publication: one(publications, { + fields: [publication_domains.publication], + references: [publications.uri] + }), +})); + +export const publicationsRelations = relations(publications, ({many}) => ({ + publication_domains: many(publication_domains), + subscribers_to_publications: many(subscribers_to_publications), + documents_in_publications: many(documents_in_publications), + leaflets_in_publications: many(leaflets_in_publications), +})); + export const subscribers_to_publicationsRelations = relations(subscribers_to_publications, ({one}) => ({ identity: one(identities, { fields: [subscribers_to_publications.identity], @@ -131,12 +150,6 @@ fields: [subscribers_to_publications.publication], references: [publications.uri] }), -})); - -export const publicationsRelations = relations(publications, ({many}) => ({ - subscribers_to_publications: many(subscribers_to_publications), - documents_in_publications: many(documents_in_publications), - leaflets_in_publications: many(leaflets_in_publications), })); export const permission_token_on_homepageRelations = relations(permission_token_on_homepage, ({one}) => ({ diff --git a/drizzle/schema.ts b/drizzle/schema.ts --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -160,6 +160,17 @@ voter_token: uuid("voter_token").notNull(), }); +export const publication_domains = pgTable("publication_domains", { + publication: text("publication").notNull().references(() => publications.uri, { onDelete: "cascade" } ), + domain: text("domain").notNull().references(() => custom_domains.domain, { onDelete: "cascade" } ), + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), +}, +(table) => { + return { + publication_domains_pkey: primaryKey({ columns: [table.publication, table.domain], name: "publication_domains_pkey"}), + } +}); + export const subscribers_to_publications = pgTable("subscribers_to_publications", { identity: text("identity").notNull().references(() => identities.email, { onUpdate: "cascade" } ), publication: text("publication").notNull().references(() => publications.uri), diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,13 @@ +import { cookies } from "next/headers"; +import { isProductionDomain } from "./utils/isProductionDeployment"; + +export async function setAuthToken(tokenID: string) { + let c = await cookies(); + c.set("auth_token", tokenID, { + maxAge: 60 * 60 * 24 * 365, + secure: process.env.NODE_ENV === "production", + domain: isProductionDomain() ? "leaflet.pub" : undefined, + httpOnly: true, + sameSite: "lax", + }); +} diff --git a/supabase/database.types.ts b/supabase/database.types.ts --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -641,6 +641,39 @@ }, ] } + publication_domains: { + Row: { + created_at: string + domain: string + publication: string + } + Insert: { + created_at?: string + domain: string + publication: string + } + Update: { + created_at?: string + domain?: string + publication?: string + } + Relationships: [ + { + foreignKeyName: "publication_domains_domain_fkey" + columns: ["domain"] + isOneToOne: false + referencedRelation: "custom_domains" + referencedColumns: ["domain"] + }, + { + foreignKeyName: "publication_domains_publication_fkey" + columns: ["publication"] + isOneToOne: false + referencedRelation: "publications" + referencedColumns: ["uri"] + }, + ] + } publications: { Row: { identity_did: string diff --git a/app/[leaflet_id]/Actions.tsx b/app/[leaflet_id]/Actions.tsx --- a/app/[leaflet_id]/Actions.tsx +++ b/app/[leaflet_id]/Actions.tsx @@ -1,4 +1,5 @@ import { publishToPublication } from "actions/publishToPublication"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; import { ActionButton } from "components/ActionBar/ActionButton"; import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { GoBackSmall } from "components/Icons/GoBackSmall"; @@ -11,11 +12,13 @@ import { useParams } from "next/navigation"; import { useBlocks } from "src/hooks/queries/useBlocks"; import { useEntity, useReplicache } from "src/replicache"; +import { Json } from "supabase/database.types"; export const BackToPubButton = (props: { publication: { identity_did: string; indexed_at: string; name: string; + record: Json; uri: string; }; }) => { @@ -25,7 +28,7 @@ let name = props.publication.name; return ( {pub.doc ? "Updated! " : "Published! "} link diff --git a/app/home/Publications.tsx b/app/home/Publications.tsx --- a/app/home/Publications.tsx +++ b/app/home/Publications.tsx @@ -6,6 +6,8 @@ import { theme } from "tailwind.config"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { AddTiny } from "components/Icons/AddTiny"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { Json } from "supabase/database.types"; export const MyPublicationList = () => { let { identity } = useIdentityData(); @@ -31,6 +33,7 @@ identity_did: string; indexed_at: string; name: string; + record: Json; uri: string; }[]; }) => { @@ -43,17 +46,23 @@ {...d} key={d.uri} handle={identity?.resolved_did?.alsoKnownAs?.[0].slice(5)!} + record={d.record} /> ))} ); }; -function Publication(props: { uri: string; name: string; handle: string }) { +function Publication(props: { + uri: string; + name: string; + handle: string; + record: Json; +}) { return (

{props.name}

diff --git a/app/lish/PostList.tsx b/app/lish/PostList.tsx --- a/app/lish/PostList.tsx +++ b/app/lish/PostList.tsx @@ -5,9 +5,11 @@ import { useIdentityData } from "components/IdentityProvider"; import { useParams } from "next/navigation"; import { AtUri } from "@atproto/syntax"; +import { getPublicationURL } from "./createPub/getPublicationURL"; export const PostList = (props: { isFeed?: boolean; + publication: { uri: string; record: Json; name: string }; posts: { documents: { data: Json; @@ -38,7 +40,13 @@ let uri = new AtUri(post.documents?.uri!); return ( - + ); })}
@@ -47,6 +55,7 @@ const PostListItem = ( props: { + publication_data: { uri: string; record: Json; name: string }; isFeed?: boolean; uri: AtUri; } & PubLeafletDocument.Record, @@ -57,7 +66,7 @@
{props.isFeed && ( {props.publication} @@ -65,7 +74,7 @@ )}

{props.title}

diff --git a/components/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -11,6 +11,7 @@ import { AtUri } from "@atproto/syntax"; import { PubLeafletDocument } from "lexicons/api"; import { publications } from "drizzle/schema"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; export const PublicationMetadata = ({ cardBorderHidden, }: { @@ -24,7 +25,7 @@ let [descriptionState, setDescriptionState] = useState( pub?.description || "", ); - let record = pub.documents?.data as PubLeafletDocument.Record | null; + let record = pub?.documents?.data as PubLeafletDocument.Record | null; let publishedAt = record?.publishedAt; useEffect(() => { @@ -55,7 +56,7 @@ >
{pub.publications?.name} @@ -98,7 +99,7 @@ View Post diff --git a/components/ShareOptions/index.tsx b/components/ShareOptions/index.tsx --- a/components/ShareOptions/index.tsx +++ b/components/ShareOptions/index.tsx @@ -57,7 +57,7 @@ icon= primary={!!!pub} secondary={!!pub} - label={`Share ${pub && "Draft"}`} + label={`Share ${pub ? "Draft" : ""}`} /> } > diff --git a/src/utils/isProductionDeployment.ts b/src/utils/isProductionDeployment.ts new file mode 100644 --- /dev/null +++ b/src/utils/isProductionDeployment.ts @@ -0,0 +1,7 @@ +export function isProductionDomain() { + let url = + process.env.NEXT_PUBLIC_VERCEL_URL || + process.env.VERCEL_URL || + "http://localhost:3000"; + return process.env.NODE_ENV === "production" && url.includes("leaflet.pub"); +} diff --git a/supabase/migrations/20250520190442_add_publication_domains_table.sql b/supabase/migrations/20250520190442_add_publication_domains_table.sql new file mode 100644 --- /dev/null +++ b/supabase/migrations/20250520190442_add_publication_domains_table.sql @@ -0,0 +1,33 @@ +create table "public"."publication_domains" ( + "publication" text not null, + "domain" text not null, + "created_at" timestamp with time zone not null default now() +); +alter table "public"."publication_domains" enable row level security; +CREATE UNIQUE INDEX publication_domains_pkey ON public.publication_domains USING btree (publication, domain); +alter table "public"."publication_domains" add constraint "publication_domains_pkey" PRIMARY KEY using index "publication_domains_pkey"; +alter table "public"."publication_domains" add constraint "publication_domains_domain_fkey" FOREIGN KEY (domain) REFERENCES custom_domains(domain) ON DELETE CASCADE not valid; +alter table "public"."publication_domains" validate constraint "publication_domains_domain_fkey"; +alter table "public"."publication_domains" add constraint "publication_domains_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid; +alter table "public"."publication_domains" validate constraint "publication_domains_publication_fkey"; +grant delete on table "public"."publication_domains" to "anon"; +grant insert on table "public"."publication_domains" to "anon"; +grant references on table "public"."publication_domains" to "anon"; +grant select on table "public"."publication_domains" to "anon"; +grant trigger on table "public"."publication_domains" to "anon"; +grant truncate on table "public"."publication_domains" to "anon"; +grant update on table "public"."publication_domains" to "anon"; +grant delete on table "public"."publication_domains" to "authenticated"; +grant insert on table "public"."publication_domains" to "authenticated"; +grant references on table "public"."publication_domains" to "authenticated"; +grant select on table "public"."publication_domains" to "authenticated"; +grant trigger on table "public"."publication_domains" to "authenticated"; +grant truncate on table "public"."publication_domains" to "authenticated"; +grant update on table "public"."publication_domains" to "authenticated"; +grant delete on table "public"."publication_domains" to "service_role"; +grant insert on table "public"."publication_domains" to "service_role"; +grant references on table "public"."publication_domains" to "service_role"; +grant select on table "public"."publication_domains" to "service_role"; +grant trigger on table "public"."publication_domains" to "service_role"; +grant truncate on table "public"."publication_domains" to "service_role"; +grant update on table "public"."publication_domains" to "service_role"; diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx --- a/app/lish/createPub/CreatePubForm.tsx +++ b/app/lish/createPub/CreatePubForm.tsx @@ -8,8 +8,9 @@ import { useRouter } from "next/navigation"; import { useState, useRef, useEffect } from "react"; import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; -import { set } from "colorjs.io/fn"; import { theme } from "tailwind.config"; +import { getPublicationURL } from "./getPublicationURL"; +import { string } from "zod"; export const CreatePubForm = () => { let [nameValue, setNameValue] = useState(""); @@ -20,21 +21,20 @@ let fileInputRef = useRef(null); let router = useRouter(); - let { identity } = useIdentityData(); return (
{ e.preventDefault(); - // Note: You'll need to update the createPublication function to handle the logo file - await createPublication({ + if (!subdomainValidator.safeParse(domainValue).success) return; + let data = await createPublication({ name: nameValue, description: descriptionValue, iconFile: logoFile, + subdomain: domainValue, }); - router.push( - `/lish/${identity?.resolved_did?.alsoKnownAs?.[0].slice(5)}/${nameValue}/dashboard`, - ); + if (data?.publication) + router.push(`${getPublicationURL(data.publication)}/dashboard`); }} >
@@ -103,39 +103,72 @@ ); }; +let subdomainValidator = string() + .min(3) + .max(63) + .regex(/^[a-z0-9-]+$/); function DomainInput(props: { domain: string; setDomain: (d: string) => void; }) { - let [state, setState] = useState<"empty" | "valid" | "invalid" | "pending">( - "empty", - ); + type DomainState = + | { status: "empty" } + | { status: "valid" } + | { status: "invalid" } + | { status: "pending" } + | { status: "error"; message: string }; + + let [state, setState] = useState({ status: "empty" }); + useEffect(() => { if (!props.domain) { - setState("empty"); + setState({ status: "empty" }); } else { - setState("pending"); + let valid = subdomainValidator.safeParse(props.domain); + if (!valid.success) { + let reason = valid.error.errors[0].code; + setState({ + status: "error", + message: + reason === "too_small" + ? "Must be at least 3 characters long" + : reason === "invalid_string" + ? "Must contain only lowercase letters, numbers, and dashes" + : "", + }); + return; + } + setState({ status: "pending" }); } }, [props.domain]); + useDebouncedEffect( async () => { - if (!props.domain) return setState("empty"); + if (!props.domain) return setState({ status: "empty" }); + + let valid = subdomainValidator.safeParse(props.domain); + if (!valid.success) { + return; + } let status = await callRPC("get_leaflet_subdomain_status", { domain: props.domain, }); console.log(status); - if (status.error === "Not Found") setState("valid"); - else setState("invalid"); + if (status.error === "Not Found") setState({ status: "valid" }); + else setState({ status: "invalid" }); }, 500, [props.domain], ); + return (
); diff --git a/app/lish/createPub/createPublication.ts b/app/lish/createPub/createPublication.ts --- a/app/lish/createPub/createPublication.ts +++ b/app/lish/createPub/createPublication.ts @@ -6,25 +6,46 @@ import { supabaseServerClient } from "supabase/serverClient"; import { Un$Typed } from "@atproto/api"; import { Json } from "supabase/database.types"; +import { Vercel } from "@vercel/sdk"; +import { isProductionDomain } from "src/utils/isProductionDeployment"; +import { string } from "zod"; +const VERCEL_TOKEN = process.env.VERCEL_TOKEN; +const vercel = new Vercel({ + bearerToken: VERCEL_TOKEN, +}); +let subdomainValidator = string() + .min(3) + .max(63) + .regex(/^[a-z0-9-]+$/); export async function createPublication({ name, description, iconFile, + subdomain, }: { name: string; description: string; iconFile: File | null; + subdomain: string; }) { + let isSubdomainValid = subdomainValidator.safeParse(subdomain); + if (!isSubdomainValid.success) { + return { success: false }; + } const oauthClient = await createOauthClient(); let identity = await getIdentityData(); if (!identity || !identity.atp_did) return; + + let domain = `${subdomain}.leaflet.pub`; + let credentialSession = await oauthClient.restore(identity.atp_did); let agent = new AtpBaseClient( credentialSession.fetchHandler.bind(credentialSession), ); let record: Un$Typed = { name, + base_path: domain, }; if (description) { @@ -50,12 +71,34 @@ ); //optimistically write to our db! - await supabaseServerClient.from("publications").upsert({ - uri: result.uri, - identity_did: credentialSession.did!, - name: record.name, - record: record as Json, - }); + let { data: publication } = await supabaseServerClient + .from("publications") + .upsert({ + uri: result.uri, + identity_did: credentialSession.did!, + name: record.name, + record: record as Json, + }) + .select() + .single(); - return { success: true, name }; + // Create the custom domain + if (isProductionDomain()) { + console.log("Creating domain! " + domain); + await vercel.projects.addProjectDomain({ + idOrName: "prj_9jX4tmYCISnm176frFxk07fF74kG", + teamId: "team_42xaJiZMTw9Sr7i0DcLTae9d", + requestBody: { + name: domain + ".leaflet.pub", + }, + }); + } + await supabaseServerClient + .from("custom_domains") + .insert({ domain, identity: identity.id, confirmed: true }); + await supabaseServerClient + .from("publication_domains") + .insert({ domain, publication: result.uri }); + + return { success: true, publication }; } diff --git a/app/lish/createPub/getPublicationURL.ts b/app/lish/createPub/getPublicationURL.ts new file mode 100644 --- /dev/null +++ b/app/lish/createPub/getPublicationURL.ts @@ -0,0 +1,17 @@ +import { AtUri } from "@atproto/syntax"; +import { PubLeafletPublication } from "lexicons/api"; +import { isProductionDomain } from "src/utils/isProductionDeployment"; +import { Json } from "supabase/database.types"; + +export function getPublicationURL(pub: { + uri: string; + name: string; + record: Json; +}) { + let record = pub.record as PubLeafletPublication.Record; + if (isProductionDomain() && record?.base_path) { + return new URL(record.base_path); + } + let aturi = new AtUri(pub.uri); + return `/lish/${aturi.host}/${record?.name || pub.name}`; +} diff --git a/app/lish/createPub/page.tsx b/app/lish/createPub/page.tsx --- a/app/lish/createPub/page.tsx +++ b/app/lish/createPub/page.tsx @@ -1,16 +1,20 @@ +import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; import { CreatePubForm } from "./CreatePubForm"; export default async function CreatePub() { return ( -
-
-
-

Create Your Publication!

-
- + // Eventually this can pull from home theme? + +
+
+
+

Create Your Publication!

+
+ +
-
+ ); } diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -6,6 +6,7 @@ import { NextRequest, NextResponse } from "next/server"; import postgres from "postgres"; import { createOauthClient } from "src/atproto-oauth"; +import { setAuthToken } from "src/auth"; import { supabaseServerClient } from "supabase/serverClient"; @@ -14,7 +15,7 @@ }; export async function GET( req: NextRequest, - props: { params: Promise<{ route: string; handle?: string }> } + props: { params: Promise<{ route: string; handle?: string }> }, ) { const params = await props.params; let client = await createOauthClient(); @@ -89,13 +90,7 @@ .select() .single(); - if (token) - (await cookies()).set("auth_token", token.id, { - maxAge: 60 * 60 * 24 * 365, - secure: process.env.NODE_ENV === "production", - httpOnly: true, - sameSite: "lax", - }); + if (token) await setAuthToken(token.id); // Process successful authentication here console.log("authorize() was called with state:", state); diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/page.tsx @@ -0,0 +1,131 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import { Metadata } from "next"; + +import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; +import React from "react"; +import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; +import { AtUri } from "@atproto/syntax"; +import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; +import Link from "next/link"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; + +export async function generateMetadata(props: { + params: Promise<{ publication: string; did: string }>; +}): Promise { + let params = await props.params; + let did = decodeURIComponent(params.did); + if (!did) return { title: "Publication 404" }; + + let { result: publication } = await get_publication_data.handler( + { + did, + publication_name: decodeURIComponent(params.publication), + }, + { supabase: supabaseServerClient }, + ); + if (!publication) return { title: "404 Publication" }; + return { title: decodeURIComponent(params.publication) }; +} + +export default async function Publication(props: { + params: Promise<{ publication: string; did: string }>; +}) { + let params = await props.params; + let did = decodeURIComponent(params.did); + if (!did) return ; + let { data: publication } = await supabaseServerClient + .from("publications") + .select( + `*, + documents_in_publications(documents(*)) + `, + ) + .eq("identity_did", did) + .eq("name", decodeURIComponent(params.publication)) + .single(); + + let record = publication?.record as PubLeafletPublication.Record; + + if (!publication) return ; + try { + return ( + +
+
+
+
+ {record.icon && ( +
+ )} +

{publication.name}

+
+

{record.description}

+
+
+ {publication.documents_in_publications + .filter((d) => !!d?.documents) + .sort((a, b) => { + let aRecord = a.documents?.data! as PubLeafletDocument.Record; + let bRecord = a.documents?.data! as PubLeafletDocument.Record; + const aDate = aRecord.publishedAt + ? new Date(aRecord.publishedAt) + : new Date(0); + const bDate = bRecord.publishedAt + ? new Date(bRecord.publishedAt) + : new Date(0); + return bDate.getTime() - aDate.getTime(); // Sort by most recent first + }) + .map((doc) => { + if (!doc.documents) return null; + let uri = new AtUri(doc.documents.uri); + let record = doc.documents.data as PubLeafletDocument.Record; + return ( + +
+ +

{record.title}

+

+ {record.description} +

+

+ {record.publishedAt && + new Date(record.publishedAt).toLocaleDateString( + undefined, + { + year: "numeric", + month: "long", + day: "2-digit", + }, + )}{" "} +

+ +
+
+
+ ); + })} +
+
+
+ + ); + } catch (e) { + console.log(e); + return
{JSON.stringify(e, undefined, 2)}
; + } +} + +const PubNotFound = () => { + return
ain't no pub here
; +}; diff --git a/app/lish/[handle]/[publication]/page.tsx b/app/lish/[handle]/[publication]/page.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/page.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { IdResolver } from "@atproto/identity"; -import { supabaseServerClient } from "supabase/serverClient"; -import { Metadata } from "next"; - -import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; -import React from "react"; -import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; -import { AtUri } from "@atproto/syntax"; -import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; -import Link from "next/link"; - -const idResolver = new IdResolver(); - -export async function generateMetadata(props: { - params: Promise<{ publication: string; handle: string }>; -}): Promise { - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return { title: "Publication 404" }; - - let { result: publication } = await get_publication_data.handler( - { - did, - publication_name: decodeURIComponent((await props.params).publication), - }, - { supabase: supabaseServerClient }, - ); - if (!publication) return { title: "404 Publication" }; - return { title: decodeURIComponent((await props.params).publication) }; -} - -export default async function Publication(props: { - params: Promise<{ publication: string; handle: string }>; -}) { - let params = await props.params; - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return ; - let { data: publication } = await supabaseServerClient - .from("publications") - .select( - `*, - documents_in_publications(documents(*)) - `, - ) - .eq("identity_did", did) - .eq("name", decodeURIComponent((await props.params).publication)) - .single(); - - let record = publication?.record as PubLeafletPublication.Record; - - if (!publication) return ; - console.log(record.icon); - try { - return ( - -
-
-
-
- {record.icon && ( -
- )} -

{publication.name}

-
-

{record.description}

-
-
- {publication.documents_in_publications - .filter((d) => !!d?.documents) - .sort((a, b) => { - let aRecord = a.documents?.data! as PubLeafletDocument.Record; - let bRecord = a.documents?.data! as PubLeafletDocument.Record; - const aDate = aRecord.publishedAt - ? new Date(aRecord.publishedAt) - : new Date(0); - const bDate = bRecord.publishedAt - ? new Date(bRecord.publishedAt) - : new Date(0); - return bDate.getTime() - aDate.getTime(); // Sort by most recent first - }) - .map((doc) => { - if (!doc.documents) return null; - let uri = new AtUri(doc.documents.uri); - let record = doc.documents.data as PubLeafletDocument.Record; - return ( - -
- -

{record.title}

-

- {record.description} -

-

- {record.publishedAt && - new Date(record.publishedAt).toLocaleDateString( - undefined, - { - year: "numeric", - month: "long", - day: "2-digit", - }, - )}{" "} -

- -
-
-
- ); - })} -
-
-
- - ); - } catch (e) { - console.log(e); - return
{JSON.stringify(e, undefined, 2)}
; - } -} - -const PubNotFound = () => { - return
ain't no pub here
; -}; diff --git a/app/lish/[did]/[publication]/[rkey]/page.tsx b/app/lish/[did]/[publication]/[rkey]/page.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/page.tsx @@ -0,0 +1,138 @@ +import Link from "next/link"; +import { supabaseServerClient } from "supabase/serverClient"; +import { AtUri } from "@atproto/syntax"; +import { ids } from "lexicons/api/lexicons"; +import { + PubLeafletBlocksHeader, + PubLeafletBlocksImage, + PubLeafletBlocksText, + PubLeafletDocument, + PubLeafletPagesLinearDocument, +} from "lexicons/api"; +import { Metadata } from "next"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; + +export async function generateMetadata(props: { + params: Promise<{ publication: string; did: string; rkey: string }>; +}): Promise { + let did = decodeURIComponent((await props.params).did); + if (!did) return { title: "Publication 404" }; + + let { data: document } = await supabaseServerClient + .from("documents") + .select("*") + .eq( + "uri", + AtUri.make(did, ids.PubLeafletDocument, (await props.params).rkey), + ) + .single(); + + if (!document) return { title: "404" }; + let record = document.data as PubLeafletDocument.Record; + return { + title: + record.title + + " - " + + decodeURIComponent((await props.params).publication), + }; +} +export default async function Post(props: { + params: Promise<{ publication: string; did: string; rkey: string }>; +}) { + let did = decodeURIComponent((await props.params).did); + if (!did) return
can't resolve handle
; + let { data: document } = await supabaseServerClient + .from("documents") + .select("*, documents_in_publications(publications(*))") + .eq( + "uri", + AtUri.make(did, ids.PubLeafletDocument, (await props.params).rkey), + ) + .single(); + if (!document?.data || !document.documents_in_publications[0].publications) + return
notfound
; + let record = document.data as PubLeafletDocument.Record; + let firstPage = record.pages[0]; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } + return ( +
+
+
+
+ + {decodeURIComponent((await props.params).publication)} + +

{record.title}

+ {record.description ? ( +

{record.description}

+ ) : null} + {record.publishedAt ? ( +

+ Published{" "} + {new Date(record.publishedAt).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "2-digit", + })} +

+ ) : null} +
+ {blocks.map((b, index) => { + switch (true) { + case PubLeafletBlocksImage.isMain(b.block): { + return ( + + ); + } + case PubLeafletBlocksText.isMain(b.block): + return ( +

+ {b.block.plaintext} +

+ ); + case PubLeafletBlocksHeader.isMain(b.block): { + if (b.block.level === 1) + return ( +

+ {b.block.plaintext} +

+ ); + if (b.block.level === 2) + return ( +

+ {b.block.plaintext} +

+ ); + if (b.block.level === 3) + return ( +

+ {b.block.plaintext} +

+ ); + // if (b.block.level === 4) return

{b.block.plaintext}

; + // if (b.block.level === 5) return
{b.block.plaintext}
; + return
{b.block.plaintext}
; + } + default: + return null; + } + })} +
+
+
+ ); +} diff --git a/app/lish/[did]/[publication]/dashboard/Actions.tsx b/app/lish/[did]/[publication]/dashboard/Actions.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/Actions.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Media } from "components/Media"; +import { NewDraftActionButton } from "./NewDraftButton"; +import { ActionButton } from "components/ActionBar/ActionButton"; +import { useRouter } from "next/navigation"; +import { Popover } from "components/Popover"; +import { SettingsSmall } from "components/Icons/SettingsSmall"; +import { CreatePubForm } from "app/lish/createPub/CreatePubForm"; +import { ShareSmall } from "components/Icons/ShareSmall"; +import { Menu } from "components/Layout"; +import { MenuItem } from "components/Layout"; +import Link from "next/link"; +import { HomeSmall } from "components/Icons/HomeSmall"; + +export const Actions = (props: { publication: string }) => { + return ( + <> + + + } label="Go Home" /> + + + + + +
+ + } label="Go Home" /> + + + ); +}; + +function PublicationShareButton() { + return ( + + secondary + label="Share" + onClick={() => {}} + /> + } + > + {}}> + +
Viewer Mode
+
+ View your publication as a reader +
+ +
+ {}}> +
+
Share Your Publication
+
+ Copy link for the published site +
+
+
+
+ ); +} + +function PublicationSettingsButton(props: { publication: string }) { + let router = useRouter(); + + return ( + + label="Settings" + /> + } + > + + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/DraftList.tsx b/app/lish/[did]/[publication]/dashboard/DraftList.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/DraftList.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Link from "next/link"; +import { NewDraftSecondaryButton } from "./NewDraftButton"; +import React from "react"; +import { usePublicationData } from "./PublicationSWRProvider"; + +export function DraftList() { + let pub_data = usePublicationData(); + if (!pub_data) return null; + return ( +
+ + {pub_data.leaflets_in_publications.map((d) => { + return ( + + +
+
+ ); + })} +
+ ); +} + +function Draft(props: { id: string; title: string; description: string }) { + return ( +
+ + {props.title ? ( +

{props.title}

+ ) : ( +

Untitled

+ )} +
{props.description}
+ +
+ ); +} diff --git a/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx b/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/NewDraftButton.tsx @@ -0,0 +1,44 @@ +"use client"; +import { createPublicationDraft } from "actions/createPublicationDraft"; +import { ActionButton } from "components/ActionBar/ActionButton"; +import { ButtonSecondary } from "components/Buttons"; +import { AddTiny } from "components/Icons/AddTiny"; +import { useRouter } from "next/navigation"; + +export function NewDraftActionButton(props: { publication: string }) { + let router = useRouter(); + + return ( + { + let newLeaflet = await createPublicationDraft(props.publication); + router.push(`/${newLeaflet}`); + }} + icon= + label="New Draft" + /> + ); +} + +export function NewDraftSecondaryButton(props: { + publication: string; + fullWidth?: boolean; +}) { + let router = useRouter(); + + return ( + { + let newLeaflet = await createPublicationDraft(props.publication); + router.push(`/${newLeaflet}`); + }} + > + + New Draft + + ); +} diff --git a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx @@ -0,0 +1,58 @@ +"use client"; +import { BlobRef } from "@atproto/lexicon"; +import { useState } from "react"; + +type Tabs = { [tabName: string]: React.ReactNode }; +export function PublicationDashboard(props: { + name: string; + tabs: T; + defaultTab: keyof T; + icon: BlobRef | null; + did: string; +}) { + let [tab, setTab] = useState(props.defaultTab); + let content = props.tabs[tab]; + + return ( +
+
+ {props.icon && ( +
+ )}{" "} +
+ {props.name} +
+
+ {Object.keys(props.tabs).map((t) => ( + setTab(t)} + /> + ))} +
+
+
{content}
+
+ ); +} + +function Tab(props: { name: string; selected: boolean; onSelect: () => void }) { + return ( +
props.onSelect()} + > + {props.name} +
+ ); +} diff --git a/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx b/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx @@ -0,0 +1,41 @@ +"use client"; + +import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; +import { callRPC } from "app/api/rpc/client"; +import { createContext, useContext } from "react"; +import useSWR, { SWRConfig } from "swr"; + +const PublicationContext = createContext({ name: "", did: "" }); +export function PublicationSWRDataProvider(props: { + publication_name: string; + publication_did: string; + publication_data: GetPublicationDataReturnType["result"]; + children: React.ReactNode; +}) { + return ( + + + {props.children} + + + ); +} + +export function usePublicationData() { + let { name, did } = useContext(PublicationContext); + let { data } = useSWR( + "publication-data", + async () => + (await callRPC("get_publication_data", { publication_name: name, did })) + ?.result, + ); + return data; +} diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx @@ -0,0 +1,72 @@ +"use client"; +import Link from "next/link"; +import { AtUri } from "@atproto/syntax"; +import { PubLeafletDocument } from "lexicons/api"; +import { EditTiny } from "components/Icons/EditTiny"; +import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; +import { Menu, MenuItem } from "components/Layout"; + +import { usePublicationData } from "./PublicationSWRProvider"; +import { Fragment } from "react"; +import { useParams } from "next/navigation"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; + +export function PublishedPostsList() { + let publication = usePublicationData(); + let params = useParams(); + if (!publication) return null; + if (publication.documents_in_publications.length === 0) + return ( +
+ Nothing's been published yet... +
+ ); + return ( +
+ {publication.documents_in_publications.map((doc) => { + if (!doc.documents) return null; + let leaflet = publication.leaflets_in_publications.find( + (l) => doc.documents && l.doc === doc.documents.uri, + ); + let uri = new AtUri(doc.documents.uri); + let record = doc.documents.data as PubLeafletDocument.Record; + + return ( + +
+ +

{record.title}

+ {record.description ? ( +

{record.description}

+ ) : null} + {record.publishedAt ? ( +

+ Published{" "} + {new Date(record.publishedAt).toLocaleDateString( + undefined, + { + year: "numeric", + month: "long", + day: "2-digit", + }, + )} +

+ ) : null} + + {leaflet && ( + + + + )} +
+
+
+ ); + })} +
+ ); +} diff --git a/app/lish/[did]/[publication]/dashboard/page.tsx b/app/lish/[did]/[publication]/dashboard/page.tsx new file mode 100644 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/page.tsx @@ -0,0 +1,107 @@ +import { IdResolver } from "@atproto/identity"; +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 { ThemeProvider } from "components/ThemeManager/ThemeProvider"; +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 } from "lexicons/api"; + +const idResolver = new IdResolver(); + +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 } = await get_publication_data.handler( + { + did, + publication_name: decodeURIComponent((await props.params).publication), + }, + { supabase: supabaseServerClient }, + ); + if (!publication) return { title: "404 Publication" }; + return { title: decodeURIComponent((await props.params).publication) }; +} + +//This is the admin dashboard of the publication +export default async function Publication(props: { + params: Promise<{ publication: string; did: string }>; +}) { + let params = await props.params; + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) return
not logged in
; + let did = decodeURIComponent(params.did); + if (!did) return ; + let { result: publication } = await get_publication_data.handler( + { + did, + publication_name: decodeURIComponent((await props.params).publication), + }, + { supabase: supabaseServerClient }, + ); + + let record = publication?.record as PubLeafletPublication.Record | null; + if (!publication || identity.atp_did !== publication.identity_did) + return ; + + try { + return ( + + +
+
+
+ + + +
+
+ , + Published: , + }} + defaultTab={"Drafts"} + /> +
+ +
+ +
+
+
+
+
+
+ ); + } catch (e) { + console.log(e); + return
{JSON.stringify(e, undefined, 2)}
; + } +} + +const PubNotFound = () => { + return
ain't no pub here
; +}; diff --git a/app/lish/[handle]/[publication]/[rkey]/page.tsx b/app/lish/[handle]/[publication]/[rkey]/page.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/[rkey]/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import Link from "next/link"; -import { IdResolver } from "@atproto/identity"; -import { supabaseServerClient } from "supabase/serverClient"; -import { AtUri } from "@atproto/syntax"; -import { ids } from "lexicons/api/lexicons"; -import { - PubLeafletBlocksHeader, - PubLeafletBlocksImage, - PubLeafletBlocksText, - PubLeafletDocument, - PubLeafletPagesLinearDocument, -} from "lexicons/api"; -import { Metadata } from "next"; - -const idResolver = new IdResolver(); -export async function generateMetadata(props: { - params: Promise<{ publication: string; handle: string; rkey: string }>; -}): Promise { - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return { title: "Publication 404" }; - - let { data: document } = await supabaseServerClient - .from("documents") - .select("*") - .eq( - "uri", - AtUri.make(did, ids.PubLeafletDocument, (await props.params).rkey), - ) - .single(); - - if (!document) return { title: "404" }; - let record = document.data as PubLeafletDocument.Record; - return { - title: - record.title + - " - " + - decodeURIComponent((await props.params).publication), - }; -} -export default async function Post(props: { - params: Promise<{ publication: string; handle: string; rkey: string }>; -}) { - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return
can't resolve handle
; - let { data: document } = await supabaseServerClient - .from("documents") - .select("*") - .eq( - "uri", - AtUri.make(did, ids.PubLeafletDocument, (await props.params).rkey), - ) - .single(); - if (!document?.data) return
notfound
; - let record = document.data as PubLeafletDocument.Record; - let firstPage = record.pages[0]; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; - } - return ( -
-
-
-
- - {decodeURIComponent((await props.params).publication)} - -

{record.title}

- {record.description ? ( -

{record.description}

- ) : null} - {record.publishedAt ? ( -

- Published{" "} - {new Date(record.publishedAt).toLocaleDateString(undefined, { - year: "numeric", - month: "long", - day: "2-digit", - })} -

- ) : null} -
- {blocks.map((b, index) => { - switch (true) { - case PubLeafletBlocksImage.isMain(b.block): { - return ( - - ); - } - case PubLeafletBlocksText.isMain(b.block): - return ( -

- {b.block.plaintext} -

- ); - case PubLeafletBlocksHeader.isMain(b.block): { - if (b.block.level === 1) - return ( -

- {b.block.plaintext} -

- ); - if (b.block.level === 2) - return ( -

- {b.block.plaintext} -

- ); - if (b.block.level === 3) - return ( -

- {b.block.plaintext} -

- ); - // if (b.block.level === 4) return

{b.block.plaintext}

; - // if (b.block.level === 5) return
{b.block.plaintext}
; - return
{b.block.plaintext}
; - } - default: - return null; - } - })} -
-
-
- ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/Actions.tsx b/app/lish/[handle]/[publication]/dashboard/Actions.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/Actions.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; - -import { Media } from "components/Media"; -import { NewDraftActionButton } from "./NewDraftButton"; -import { ActionButton } from "components/ActionBar/ActionButton"; -import { useRouter } from "next/navigation"; -import { Popover } from "components/Popover"; -import { SettingsSmall } from "components/Icons/SettingsSmall"; -import { CreatePubForm } from "app/lish/createPub/CreatePubForm"; -import { ShareSmall } from "components/Icons/ShareSmall"; -import { Menu } from "components/Layout"; -import { MenuItem } from "components/Layout"; -import Link from "next/link"; -import { HomeSmall } from "components/Icons/HomeSmall"; - -export const Actions = (props: { publication: string }) => { - return ( - <> - - - } label="Go Home" /> - - - - - -
- - } label="Go Home" /> - - - ); -}; - -function PublicationShareButton() { - return ( - - secondary - label="Share" - onClick={() => {}} - /> - } - > - {}}> - -
Viewer Mode
-
- View your publication as a reader -
- -
- {}}> -
-
Share Your Publication
-
- Copy link for the published site -
-
-
-
- ); -} - -function PublicationSettingsButton(props: { publication: string }) { - let router = useRouter(); - - return ( - - label="Settings" - /> - } - > - - - ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/DraftList.tsx b/app/lish/[handle]/[publication]/dashboard/DraftList.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/DraftList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { NewDraftSecondaryButton } from "./NewDraftButton"; -import React from "react"; -import { usePublicationData } from "./PublicationSWRProvider"; - -export function DraftList() { - let pub_data = usePublicationData(); - if (!pub_data) return null; - return ( -
- - {pub_data.leaflets_in_publications.map((d) => { - return ( - - -
-
- ); - })} -
- ); -} - -function Draft(props: { id: string; title: string; description: string }) { - return ( -
- - {props.title ? ( -

{props.title}

- ) : ( -

Untitled

- )} -
{props.description}
- -
- ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/NewDraftButton.tsx b/app/lish/[handle]/[publication]/dashboard/NewDraftButton.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/NewDraftButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; -import { createPublicationDraft } from "actions/createPublicationDraft"; -import { ActionButton } from "components/ActionBar/ActionButton"; -import { ButtonSecondary } from "components/Buttons"; -import { AddTiny } from "components/Icons/AddTiny"; -import { useRouter } from "next/navigation"; - -export function NewDraftActionButton(props: { publication: string }) { - let router = useRouter(); - - return ( - { - let newLeaflet = await createPublicationDraft(props.publication); - router.push(`/${newLeaflet}`); - }} - icon= - label="New Draft" - /> - ); -} - -export function NewDraftSecondaryButton(props: { - publication: string; - fullWidth?: boolean; -}) { - let router = useRouter(); - - return ( - { - let newLeaflet = await createPublicationDraft(props.publication); - router.push(`/${newLeaflet}`); - }} - > - - New Draft - - ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/PublicationDashboard.tsx b/app/lish/[handle]/[publication]/dashboard/PublicationDashboard.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/PublicationDashboard.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; -import { BlobRef } from "@atproto/lexicon"; -import { useState } from "react"; - -type Tabs = { [tabName: string]: React.ReactNode }; -export function PublicationDashboard(props: { - name: string; - tabs: T; - defaultTab: keyof T; - icon: BlobRef | null; - did: string; -}) { - let [tab, setTab] = useState(props.defaultTab); - let content = props.tabs[tab]; - - return ( -
-
- {props.icon && ( -
- )}{" "} -
- {props.name} -
-
- {Object.keys(props.tabs).map((t) => ( - setTab(t)} - /> - ))} -
-
-
{content}
-
- ); -} - -function Tab(props: { name: string; selected: boolean; onSelect: () => void }) { - return ( -
props.onSelect()} - > - {props.name} -
- ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/PublicationSWRProvider.tsx b/app/lish/[handle]/[publication]/dashboard/PublicationSWRProvider.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/PublicationSWRProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; - -import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; -import { callRPC } from "app/api/rpc/client"; -import { createContext, useContext } from "react"; -import useSWR, { SWRConfig } from "swr"; - -const PublicationContext = createContext({ name: "", did: "" }); -export function PublicationSWRDataProvider(props: { - publication_name: string; - publication_did: string; - publication_data: GetPublicationDataReturnType["result"]; - children: React.ReactNode; -}) { - return ( - - - {props.children} - - - ); -} - -export function usePublicationData() { - let { name, did } = useContext(PublicationContext); - let { data } = useSWR( - "publication-data", - async () => - (await callRPC("get_publication_data", { publication_name: name, did })) - ?.result, - ); - return data; -} diff --git a/app/lish/[handle]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[handle]/[publication]/dashboard/PublishedPostsLists.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/PublishedPostsLists.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; -import Link from "next/link"; -import { AtUri } from "@atproto/syntax"; -import { PubLeafletDocument } from "lexicons/api"; -import { EditTiny } from "components/Icons/EditTiny"; -import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; -import { Menu, MenuItem } from "components/Layout"; - -import { usePublicationData } from "./PublicationSWRProvider"; -import { Fragment } from "react"; -import { useParams } from "next/navigation"; - -export function PublishedPostsList() { - let publication = usePublicationData(); - let params = useParams(); - if (!publication) return null; - if (publication.documents_in_publications.length === 0) - return ( -
- Nothing's been published yet... -
- ); - return ( -
- {publication.documents_in_publications.map((doc) => { - if (!doc.documents) return null; - let leaflet = publication.leaflets_in_publications.find( - (l) => doc.documents && l.doc === doc.documents.uri, - ); - let uri = new AtUri(doc.documents.uri); - let record = doc.documents.data as PubLeafletDocument.Record; - - return ( - -
- -

{record.title}

- {record.description ? ( -

{record.description}

- ) : null} - {record.publishedAt ? ( -

- Published{" "} - {new Date(record.publishedAt).toLocaleDateString( - undefined, - { - year: "numeric", - month: "long", - day: "2-digit", - }, - )} -

- ) : null} - - {leaflet && ( - - - - )} -
-
-
- ); - })} -
- ); -} diff --git a/app/lish/[handle]/[publication]/dashboard/page.tsx b/app/lish/[handle]/[publication]/dashboard/page.tsx deleted file mode 100644 --- a/app/lish/[handle]/[publication]/dashboard/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { IdResolver } from "@atproto/identity"; -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 { ThemeProvider } from "components/ThemeManager/ThemeProvider"; -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 } from "lexicons/api"; - -const idResolver = new IdResolver(); - -export async function generateMetadata(props: { - params: Promise<{ publication: string; handle: string }>; -}): Promise { - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return { title: "Publication 404" }; - - let { result: publication } = await get_publication_data.handler( - { - did, - publication_name: decodeURIComponent((await props.params).publication), - }, - { supabase: supabaseServerClient }, - ); - if (!publication) return { title: "404 Publication" }; - return { title: decodeURIComponent((await props.params).publication) }; -} - -//This is the admin dashboard of the publication -export default async function Publication(props: { - params: Promise<{ publication: string; handle: string }>; -}) { - let params = await props.params; - let identity = await getIdentityData(); - if (!identity || !identity.atp_did) return ; - let did = await idResolver.handle.resolve((await props.params).handle); - if (!did) return ; - let { result: publication } = await get_publication_data.handler( - { - did, - publication_name: decodeURIComponent((await props.params).publication), - }, - { supabase: supabaseServerClient }, - ); - - let record = publication?.record as PubLeafletPublication.Record; - if (!publication || identity.atp_did !== publication.identity_did) - return ; - - try { - return ( - - -
-
-
- - - -
-
- , - Published: , - }} - defaultTab={"Drafts"} - /> -
- -
- -
-
-
-
-
-
- ); - } catch (e) { - console.log(e); - return
{JSON.stringify(e, undefined, 2)}
; - } -} - -const PubNotFound = () => { - return
ain't no pub here
; -}; -- tangled.sh