diff --git a/app/(home-pages)/home/Actions/Actions.tsx b/app/(home-pages)/home/Actions/Actions.tsx index e711d40e..6720edc0 100644 --- a/app/(home-pages)/home/Actions/Actions.tsx +++ b/app/(home-pages)/home/Actions/Actions.tsx @@ -5,7 +5,6 @@ import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; import { AccountSettings } from "./AccountSettings"; import { useIdentityData } from "components/IdentityProvider"; import { useReplicache } from "src/replicache"; -import { LoginActionButton } from "components/LoginButton"; export const Actions = () => { let { identity } = useIdentityData(); diff --git a/app/(home-pages)/home/LoggedOutWarning.tsx b/app/(home-pages)/home/LoggedOutWarning.tsx deleted file mode 100644 index b4ca4955..00000000 --- a/app/(home-pages)/home/LoggedOutWarning.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import { useIdentityData } from "components/IdentityProvider"; -import { LoginButton } from "components/LoginButton"; - -export const LoggedOutWarning = (props: {}) => { - let { identity } = useIdentityData(); - if (identity) return null; - return ( -
-
-

- Log in to collect all your Leaflets and access them on multiple - devices -

- -
-
- ); -}; diff --git a/app/(home-pages)/p/[didOrHandle]/PubListing.tsx b/app/(home-pages)/p/[didOrHandle]/PubListing.tsx index 10364400..fb19c17b 100644 --- a/app/(home-pages)/p/[didOrHandle]/PubListing.tsx +++ b/app/(home-pages)/p/[didOrHandle]/PubListing.tsx @@ -1,10 +1,11 @@ "use client"; import { AtUri } from "@atproto/syntax"; import { PublicationSubscription } from "app/(home-pages)/reader/getSubscriptions"; +import { dummy } from "app/lish/[did]/[publication]/[rkey]/PostPubInfo"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; import { PubIcon } from "components/ActionBar/Publications"; -import { Separator } from "components/Layout"; +import { SubscribeButton } from "components/Subscribe/SubscribeButton"; import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; @@ -64,7 +65,8 @@ export const PubListing = (props: PublicationSubscription) => { )}

-
+
+ {/**/} -

Sorry, can't resolve handle!

+

Sorry, we can't find that handle!

