Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433"use client";import { ButtonPrimary } from "components/Buttons";import { useActionState, useEffect, useState } from "react";import { Input } from "components/Input";import { useIdentityData } from "components/IdentityProvider";import { confirmEmailAuthToken, requestAuthEmailToken,} from "actions/emailAuth";import { subscribeToPublicationWithEmail } from "actions/subscribeToPublicationWithEmail";import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";import { ShareSmall } from "components/Icons/ShareSmall";import { Popover } from "components/Popover";import { BlueskyTiny } from "components/Icons/BlueskyTiny";import { useToaster } from "components/Toast";import * as Dialog from "@radix-ui/react-dialog";import { subscribeToPublication, unsubscribeToPublication,} from "./subscribeToPublication";import { DotLoader } from "components/utils/DotLoader";import { addFeed } from "./addFeed";import { useSearchParams } from "next/navigation";import LoginForm from "app/login/LoginForm";import { RSSSmall } from "components/Icons/RSSSmall";import { SpeedyLink } from "components/SpeedyLink";
type State = | { state: "email" } | { state: "code"; token: string } | { state: "success" };export const SubscribeButton = (props: { compact?: boolean; publication: string;}) => { let { identity, mutate } = useIdentityData(); let [emailInputValue, setEmailInputValue] = useState(""); let [codeInputValue, setCodeInputValue] = useState(""); let [state, setState] = useState<State>({ state: "email" });
if (state.state === "email") { return ( <div className="flex gap-2"> <div className="flex relative w-full max-w-sm"> <Input type="email" className="input-with-border pr-[104px]! py-1! grow w-full" placeholder={ props.compact ? "subscribe with email..." : "email here..." } disabled={!!identity?.email} value={identity?.email ? identity.email : emailInputValue} onChange={(e) => { setEmailInputValue(e.currentTarget.value); }} /> <ButtonPrimary compact className="absolute right-1 top-1 outline-0!" onClick={async () => { if (identity?.email) { await subscribeToPublicationWithEmail(props.publication); //optimistically could add! await mutate(); return; } let tokenID = await requestAuthEmailToken(emailInputValue); setState({ state: "code", token: tokenID }); }} > {props.compact ? ( <ArrowRightTiny className="w-4 h-6" /> ) : ( "Subscribe" )} </ButtonPrimary> </div> {/* <ShareButton /> */} </div> ); } if (state.state === "code") { return ( <div className="w-full flex flex-col justify-center place-items-center p-4 rounded-md" style={{ background: "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", }} > <div className="flex flex-col leading-snug text-secondary"> <div>Please enter the code we sent to </div> <div className="italic font-bold">{emailInputValue}</div> </div>
<ConfirmCodeInput publication={props.publication} token={state.token} codeInputValue={codeInputValue} setCodeInputValue={setCodeInputValue} setState={setState} />
<button className="text-accent-contrast text-sm mt-1" onClick={() => { setState({ state: "email" }); }} > Re-enter Email </button> </div> ); }
if (state.state === "success") { return ( <div className={`w-full flex flex-col gap-2 justify-center place-items-center p-4 rounded-md text-secondary ${props.compact ? "py-1 animate-bounce" : "p-4"}`} style={{ background: "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", }} > <div className="flex gap-2 leading-snug font-bold italic"> <div>You're subscribed!</div> {/* <ShareButton /> */} </div> </div> ); }};
export const ShareButton = () => { return ( <button className="text-accent-contrast"> <ShareSmall /> </button> );};
const ConfirmCodeInput = (props: { codeInputValue: string; token: string; setCodeInputValue: (value: string) => void; setState: (state: State) => void; publication: string;}) => { let { mutate } = useIdentityData(); return ( <div className="relative w-fit mt-2"> <Input type="text" pattern="[0-9]" className="input-with-border pr-[88px]! py-1! max-w-[156px]" placeholder="000000" value={props.codeInputValue} onChange={(e) => { props.setCodeInputValue(e.currentTarget.value); }} /> <ButtonPrimary compact className="absolute right-1 top-1 outline-0!" onClick={async () => { console.log( await confirmEmailAuthToken(props.token, props.codeInputValue), );
await subscribeToPublicationWithEmail(props.publication); //optimistically could add! await mutate(); props.setState({ state: "success" }); return; }} > Confirm </ButtonPrimary> </div> );};
export const SubscribeWithBluesky = (props: { isPost?: boolean; pubName: string; pub_uri: string; base_url: string; subscribers: { identity: string }[];}) => { let { identity } = useIdentityData(); let searchParams = useSearchParams(); let [successModalOpen, setSuccessModalOpen] = useState( !!searchParams.has("showSubscribeSuccess"), ); let subscribed = identity?.atp_did && props.subscribers.find((s) => s.identity === identity.atp_did);
if (successModalOpen) return ( <SubscribeSuccessModal open={successModalOpen} setOpen={setSuccessModalOpen} /> ); if (subscribed) { return <ManageSubscription {...props} />; } return ( <div className="flex flex-col gap-2 text-center justify-center"> {props.isPost && ( <div className="text-sm text-tertiary font-bold"> Get updates from {props.pubName}! </div> )} <div className="flex flex-row gap-2 place-self-center"> <BlueskySubscribeButton pub_uri={props.pub_uri} setSuccessModalOpen={setSuccessModalOpen} /> <a href={`${props.base_url}/rss`} className="flex" target="_blank" aria-label="Subscribe to RSS" > <RSSSmall className="self-center" aria-hidden /> </a> </div> </div> );};
const ManageSubscription = (props: { isPost?: boolean; pubName: string; pub_uri: string; subscribers: { identity: string }[]; base_url: string;}) => { let toaster = useToaster(); let [hasFeed] = useState(false); let [, unsubscribe, unsubscribePending] = useActionState(async () => { await unsubscribeToPublication(props.pub_uri); toaster({ content: "You unsubscribed.", type: "success", }); }, null); return ( <div className={`flex ${props.isPost ? "flex-col " : "gap-2"} justify-center text-center`} > <div className="font-bold text-tertiary text-sm"> You're Subscribed{props.isPost ? ` to ` : "!"} {props.isPost && ( <SpeedyLink href={props.base_url} className="text-accent-contrast"> {props.pubName} </SpeedyLink> )} </div> <Popover trigger={<div className="text-accent-contrast text-sm">Manage</div>} > <div className="max-w-sm flex flex-col gap-1"> <h4>Update Options</h4>
{!hasFeed && ( <a href="https://bsky.app/profile/leaflet.pub/feed/subscribedPublications" target="_blank" className=" place-self-center" > <ButtonPrimary fullWidth compact className="!px-4"> View Bluesky Custom Feed </ButtonPrimary> </a> )}
<a href={`${props.base_url}/rss`} className="flex" target="_blank" aria-label="Subscribe to RSS" > <ButtonPrimary fullWidth compact> Get RSS </ButtonPrimary> </a>
<hr className="border-border-light my-1" />
<form action={unsubscribe}> <button className="font-bold text-accent-contrast w-max place-self-center"> {unsubscribePending ? <DotLoader /> : "Unsubscribe"} </button> </form> </div>{" "} </Popover> </div> );};
let BlueskySubscribeButton = (props: { pub_uri: string; setSuccessModalOpen: (open: boolean) => void;}) => { let { identity } = useIdentityData(); let toaster = useToaster(); let [, subscribe, subscribePending] = useActionState(async () => { let result = await subscribeToPublication( props.pub_uri, window.location.href + "?refreshAuth", ); if (result.hasFeed === false) { props.setSuccessModalOpen(true); } toaster({ content: <div>You're Subscribed!</div>, type: "success" }); }, null);
let [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []);
if (!identity?.atp_did) { return ( <Popover asChild trigger={ <ButtonPrimary className="place-self-center"> <BlueskyTiny /> Subscribe with Bluesky </ButtonPrimary> } > {isClient && ( <LoginForm text="Log in to subscribe to this publication!" noEmail redirectRoute={window?.location.href + "?refreshAuth"} action={{ action: "subscribe", publication: props.pub_uri }} /> )} </Popover> ); }
return ( <> <form action={subscribe} className="place-self-center flex flex-row gap-1" > <ButtonPrimary> {subscribePending ? ( <DotLoader /> ) : ( <> <BlueskyTiny /> Subscribe with Bluesky </> )} </ButtonPrimary> </form> </> );};
const SubscribeSuccessModal = ({ open, setOpen,}: { open: boolean; setOpen: (open: boolean) => void;}) => { let searchParams = useSearchParams(); let [loading, setLoading] = useState(false); let toaster = useToaster(); return ( <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Trigger asChild></Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-primary data-[state=open]:animate-overlayShow opacity-10 blur-xs" /> <Dialog.Content className={` z-20 opaque-container fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-96 px-3 py-4 max-w-(--radix-popover-content-available-width) max-h-(--radix-popover-content-available-height) overflow-y-scroll no-scrollbar flex flex-col gap-1 text-center justify-center `} > <Dialog.Title asChild={true}> <h3>Subscribed!</h3> </Dialog.Title> <Dialog.Description className="w-full flex flex-col"> You'll get updates about this publication via a Feed just for you. <ButtonPrimary className="place-self-center mt-4" onClick={async () => { if (loading) return;
setLoading(true); let feedurl = "https://bsky.app/profile/leaflet.pub/feed/subscribedPublications"; await addFeed(); toaster({ content: "Feed added!", type: "success" }); setLoading(false); window.open(feedurl, "_blank"); }} > {loading ? <DotLoader /> : "Add Bluesky Feed"} </ButtonPrimary> <button className="text-accent-contrast mt-1" onClick={() => { const newUrl = new URL(window.location.href); newUrl.searchParams.delete("showSubscribeSuccess"); window.history.replaceState({}, "", newUrl.toString()); setOpen(false); }} > No thanks </button> </Dialog.Description> <Dialog.Close /> </Dialog.Content> </Dialog.Portal> </Dialog.Root> );};