This may be a glitch on our end. If the issue persists please{" "} send us a note. diff --git a/app/(home-pages)/reader/InboxContent.tsx b/app/(home-pages)/reader/InboxContent.tsx index 1c15d9c6..6d8f9169 100644 --- a/app/(home-pages)/reader/InboxContent.tsx +++ b/app/(home-pages)/reader/InboxContent.tsx @@ -109,7 +109,7 @@ export const InboxContent = (props: { export const ReaderEmpty = () => { return ( -

+
Nothing to read yet…
Subscribe to publications and find their posts here! diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index f7f99ff4..8823ece8 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -36,11 +36,11 @@ import { import * as Y from "yjs"; import * as base64 from "base64-js"; import { YJSFragmentToString } from "src/utils/yjsFragmentToString"; -import { BlueskyLogin } from "app/login/LoginForm"; import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; import { AddTiny } from "components/Icons/AddTiny"; import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; import { useLocalPublishedAt } from "components/Pages/Backdater"; +import { LoginModal } from "components/LoginButton"; export const PublishButton = (props: { entityID: string }) => { let { data: pub } = useLeafletPublicationData(); @@ -188,7 +188,7 @@ const PublishToPublicationButton = (props: { entityID: string }) => { onOpenChange={(o) => setOpen(o)} side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} - className="sm:max-w-sm w-[1000px] p-0!" + className="sm:max-w-xs w-[1000px] p-0!" trigger={ { {!identity || !identity.atp_did ? (
-
Publish on AT Proto
+
Publish to Atmosphere
{ <> -
- Link a Bluesky account to start
a publishing on AT - Proto +
+ {!identity ? `Log in with` : "Link"} an Atmosphere account +
to start publishing!
- + {!identity ? `Log in` : "Link"} + + } /> } diff --git a/app/[leaflet_id]/actions/ShareOptions/index.tsx b/app/[leaflet_id]/actions/ShareOptions/index.tsx index 2f4aa3da..3b5d0e3d 100644 --- a/app/[leaflet_id]/actions/ShareOptions/index.tsx +++ b/app/[leaflet_id]/actions/ShareOptions/index.tsx @@ -6,7 +6,7 @@ import { useSmoker } from "components/Toast"; import { Menu, MenuItem } from "components/Menu"; import { ActionButton } from "components/ActionBar/ActionButton"; import useSWR from "swr"; -import LoginForm from "app/login/LoginForm"; + import { CustomDomainMenu } from "./DomainOptions"; import { useIdentityData } from "components/IdentityProvider"; import { @@ -14,11 +14,15 @@ import { useLeafletPublicationData, } from "components/PageSWRDataProvider"; import { ShareSmall } from "components/Icons/ShareSmall"; -import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; +import { + getPublicationURL, + getDocumentURL, +} from "app/lish/createPub/getPublicationURL"; import { AtUri } from "@atproto/syntax"; import { useIsMobile } from "src/hooks/isMobile"; +import { LoginModal } from "components/LoginButton"; -export type ShareMenuStates = "default" | "login" | "domain"; +export type ShareMenuStates = "default" | "domain"; export let useReadOnlyShareLink = () => { let { permission_token, rootEntity } = useReplicache(); @@ -64,11 +68,7 @@ export function ShareOptions() { /> } > - {menuState === "login" ? ( -
- -
- ) : menuState === "domain" ? ( + {menuState === "domain" ? ( ) : ( - {" "} +
+ Log in
} + />{" "} to publish on a custom domain!
); diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index 20c3e202..9fd2e4aa 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -180,7 +180,7 @@ const PublishPostForm = ( submit(); }} > -
+
{ let uri = props.publication_uri ? new AtUri(props.publication_uri) : null; return ( -
+

Published!

{uri && props.record ? ( diff --git a/app/api/oauth/[route]/route.ts b/app/api/oauth/[route]/route.ts index 94c113eb..f243f1f9 100644 --- a/app/api/oauth/[route]/route.ts +++ b/app/api/oauth/[route]/route.ts @@ -32,6 +32,7 @@ export async function GET( case "login": { const searchParams = req.nextUrl.searchParams; const handle = searchParams.get("handle") as string; + const signup = searchParams.get("signup") === "true"; // Put originating page here! let redirect = searchParams.get("redirect_url"); if (redirect) redirect = decodeURIComponent(redirect); @@ -46,6 +47,7 @@ export async function GET( "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview rpc:parts.page.mention.search?aud=* blob:*/*", signal: ac.signal, state: JSON.stringify(state), + ...(signup ? { prompt: "create" } : {}), }); return NextResponse.redirect(url); diff --git a/app/globals.css b/app/globals.css index 7573c319..d17e1d74 100644 --- a/app/globals.css +++ b/app/globals.css @@ -538,6 +538,21 @@ pre.shiki { padding-bottom: var(--safe-padding-bottom); } +/* animation for atmosphere logo carousel */ +@keyframes logo-scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +.logo-scroll-track { + animation: logo-scroll 50s linear infinite; + will-change: transform; +} + /* animation for new star in publish success illustration */ @keyframes new-star-animation { 0% { @@ -611,6 +626,7 @@ pre.shiki { border-radius: 4px; padding: 4px 6px; } + .footnote-side-item:not(:hover):not(:focus-within):not(.footnote-side-focused) { max-height: calc(4.5em + 8px); opacity: calc(var(--bg-page-alpha) * 0.7); diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index 3ce02990..4b75c478 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -21,10 +21,13 @@ import { 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 { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; import { RSSTiny } from "components/Icons/RSSTiny"; +import { LoginModal } from "components/LoginButton"; +import { Avatar } from "components/Avatar"; +import { useRecordFromDid } from "src/utils/useRecordFromDid"; export const SubscribeWithBluesky = (props: { compact?: boolean; @@ -188,52 +191,39 @@ let BlueskySubscribeButton = (props: { props.setLocalSubscribeState(); }, null); - let [isClient, setIsClient] = useState(false); - useEffect(() => { - setIsClient(true); - }, []); + let { data: profile } = useRecordFromDid(identity?.atp_did); if (!identity?.atp_did) { return ( - - Subscribe with Bluesky - - } - > - {isClient && ( - - )} - + Subscribe!} + noEmailLogin + /> ); } - return (
- + {subscribePending ? ( ) : ( <> - Subscribe with Bluesky + Subscribe as + + + +
+ {profile?.displayName ? profile.displayName : profile?.handle} +
+
)}
diff --git a/app/lish/[did]/[publication]/PublicationAuthor.tsx b/app/lish/[did]/[publication]/PublicationAuthor.tsx index 15f3e949..16e92c1c 100644 --- a/app/lish/[did]/[publication]/PublicationAuthor.tsx +++ b/app/lish/[did]/[publication]/PublicationAuthor.tsx @@ -11,7 +11,7 @@ export const PublicationAuthor = (props: { + by {props.displayName} @{props.handle} } diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx index c552375b..ddd512d5 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx @@ -12,11 +12,11 @@ import { AtpAgent } from "@atproto/api"; import { voteOnPublishedPoll } from "../voteOnPublishedPoll"; import { PollData } from "../fetchPollData"; import { Popover } from "components/Popover"; -import LoginForm from "app/login/LoginForm"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { getVoterIdentities, VoterIdentity } from "../getVoterIdentities"; import { Json } from "supabase/database.types"; import { InfoSmall } from "components/Icons/InfoSmall"; +import { LoginModal } from "components/LoginButton"; // Helper function to extract the first option from a vote record const getVoteOption = (voteRecord: any): string | null => { @@ -149,22 +149,15 @@ export const PublishedPollBlock = (props: { {isVoting ? "Voting..." : "Vote!"}
) : ( - Login to vote } - > - {isClient && ( - - )} - + /> )}
diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx index a2a92486..69a40edc 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx @@ -7,7 +7,11 @@ type Facet = PubLeafletRichtextFacet.Main; export type FacetRenderers = { DidMention?: (props: { did: string; children: ReactNode }) => ReactNode; - FootnoteRef?: (props: { footnoteId: string; index: number; children: ReactNode }) => ReactNode; + FootnoteRef?: (props: { + footnoteId: string; + index: number; + children: ReactNode; + }) => ReactNode; }; export type TextBlockCoreProps = { @@ -50,14 +54,29 @@ export function TextBlockCore(props: TextBlockCoreProps) { const FootnoteRenderer = props.renderers?.FootnoteRef; if (FootnoteRenderer) { children.push( - - {fnIndex} + + + {fnIndex} + , ); } else { children.push( - - {fnIndex} + + + {fnIndex} + , ); } diff --git a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx index c7dc68c2..a8bdb543 100644 --- a/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Footnotes/PublishedFootnotePopover.tsx @@ -19,15 +19,13 @@ type PopoverState = { close: () => void; }; -export const usePublishedFootnotePopoverStore = create( - (set) => ({ - activeFootnoteId: null, - anchorElement: null, - open: (footnoteId, anchor) => - set({ activeFootnoteId: footnoteId, anchorElement: anchor }), - close: () => set({ activeFootnoteId: null, anchorElement: null }), - }), -); +export const usePublishedFootnotePopoverStore = create((set) => ({ + activeFootnoteId: null, + anchorElement: null, + open: (footnoteId, anchor) => + set({ activeFootnoteId: footnoteId, anchorElement: anchor }), + close: () => set({ activeFootnoteId: null, anchorElement: null }), +})); export function PublishedFootnoteRefRenderer(props: { footnoteId: string; @@ -38,7 +36,7 @@ export function PublishedFootnoteRefRenderer(props: { return ( { e.preventDefault(); diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx index 65a72f7a..549bdc06 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx @@ -13,12 +13,12 @@ import { ButtonPrimary } from "components/Buttons"; import { BlueskyTiny } from "components/Icons/BlueskyTiny"; import { Popover } from "components/Popover"; import { AppBskyActorProfile, AtUri } from "@atproto/api"; -import { BlueskyLogin } from "app/login/LoginForm"; import { usePathname } from "next/navigation"; import { QuoteContent } from "../Quotes"; import { timeAgo } from "src/utils/timeAgo"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { ProfilePopover } from "components/ProfilePopover"; +import { LoginModal } from "components/LoginButton"; export type Comment = { record: Json; @@ -61,9 +61,16 @@ export function CommentsDrawerContent(props: { {identity?.atp_did ? ( ) : ( -
- Connect a Bluesky account to comment - +
+ + + {" "} + {identity ? " " : "with "} + an Atmosphere account to comment
)}
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx index bda5e413..ddf76e03 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx @@ -15,7 +15,7 @@ import { PubLeafletComment } from "lexicons/api"; import { type CommentOnDocument } from "contexts/DocumentContext"; import { prefetchQuotesData } from "./Quotes"; import { useIdentityData } from "components/IdentityProvider"; -import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; +import { ManageSubscription } from "app/lish/Subscribe"; import { EditTiny } from "components/Icons/EditTiny"; import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; import { RecommendButton } from "components/RecommendButton"; diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index 76b5f7f9..550094e7 100644 --- a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -1,11 +1,6 @@ "use client"; import { PubLeafletPagesLinearDocument } from "lexicons/api"; import { useLeafletContent } from "contexts/LeafletContentContext"; -import { PostPageData } from "./getPostPageData"; -import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; -import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; -import { SubscribeWithBluesky } from "app/lish/Subscribe"; -import { EditTiny } from "components/Icons/EditTiny"; import { ExpandedInteractions, getCommentCount, @@ -22,6 +17,7 @@ import { PollData } from "./fetchPollData"; import { SharedPageProps } from "./PostPages"; import { PostPrevNextButtons } from "./PostPrevNextButtons"; import { PostSubscribe } from "./PostSubscribe"; + import { collectFootnotesFromBlocks, buildFootnoteIndexMap, @@ -29,6 +25,7 @@ import { } from "./Footnotes/PublishedFootnotes"; import { PublishedFootnoteSideColumn } from "./Footnotes/PublishedFootnoteSideColumn"; import { PublishedFootnotePopover } from "./Footnotes/PublishedFootnotePopover"; +import { dummy, PostPubInfo } from "./PostPubInfo"; export function LinearDocumentPage({ blocks, @@ -111,6 +108,9 @@ export function LinearDocumentPage({ quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} recommendsCount={document.recommendsCount} /> +
+ {/**/} + {!hasPageBackground &&
} diff --git a/app/lish/[did]/[publication]/[rkey]/PostPubInfo.tsx b/app/lish/[did]/[publication]/[rkey]/PostPubInfo.tsx new file mode 100644 index 00000000..7d2f72bf --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/PostPubInfo.tsx @@ -0,0 +1,39 @@ +import { SubscribeInput } from "components/Subscribe/SubscribeButton"; + +export const dummy = { + newsletterMode: true, + user: { + loggedIn: true, + email: "example@example.com", + handle: "example.handle", + subscribed: false, + }, +}; + +export const PostPubInfo = (props: { + autoFocus?: boolean; + newsletterMode: boolean; + user: { + loggedIn: boolean; + email: string | undefined; + handle: string | undefined; + subscribed: boolean; + }; +}) => { + return ( +
+
+

Pub Title here

+
this is the pubs description
+ +
+ +
+
+
+ ); +}; diff --git a/app/lish/[did]/[publication]/[rkey]/page.tsx b/app/lish/[did]/[publication]/[rkey]/page.tsx index 43739aa2..6bce7d22 100644 --- a/app/lish/[did]/[publication]/[rkey]/page.tsx +++ b/app/lish/[did]/[publication]/[rkey]/page.tsx @@ -59,7 +59,7 @@ export default async function Post(props: { if (!did) return (
-

Sorry, can't resolve handle.

+

Sorry, we can't find that handle!

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 6dfd25bb..4e9bfe4a 100644 --- a/app/lish/[did]/[publication]/dashboard/page.tsx +++ b/app/lish/[did]/[publication]/dashboard/page.tsx @@ -8,6 +8,7 @@ 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"; export async function generateMetadata(props: { params: Promise<{ publication: string; did: string }>; @@ -37,7 +38,16 @@ export default async function Publication(props: { if (!identity || !identity.atp_did) return ( -

Looks like you're not logged in.

+

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

+ } + /> + ! +

If the issue persists please{" "} send us a note. diff --git a/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx b/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx new file mode 100644 index 00000000..7e7a03f9 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { useState } from "react"; +import { mutate } from "swr"; +import { useDomainStatus } from "components/Domains/useDomainStatus"; +import { + getDomainAssignment, + describeAssignment, +} from "components/Domains/domainAssignment"; +import { + useIdentityData, + mutateIdentityData, +} from "components/IdentityProvider"; +import { ButtonPrimary } from "components/Buttons"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; +import { updatePublicationBasePath } from "app/lish/createPub/updatePublication"; +import { + assignDomainToPublication, + removeDomainAssignment, +} from "actions/domains"; +import { PinTiny } from "components/Icons/PinTiny"; +import { LoadingTiny } from "components/Icons/LoadingTiny"; +import { UnlinkTiny } from "components/Icons/UnlinkTiny"; +import { DotLoader } from "components/utils/DotLoader"; +import { useToaster } from "components/Toast"; +import type { CustomDomain } from "components/Domains/DomainList"; + +export const DomainsInline = () => { + let { data, mutate: mutatePubData } = usePublicationData(); + let { publication: pubData } = data || {}; + let record = useNormalizedPublicationRecord(); + let { identity, mutate: mutateIdentity } = useIdentityData(); + let toaster = useToaster(); + let basePath = record?.url?.replace(/^https?:\/\//, "") || ""; + + let pubDomains = pubData?.publication_domains || []; + let pubDomainNames = new Set(pubDomains.map((d) => d.domain)); + + if (!pubData) return null; + + return ( +

+

Domains

+
+

This Publication's Domains

+
DEFAULT
+ {pubDomains + .filter((d) => d.domain === basePath) + .map((d) => ( + + ))} + {pubDomains.filter((d) => d.domain !== basePath).length !== 0 && ( + <> +
ALTERNATES
+ {pubDomains + .filter((d) => d.domain !== basePath) + .map((d) => ( + + ))} + + )} + +
+

Available Domains

+ {(() => { + let availableDomains = (identity?.custom_domains || []) + .filter((d) => !pubDomainNames.has(d.domain)) + .filter( + (d) => + d.publication_domains.length === 0 && + d.custom_domain_routes.length === 0, + ); + return availableDomains.length > 0 ? ( + <> + {availableDomains.map((d) => ( + { + mutateIdentity(); + mutate("publication-data"); + }} + /> + ))} +
+ Add new domains from your profile settings! +
+ + ) : ( +
+ No available domains! +
+ Add new domains from your profile settings! +
+ ); + })()} +
+
+ ); +}; + +function PubDomainRow(props: { + domain: string; + publication_uri: string; + basePath: string; + mutatePubData: ReturnType["mutate"]; + mutateIdentity: ReturnType["mutate"]; + toaster: ReturnType; +}) { + let { pending } = useDomainStatus(props.domain); + let [loading, setLoading] = useState(false); + let [unlinking, setUnlinking] = useState(false); + let toaster = props.toaster; + + return ( +
+ {props.domain} +
+ {pending ? ( +
+

+ pending +

+ +
+ ) : ( + <> + {props.basePath !== props.domain && ( +
+ {!props.domain.endsWith(".leaflet.pub") && ( + + )} + +
+ )} + + )} +
+
+ ); +} + +function UnassignedDomainRow(props: { + domainData: CustomDomain; + publication_uri: string; + mutatePubData: ReturnType["mutate"]; + onAssigned: () => void; +}) { + let { pending } = useDomainStatus(props.domainData.domain); + let { mutate: mutateIdentity } = useIdentityData(); + let assignment = getDomainAssignment(props.domainData); + let [confirming, setConfirming] = useState(false); + let [loading, setLoading] = useState(false); + + async function doAssign() { + setLoading(true); + mutateIdentityData(mutateIdentity, (draft) => { + let domain = draft.custom_domains.find( + (d) => d.domain === props.domainData.domain, + ); + if (domain) { + domain.custom_domain_routes = []; + let pub = draft.publications?.find( + (p) => p.uri === props.publication_uri, + ); + domain.publication_domains = [ + { + publication: props.publication_uri, + domain: props.domainData.domain, + identity: "", + created_at: new Date().toISOString(), + publications: pub ? { name: pub.name } : null, + }, + ]; + } + }); + props.mutatePubData( + (current) => { + if (!current) return current; + let pub = current.publication; + if (!pub) return current; + return { + ...current, + publication: { + ...pub, + publication_domains: [ + ...(pub.publication_domains || []), + { + publication: props.publication_uri, + domain: props.domainData.domain, + created_at: new Date().toISOString(), + identity: "", + }, + ], + }, + }; + }, + { revalidate: false }, + ); + setConfirming(false); + props.onAssigned(); + await assignDomainToPublication({ + domain: props.domainData.domain, + publication_uri: props.publication_uri, + }); + } + + return ( +
+
+ {props.domainData.domain} + {pending ? ( +
unverified
+ ) : confirming ? null : ( + + )} +
+ {confirming && ( +
+

+ This domain is currently assigned to{" "} + {describeAssignment(assignment)}. Assigning it here will remove that + assignment. +

+
+ + + {loading ? : "Reassign"} + +
+
+ )} +
+ ); +} diff --git a/app/lish/[did]/[publication]/subscribeSuccess/page.tsx b/app/lish/[did]/[publication]/subscribeSuccess/page.tsx index b0d572cd..deb03a81 100644 --- a/app/lish/[did]/[publication]/subscribeSuccess/page.tsx +++ b/app/lish/[did]/[publication]/subscribeSuccess/page.tsx @@ -4,7 +4,7 @@ import { RSSSmall } from "components/Icons/RSSSmall"; export default function SubscribeSuccess() { return (
-
+

You've Subscribed!

Add this custom feed to your Bluesky to get the updates from this and diff --git a/app/lish/createPub/page.tsx b/app/lish/createPub/page.tsx index f4d4d510..b007b529 100644 --- a/app/lish/createPub/page.tsx +++ b/app/lish/createPub/page.tsx @@ -1,21 +1,15 @@ import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; import { CreatePubForm } from "./CreatePubForm"; import { getIdentityData } from "actions/getIdentityData"; -import LoginForm from "app/login/LoginForm"; +import { LoginContent } from "components/LoginButton"; export default async function CreatePub() { let identity = await getIdentityData(); if (!identity) return (
-
-
- -
+
+
); diff --git a/app/lish/sorry-boris/page.tsx b/app/lish/sorry-boris/page.tsx index 6131343a..905748d0 100644 --- a/app/lish/sorry-boris/page.tsx +++ b/app/lish/sorry-boris/page.tsx @@ -1,14 +1,20 @@ import { getIdentityData } from "actions/getIdentityData"; -import { BlueskyLogin } from "app/login/LoginForm"; import { codeToHtml } from "shiki"; import { FixFeedsButton } from "./fixFeedsButton"; +import { LoginModal } from "components/LoginButton"; +import { ButtonPrimary } from "components/Buttons"; export default async function Page() { let identity = await getIdentityData(); if (!identity) { return ( - + Sorry Boris!} + /> ); } diff --git a/app/login/LoginForm.tsx b/app/login/LoginForm.tsx deleted file mode 100644 index a9ffa1ba..00000000 --- a/app/login/LoginForm.tsx +++ /dev/null @@ -1,225 +0,0 @@ -"use client"; -import { - confirmEmailAuthToken, - requestAuthEmailToken, -} from "actions/emailAuth"; -import { loginWithEmailToken } from "actions/login"; -import { ActionAfterSignIn } from "app/api/oauth/[route]/afterSignInActions"; -import { getHomeDocs } from "app/(home-pages)/home/storage"; -import { ButtonPrimary } from "components/Buttons"; -import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; -import { BlueskySmall } from "components/Icons/BlueskySmall"; -import { Input } from "components/Input"; -import { useSmoker, useToaster } from "components/Toast"; -import React, { useState } from "react"; -import { mutate } from "swr"; -import { BlueskyTiny } from "components/Icons/BlueskyTiny"; - -export default function LoginForm(props: { - noEmail?: boolean; - redirectRoute?: string; - action?: ActionAfterSignIn; - text: React.ReactNode; -}) { - type FormState = - | { - stage: "email"; - email: string; - } - | { - stage: "code"; - email: string; - tokenId: string; - confirmationCode: string; - }; - - const [formState, setFormState] = useState({ - stage: "email", - email: "", - }); - - const handleSubmitEmail = async (e: React.FormEvent) => { - e.preventDefault(); - const tokenId = await requestAuthEmailToken(formState.email); - setFormState({ - stage: "code", - email: formState.email, - tokenId, - confirmationCode: "", - }); - }; - - let smoker = useSmoker(); - let toaster = useToaster(); - - const handleSubmitCode = async (e: React.FormEvent) => { - e.preventDefault(); - let rect = e.currentTarget.getBoundingClientRect(); - - if (formState.stage !== "code") return; - const confirmedToken = await confirmEmailAuthToken( - formState.tokenId, - formState.confirmationCode, - ); - - if (!confirmedToken) { - smoker({ - error: true, - text: "incorrect code!", - position: { - y: rect.bottom - 16, - x: rect.right - 220, - }, - }); - } else { - let localLeaflets = getHomeDocs(); - - await loginWithEmailToken(localLeaflets.filter((l) => !l.hidden)); - mutate("identity"); - toaster({ - content:
Logged in! Welcome!
, - type: "success", - }); - } - }; - - if (formState.stage === "code") { - return ( -
-
- Please enter the code we sent to -
{formState.email}
-
- - - setFormState({ - ...formState, - confirmationCode: e.target.value, - }) - } - required - /> - - {}} - > - Confirm - - -
- ); - } - - return ( -
-
-

Log In or Sign Up

-
{props.text}
-
- - - - {props.noEmail ? null : ( - <> -
-
-
or
-
-
-
- - setFormState({ - ...formState, - email: e.target.value, - }) - } - required - /> - - - {" "} - -
- - )} -
- ); -} - -export function BlueskyLogin(props: { - redirectRoute?: string; - action?: ActionAfterSignIn; - compact?: boolean; -}) { - const [signingWithHandle, setSigningWithHandle] = useState(false); - const [handle, setHandle] = useState(""); - - return ( -
- - {props.action && ( - - )} - {signingWithHandle ? ( -
- setHandle(e.target.value)} - required - /> - Sign In -
- ) : ( -
- - {props.compact ? : } - {props.compact ? "Link" : "Log In/Sign Up with"} Bluesky - - -
- )} -
- ); -} diff --git a/components/ActionBar/ActionButton.tsx b/components/ActionBar/ActionButton.tsx index 6dc14129..2450f348 100644 --- a/components/ActionBar/ActionButton.tsx +++ b/components/ActionBar/ActionButton.tsx @@ -57,10 +57,10 @@ export const ActionButton = forwardRef< className={` actionButton relative font-bold rounded-md border - flex gap-2 items-center justify-start + flex gap-2 items-start justify-start sm:w-full sm:max-w-full p-1 w-max - ${smallOnMobile && "sm:text-base text-sm py-0! sm:py-1! sm:h-fit h-6"} + ${smallOnMobile && "sm:text-base text-sm py-0! sm:py-1! sm:h-fit h-6 items-center!"} ${ primary ? "bg-accent-1 border-accent-1 text-accent-2 transparent-outline sm:hover:outline-accent-contrast focus:outline-accent-1 outline-offset-1 " diff --git a/components/ActionBar/DesktopNavigation.tsx b/components/ActionBar/DesktopNavigation.tsx index 65201200..82ea1734 100644 --- a/components/ActionBar/DesktopNavigation.tsx +++ b/components/ActionBar/DesktopNavigation.tsx @@ -8,8 +8,10 @@ import { } from "./NavigationButtons"; import { PublicationButtons } from "./Publications"; import { Sidebar } from "./Sidebar"; -import { LoginActionButton, LoginButton } from "components/LoginButton"; +import { LoginModal } from "components/LoginButton"; import { ProfileButton } from "./ProfileButton"; +import { ActionButton } from "./ActionButton"; +import { AccountSmall } from "components/Icons/AccountSmall"; export const DesktopNavigation = (props: { currentPage: navPages; @@ -35,7 +37,16 @@ export const DesktopNavigation = (props: { /> ) : ( - + } + label="Log In/Sign Up" + /> + } + /> )} diff --git a/components/ActionBar/MobileNavigation.tsx b/components/ActionBar/MobileNavigation.tsx index bcf52df7..f0aaf51a 100644 --- a/components/ActionBar/MobileNavigation.tsx +++ b/components/ActionBar/MobileNavigation.tsx @@ -7,8 +7,10 @@ import { WriterButton, } from "./NavigationButtons"; import { PublicationNavigation } from "./PublicationNavigation"; -import { LoginActionButton } from "components/LoginButton"; +import { LoginModal } from "components/LoginButton"; import { ProfileButton } from "./ProfileButton"; +import { ActionButton } from "./ActionButton"; +import { AccountSmall } from "components/Icons/AccountSmall"; export const MobileNavigation = (props: { currentPage: navPages; @@ -56,7 +58,16 @@ export const MobileNavigation = (props: {
) : ( - + } + label="Log In/Sign Up" + /> + } + /> )}
); diff --git a/components/ActionBar/Publications.tsx b/components/ActionBar/Publications.tsx index 2fa0355f..a23243b1 100644 --- a/components/ActionBar/Publications.tsx +++ b/components/ActionBar/Publications.tsx @@ -14,12 +14,12 @@ import { import { SpeedyLink } from "components/SpeedyLink"; import { PublishSmall } from "components/Icons/PublishSmall"; import { Popover } from "components/Popover"; -import { BlueskyLogin } from "app/login/LoginForm"; -import { ButtonSecondary } from "components/Buttons"; +import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { 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; @@ -67,15 +67,15 @@ export const PublicationButtons = (props: { )} {identity.publications?.map((d) => { - return ( - - ); - })} + return ( + + ); + })} { { return (
-
Publish on AT Proto
+
Publish on the Atmosphere
{identity && identity.atp_did ? ( // has ATProto account and no pubs <> -
- Start a new publication
- on AT Proto +
+ Start a new publication, newsletter, or blog on the Atmosphere!
- + Start a Publication! @@ -178,11 +177,19 @@ export const PubListEmptyContent = (props: { compact?: boolean }) => { ) : ( // no ATProto account and no pubs <> -
- Link a Bluesky account to start
a new publication on AT Proto +
+ Start a new publication, newsletter, or blog on the Atmosphere!
- + + {identity ? "Link to" : "Log in with"} Atmosphere + + } + /> )}
diff --git a/components/ActionBar/Sidebar.tsx b/components/ActionBar/Sidebar.tsx index e429a856..6736ae92 100644 --- a/components/ActionBar/Sidebar.tsx +++ b/components/ActionBar/Sidebar.tsx @@ -30,7 +30,7 @@ export function Sidebar(props: { ${!props.alwaysOpen ? "absolute top-0 left-0 z-10 w-max" : "w-[192px] max-w-[192px]"} h-fit p-[6px] flex flex-col gap-1 justify-start border - rounded-md bg-bg-page ${open && !props.alwaysOpen ? "border-border-light" : "container"} + rounded-md bg-bg-page ${open && !props.alwaysOpen ? "border-border-light" : "frosted-container"} ${props.className} `} onMouseOver={() => { diff --git a/components/Combobox.tsx b/components/Combobox.tsx index b001d0ae..90ac1b04 100644 --- a/components/Combobox.tsx +++ b/components/Combobox.tsx @@ -15,12 +15,15 @@ export const Combobox = ({ showSearch, trigger, triggerClassName, + className, sideOffset, open: openProp, + zIndex, }: { children: React.ReactNode; trigger?: React.ReactNode; triggerClassName?: string; + className?: string; results: string[]; onSelect?: () => void; onOpenChange?: (open: boolean) => void; @@ -31,6 +34,7 @@ export const Combobox = ({ showSearch?: boolean; sideOffset?: number; open?: boolean; + zIndex?: number; }) => { let ref = useRef(null); let [internalOpen, setInternalOpen] = useState(false); @@ -106,10 +110,12 @@ export const Combobox = ({ collisionPadding={16} ref={ref} onOpenAutoFocus={(e) => e.preventDefault()} + style={zIndex !== undefined ? { zIndex } : undefined} className={` commandMenuContent group/cmd-menu z-20 w-[264px] flex data-[side=top]:items-end items-start + ${className} `} > diff --git a/components/Icons/AtmosphereAccount.tsx b/components/Icons/AtmosphereAccount.tsx new file mode 100644 index 00000000..12786047 --- /dev/null +++ b/components/Icons/AtmosphereAccount.tsx @@ -0,0 +1,19 @@ +import { Props } from "./Props"; + +export const AtmosphereAccount = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/Icons/HelpTiny.tsx b/components/Icons/HelpTiny.tsx new file mode 100644 index 00000000..ed59d0d4 --- /dev/null +++ b/components/Icons/HelpTiny.tsx @@ -0,0 +1,19 @@ +import { Props } from "./Props"; + +export const HelpTiny = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/LoginButton.tsx b/components/LoginButton.tsx index f05eaad3..e4455734 100644 --- a/components/LoginButton.tsx +++ b/components/LoginButton.tsx @@ -1,43 +1,232 @@ "use client"; import { useIdentityData } from "./IdentityProvider"; import { Popover } from "./Popover"; -import LoginForm from "app/login/LoginForm"; +import { Modal } from "./Modal"; + import { ButtonPrimary } from "./Buttons"; import { ActionButton } from "./ActionBar/ActionButton"; import { AccountSmall } from "./Icons/AccountSmall"; -import { useIsMobile } from "src/hooks/isMobile"; +import { AtmosphericHandleInfo } from "./Subscribe/HandleSubscribe"; +import { HandleInput } from "./Subscribe/HandleInput"; +import { EmailInput, EmailConfirm } from "./Subscribe/EmailSubscribe"; +import { useState } from "react"; +import { GoToArrow } from "./Icons/GoToArrow"; +import { ToggleGroup } from "./ToggleGroup"; +import { useToaster } from "./Toast"; +import { BlueskyTiny } from "./Icons/BlueskyTiny"; +import { + requestAuthEmailToken, + confirmEmailAuthToken, +} from "actions/emailAuth"; +import { loginWithEmailToken } from "actions/login"; +import { getHomeDocs } from "app/(home-pages)/home/storage"; +import { mutate } from "swr"; -export function LoginButton() { - let identityData = useIdentityData(); - if (identityData.identity) return null; +export const LoginModal = (props: { + noEmailLogin?: boolean; + trigger: React.ReactNode; + asChild?: boolean; + redirectRoute?: string; +}) => { + let [open, setOpen] = useState(false); return ( - - Log In! - - } + - - + setOpen(false)} + /> + ); -} +}; -export function LoginActionButton() { +export const LoginContent = (props: { + pageView?: boolean; + noEmailLogin?: boolean; + redirectRoute?: string; + open?: boolean; + onSuccess?: () => void; +}) => { let identityData = useIdentityData(); + let [state, setState] = useState< + "log in" | "sign up" | "email log in" | "email confirm" + >("log in"); + let [loginEmail, setLoginEmail] = useState(""); + let [tokenId, setTokenId] = useState(null); + let [loading, setLoading] = useState(false); + let toaster = useToaster(); + if (identityData.identity) return null; - let isMobile = useIsMobile(); + + const handleEmailSubmit = async () => { + setLoading(true); + try { + const id = await requestAuthEmailToken(loginEmail); + setTokenId(id); + setState("email confirm"); + } catch (e) { + toaster({ + content: ( +
Could not send email — please try again
+ ), + type: "error", + }); + } finally { + setLoading(false); + } + }; + + const handleCodeSubmit = async (code: string) => { + if (!tokenId) return; + setLoading(true); + const confirmedToken = await confirmEmailAuthToken(tokenId, code); + if (!confirmedToken) { + setLoading(false); + toaster({ + content:
Incorrect code!
, + type: "error", + }); + return; + } + const localLeaflets = getHomeDocs(); + await loginWithEmailToken(localLeaflets.filter((l) => !l.hidden)); + mutate("identity"); + toaster({ + content:
Logged in! Welcome!
, + type: "success", + }); + setLoading(false); + props.onSuccess?.(); + }; + return ( - } label="Sign In" /> - } - > - - +
+ +
+ {state === "log in" ? ( + <> +
+

+ Log in with
+ Atmosphere account +

+ + What's the Atmosphere? +
+ } + /> +
+ } + loading={loading} + onSubmit={(handle) => { + setLoading(true); + const redirectUrl = props.redirectRoute || "/"; + window.location.href = `/api/oauth/login?handle=${encodeURIComponent(handle)}&redirect_url=${encodeURIComponent(redirectUrl)}`; + }} + /> + {props.noEmailLogin ? null : ( + <> +
+ + + )} + + ) : state === "email log in" ? ( +
{ + e.preventDefault(); + handleEmailSubmit(); + }} + > +

Log in with Email

+ + + + + } + /> +
+ + + ) : state === "email confirm" ? ( + + ) : ( +
+

+ Leaflet is part of
+ the Atmosphere. +

+
+ Create an Atmosphere account on Bluesky to get started! +
+
+ + + + Sign up via Bluesky + +
+ +
+ )} +
+
); -} +}; diff --git a/components/Subscribe/EmailSubscribe.tsx b/components/Subscribe/EmailSubscribe.tsx new file mode 100644 index 00000000..8dbd9f68 --- /dev/null +++ b/components/Subscribe/EmailSubscribe.tsx @@ -0,0 +1,80 @@ +"use client"; +import * as OneTimePasswordField from "@radix-ui/react-one-time-password-field"; +import { ButtonPrimary } from "components/Buttons"; +import { Input } from "components/Input"; +import { DotLoader } from "components/utils/DotLoader"; +import { theme } from "tailwind.config"; + +export const EmailInput = (props: { + action: React.ReactNode; + autoFocus?: boolean; + large?: boolean; + value: string; + onChange: (val: string) => void; + disabled?: boolean; + loading?: boolean; +}) => { + return ( +
+ props.onChange(e.target.value)} + /> +
+ {props.loading ? : props.action} +
+
+ ); +}; + +export const EmailConfirm = (props: { + emailValue: string; + autoFocus?: boolean; + loading?: boolean; + onSubmit: (code: string) => void; +}) => { + let inputClassName = `input-with-border text-2xl w-8 h-12 text-center ${props.loading ? "bg-border-light! text-tertiary!" : ""}`; + return ( +
+

Confirm your email

+ Enter the code sent to
+
{props.emailValue}
+ { + props.onSubmit(value); + }} + > +
+ + + + + + +
+ +
+
+ ); +}; diff --git a/components/Subscribe/EmailSubscribeSuccess.tsx b/components/Subscribe/EmailSubscribeSuccess.tsx new file mode 100644 index 00000000..5dc339a8 --- /dev/null +++ b/components/Subscribe/EmailSubscribeSuccess.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { LinkHandle } from "./HandleSubscribe"; + +export const EmailSubscribeSuccess = (props: { + email: string | undefined; + handle: string | undefined; +}) => { + return ( +
+

You've Subscribed!

+ You'll recieve new posts to
+ {props.email ? props.email : "your email"} + {!props.handle && ( + <> +
+ + + )} +
+ ); +}; + +export const EmailLogInSuccess = (props: {}) => { + return ( +
+

Welcome!

+
+ ); +}; diff --git a/components/Subscribe/HandleInput.tsx b/components/Subscribe/HandleInput.tsx new file mode 100644 index 00000000..71b466ca --- /dev/null +++ b/components/Subscribe/HandleInput.tsx @@ -0,0 +1,168 @@ +"use client"; +import { Agent } from "@atproto/api"; +import { Input } from "components/Input"; +import { Combobox, ComboboxResult } from "components/Combobox"; +import { useRef, useState } from "react"; +import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; +import { DotLoader } from "components/utils/DotLoader"; +import { theme } from "tailwind.config"; +import { AtmosphereAccount } from "components/Icons/AtmosphereAccount"; + +type ActorSuggestion = { + handle: string; + did: string; + displayName?: string; + avatar?: string; +}; + +export const HandleInput = (props: { + autoFocus?: boolean; + action?: React.ReactNode; + className?: string; + large?: boolean; + loading?: boolean; + onSubmit?: (handle: string) => void; +}) => { + let [handleValue, setHandleValue] = useState(""); + let [suggestions, setSuggestions] = useState([]); + let [dropdownOpen, setDropdownOpen] = useState(false); + let [highlighted, setHighlighted] = useState(undefined); + let requestIdRef = useRef(0); + + useDebouncedEffect( + async () => { + if (!handleValue) { + setSuggestions([]); + setDropdownOpen(false); + return; + } + const requestId = ++requestIdRef.current; + const agent = new Agent("https://public.api.bsky.app"); + const result = await agent.searchActorsTypeahead({ + q: handleValue, + limit: 8, + }); + if (requestId !== requestIdRef.current) return; + const actors = result.data.actors.map((actor) => ({ + handle: actor.handle, + did: actor.did, + displayName: actor.displayName, + avatar: actor.avatar, + })); + setSuggestions(actors); + setDropdownOpen(actors.length > 0); + }, + 300, + [handleValue], + ); + + const handleSelect = (handle?: string) => { + const selected = handle ?? handleValue; + if (!selected) return; + setHandleValue(selected); + setDropdownOpen(false); + setSuggestions([]); + setHighlighted(undefined); + props.onSubmit?.(selected); + }; + + const handles = suggestions.map((s) => s.handle); + + return ( + { + if (!open) { + setDropdownOpen(false); + setHighlighted(undefined); + } + }} + results={handles} + highlighted={highlighted} + setHighlighted={setHighlighted} + onSelect={() => handleSelect(highlighted)} + zIndex={60} + sideOffset={4} + className="w-(--radix-popover-trigger-width)!" + trigger={ +
+
+ +
+ setHandleValue(e.target.value)} + onClick={(e) => e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + autoComplete="off" + disabled={props.loading} + /> + {props.loading ? ( + + ) : props.onSubmit ? ( + + ) : ( + props.action + )} +
+ } + > + {suggestions.map((actor) => ( + handleSelect(actor.handle)} + className=" flex-row! gap-2! leading-snug text-sm" + > + {actor.avatar ? ( + + ) : ( +
+ )} +
+
+ {actor.displayName || actor.handle} +
+ {actor.displayName && ( +
+ @{actor.handle} +
+ )} +
+ + ))} + + ); +}; diff --git a/components/Subscribe/HandleSubscribe.tsx b/components/Subscribe/HandleSubscribe.tsx new file mode 100644 index 00000000..dea8b723 --- /dev/null +++ b/components/Subscribe/HandleSubscribe.tsx @@ -0,0 +1,191 @@ +"use client"; +import { ButtonPrimary } from "components/Buttons"; +import { BlueskyTiny } from "components/Icons/BlueskyTiny"; +import { Popover } from "components/Popover"; +import Link from "next/link"; +import { HandleInput } from "./HandleInput"; +const logos = { + Anisota: "/logos/anisota.svg", + Blacksky: "/logos/blacksky.svg", + Blento: "/logos/blento.svg", + Bluesky: "/logos/bluesky.svg", + Cartridge: "/logos/cartridge.svg", + Eurosky: "/logos/eurosky.svg", + Graze: "/logos/graze.svg", + Leaflet: "/logos/leaflet.svg", + Margin: "/logos/margin.svg", + Offprint: "/logos/offprint.svg", + Pckt: "/logos/pckt.svg", + pdsls: "/logos/pdsls.svg", + "plyr.fm": "/logos/plyr.fm.svg", + Popfeed: "/logos/popfeed.svg", + Roomy: "/logos/roomy.svg", + Semble: "/logos/semble.svg", + Sill: "/logos/sill.svg", + Spark: "/logos/spark.svg", + Surf: "/logos/surf.svg", + Tangled: "/logos/tangled.svg", +}; + +import { Separator } from "components/Layout"; + +export const SubscribeWithHandle = (props: { + autoFocus?: boolean; + user: { + loggedIn: boolean; + email: string | undefined; + handle: string | undefined; + }; +}) => { + if (props.user.loggedIn && props.user.handle) { + return ( + + Subscribe as + +
+ +
{props.user.handle}
+ + + ); + } else + return ( +
+ +
+ + +
Create
+
+
+ ); +}; + +export const LinkHandle = (props: { compact?: boolean }) => { + return ( +
+
+

+ Link your universal handle +

+
+ to comment, recommend, and see what your friends are reading +
+ +
+ +
+ ); +}; + +export const HandleInputandOAuth = (props: { + link?: boolean; + compact?: boolean; +}) => { + return ( +
+ +
+
+
+ or {props.link ? "link with" : "sign in via"} +
+
+
+ + Bluesky + +
+ ); +}; + +export const AtSubscribeSuccess = (props: {}) => { + return ( +
+

You've Subscribed!

+ You'll recieve new posts in the
+ Leaflet Reader +
+ + or any standard.site enabled reader! + +
+
+

Other ways to follow

+ Get the RSS Feed + Pin Custom Feed in Bluesky +
+
+ ); +}; + +export const AtmosphericHandleInfo = (props: { trigger?: React.ReactNode }) => { + return ( + + What's the Atmosphere +
+ ) + } + > +
+ The Atmosphere is a growing ecosystem of social apps, like Leaflet and + Bluesky. +
+
+
+ One account gets you into all of them. +
+ +
+ Apps on the Atmosphere! +
+
+
+ {( + [ + "Leaflet", "Bluesky", "Blacksky", "Eurosky", "Tangled", + "Semble", "Surf", "Spark", "Pckt", "pdsls", "plyr.fm", + "Popfeed", "Roomy", "Sill", "Offprint", "Margin", + "Anisota", "Blento", "Cartridge", "Graze", + "Leaflet", "Bluesky", "Blacksky", "Eurosky", "Tangled", + "Semble", "Surf", "Spark", "Pckt", "pdsls", "plyr.fm", + "Popfeed", "Roomy", "Sill", "Offprint", "Margin", + "Anisota", "Blento", "Cartridge", "Graze", + ] as (keyof typeof logos)[] + ).map((name, i) => ( + + ))} +
+
+ + + Sign up via Bluesky! + + + ); +}; + +const AtApp = (props: { logo: string; name: string }) => { + return ( +
+ {props.name} + {props.name} +
+ ); +}; diff --git a/components/Subscribe/ManageSubscribe.tsx b/components/Subscribe/ManageSubscribe.tsx new file mode 100644 index 00000000..7133e6c4 --- /dev/null +++ b/components/Subscribe/ManageSubscribe.tsx @@ -0,0 +1,116 @@ +"use client"; +import { useState } from "react"; +import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; +import { CheckTiny } from "components/Icons/CheckTiny"; +import { GoToArrow } from "components/Icons/GoToArrow"; +import { Modal } from "components/Modal"; +import { useToaster } from "components/Toast"; +import { LinkHandle } from "./HandleSubscribe"; +import { EmailInput, EmailConfirm } from "./EmailSubscribe"; + +export const ManageSubscription = (props: { + newsletterMode: boolean; + user: { + loggedIn: boolean; + email: string | undefined; + handle: string | undefined; + subscribed: boolean; + }; +}) => { + let [email, setEmail] = useState(props.user?.email ?? ""); + let toaster = useToaster(); + let prefClassName = + "flex gap-2 justify-between font-bold text-secondary items-center"; + return ( + +
+ Subscribed +
+
Manage
+
+ } + > +
+ {props.newsletterMode && props.user.email ? ( +
+
+

Linked Email

+

+ {props.user.email} +

+
+
+ ) : null} + {props.user.handle && ( +
+
+ Linked Handle +

+ {props.user.handle} +

+
+
+ )} +
+ Get RSS Feed +
+
+ Pin Custom Bluesky Feed + +
+ + {props.newsletterMode && !props.user.email ? ( +
+
+

Link your email

+
+ to get updates right to your inbox! +
+
+ + link + + } + > + { + toaster({ + content:
Email Linked!
, + type: "success", + }); + }} + /> + + } + /> +
+ ) : null} + {!props.user.handle ? ( +
+ +
+ ) : null} + +
+ Unsubscribe +
+ + ); +}; diff --git a/components/Subscribe/SubscribeButton.tsx b/components/Subscribe/SubscribeButton.tsx new file mode 100644 index 00000000..e816f8cd --- /dev/null +++ b/components/Subscribe/SubscribeButton.tsx @@ -0,0 +1,92 @@ +"use client"; +import { useState } from "react"; +import { SubscribeWithHandle } from "./HandleSubscribe"; +import { EmailInput, EmailConfirm } from "./EmailSubscribe"; +import { EmailSubscribeSuccess } from "./EmailSubscribeSuccess"; +import { Modal } from "components/Modal"; +import { ButtonPrimary } from "components/Buttons"; +import { PostPubInfo } from "app/lish/[did]/[publication]/[rkey]/PostPubInfo"; +import { ManageSubscription } from "./ManageSubscribe"; + +export const SubscribeButton = (props: { + autoFocus?: boolean; + newsletterMode: boolean; + user: { + loggedIn: boolean; + email: string | undefined; + handle: string | undefined; + subscribed: boolean; + }; +}) => { + return ( + + ) : ( + + Subscribe + + ) + } + > + + + ); +}; + +export const SubscribeInput = (props: { + autoFocus?: boolean; + newsletterMode: boolean; + user: { + loggedIn: boolean; + email: string | undefined; + handle: string | undefined; + subscribed: boolean; + }; +}) => { + let [email, setEmail] = useState(props.user?.email ?? ""); + let [confirmState, setConfirmState] = useState<"confirm" | "success">( + "confirm", + ); + + if (props.user.subscribed) { + return ; + } + if (props.newsletterMode) { + return ( + + subscribe + + } + > + {confirmState === "success" ? ( + + ) : ( + setConfirmState("success")} + /> + )} + + } + /> + ); + } else + return ( + + ); +}; diff --git a/components/ToggleGroup.tsx b/components/ToggleGroup.tsx index ebf786ae..18b5d8dc 100644 --- a/components/ToggleGroup.tsx +++ b/components/ToggleGroup.tsx @@ -6,18 +6,19 @@ export function ToggleGroup(props: { options: { value: T; label: ReactNode }[]; className?: string; optionClassName?: string; + fullWidth?: boolean; }) { return (
{props.options.map((option) => (