Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
12 kB · 394 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395import { useState } from "react";import { ButtonPrimary } from "components/Buttons";
import { useSmoker, useToaster } from "components/Toast";import { Input, InputWithLabel } from "components/Input";import useSWR from "swr";import { useIdentityData } from "components/IdentityProvider";import { addDomain } from "actions/domains/addDomain";import { callRPC } from "app/api/rpc/client";import { useLeafletDomains } from "components/PageSWRDataProvider";import { usePublishLink } from ".";import { addDomainPath } from "actions/domains/addDomainPath";import { useReplicache } from "src/replicache";import { deleteDomain } from "actions/domains/deleteDomain";import { AddTiny } from "components/Icons/AddTiny";
type DomainMenuState = | { state: "default"; } | { state: "domain-settings"; domain: string; } | { state: "add-domain"; } | { state: "has-domain"; domain: string; };export function CustomDomainMenu(props: { setShareMenuState: (s: "default") => void;}) { let { data: domains } = useLeafletDomains(); let [state, setState] = useState<DomainMenuState>( domains?.[0] ? { state: "has-domain", domain: domains[0].domain } : { state: "default" }, ); switch (state.state) { case "has-domain": case "default": return ( <DomainOptions setDomainMenuState={setState} domainConnected={false} setShareMenuState={props.setShareMenuState} /> ); case "domain-settings": return ( <DomainSettings domain={state.domain} setDomainMenuState={setState} /> ); case "add-domain": return <AddDomain setDomainMenuState={setState} />; }}
export const DomainOptions = (props: { setShareMenuState: (s: "default") => void; setDomainMenuState: (state: DomainMenuState) => void; domainConnected: boolean;}) => { let { data: domains, mutate: mutateDomains } = useLeafletDomains(); let [selectedDomain, setSelectedDomain] = useState<string | undefined>( domains?.[0]?.domain, ); let [selectedRoute, setSelectedRoute] = useState( domains?.[0]?.route.slice(1) || "", ); let { identity } = useIdentityData(); let { permission_token } = useReplicache();
let toaster = useToaster(); let smoker = useSmoker(); let publishLink = usePublishLink();
return ( <div className="px-3 py-1 flex flex-col gap-3 max-w-full w-[600px]"> <h3 className="text-secondary">Choose a Domain</h3> <div className="flex flex-col gap-1 text-secondary"> {identity?.custom_domains .filter((d) => !d.publication_domains.length) .map((domain) => { return ( <DomainOption selectedRoute={selectedRoute} setSelectedRoute={setSelectedRoute} key={domain.domain} domain={domain.domain} checked={selectedDomain === domain.domain} setChecked={setSelectedDomain} setDomainMenuState={props.setDomainMenuState} /> ); })} <button onMouseDown={() => { props.setDomainMenuState({ state: "add-domain" }); }} className="text-accent-contrast flex gap-2 items-center px-1 py-0.5" > <AddTiny /> Add a New Domain </button> </div>
{/* ONLY SHOW IF A DOMAIN IS CURRENTLY CONNECTED */} <div className="flex gap-3 items-center justify-end"> {props.domainConnected && ( <button onMouseDown={() => { props.setShareMenuState("default"); toaster({ content: ( <div className="font-bold"> Unpublished from custom domain! </div> ), type: "error", }); }} > Unpublish </button> )}
<ButtonPrimary id="publish-to-domain" disabled={ domains?.[0] ? domains[0].domain === selectedDomain && domains[0].route.slice(1) === selectedRoute : !selectedDomain } onClick={async () => { // let rect = document // .getElementById("publish-to-domain") // ?.getBoundingClientRect(); // smoker({ // error: true, // text: "url already in use!", // position: { // x: rect ? rect.left : 0, // y: rect ? rect.top + 26 : 0, // }, // }); if (!selectedDomain || !publishLink) return; await addDomainPath({ domain: selectedDomain, route: "/" + selectedRoute, view_permission_token: publishLink, edit_permission_token: permission_token.id, });
toaster({ content: ( <div className="font-bold"> Published to custom domain!{" "} <a className="underline text-accent-2" href={`https://${selectedDomain}/${selectedRoute}`} target="_blank" > View </a> </div> ), type: "success", }); mutateDomains(); props.setShareMenuState("default"); }} > Publish! </ButtonPrimary> </div> </div> );};
const DomainOption = (props: { selectedRoute: string; setSelectedRoute: (s: string) => void; checked: boolean; setChecked: (checked: string) => void; domain: string; setDomainMenuState: (state: DomainMenuState) => void;}) => { let [value, setValue] = useState(""); let { data } = useSWR(props.domain, async (domain) => { return await callRPC("get_domain_status", { domain }); }); let pending = data?.config?.misconfigured || data?.error; return ( <label htmlFor={props.domain}> <input type="radio" name={props.domain} id={props.domain} value={props.domain} checked={props.checked} className="hidden appearance-none" onChange={() => { if (pending) return; props.setChecked(props.domain); }} /> <div className={` px-[6px] py-1 flex border rounded-md ${ pending ? "border-border-light text-secondary justify-between gap-2 items-center " : !props.checked ? "flex-wrap border-border-light" : "flex-wrap border-accent-1 bg-accent-1 text-accent-2 font-bold" } `} > <div className={`w-max truncate ${pending && "animate-pulse"}`}> {props.domain} </div> {props.checked && ( <div className="flex gap-0 w-full"> <span className="font-normal" style={value === "" ? { opacity: "0.5" } : {}} > / </span>
<Input type="text" autoFocus className="appearance-none focus:outline-hidden font-normal text-accent-2 w-full bg-transparent placeholder:text-accent-2 placeholder:opacity-50" placeholder="add-optional-path" onChange={(e) => props.setSelectedRoute(e.target.value)} value={props.selectedRoute} /> </div> )} {pending && ( <button className="text-accent-contrast text-sm" onMouseDown={() => { props.setDomainMenuState({ state: "domain-settings", domain: props.domain, }); }} > pending </button> )} </div> </label> );};
export const AddDomain = (props: { setDomainMenuState: (state: DomainMenuState) => void;}) => { let [value, setValue] = useState(""); let { mutate } = useIdentityData(); let smoker = useSmoker(); return ( <div className="flex flex-col gap-1 px-3 py-1 max-w-full w-[600px]"> <div> <h3 className="text-secondary">Add a New Domain</h3> <div className="text-xs italic text-secondary"> Don't include the protocol or path, just the base domain name for now </div> </div>
<Input className="input-with-border text-primary" placeholder="www.example.com" value={value} onChange={(e) => setValue(e.target.value)} />
<ButtonPrimary disabled={!value} className="place-self-end mt-2" onMouseDown={async (e) => { // call the vercel api, set the thing... let { error } = await addDomain(value); if (error) { smoker({ error: true, text: error === "invalid_domain" ? "Invalid domain! Use just the base domain" : error === "domain_already_in_use" ? "That domain is already in use!" : "An unknown error occured", position: { y: e.clientY, x: e.clientX - 5, }, }); return; } mutate(); props.setDomainMenuState({ state: "domain-settings", domain: value }); }} > Verify Domain </ButtonPrimary> </div> );};
const DomainSettings = (props: { domain: string; setDomainMenuState: (s: DomainMenuState) => void;}) => { let isSubdomain = props.domain.split(".").length > 2; return ( <div className="flex flex-col gap-1 px-3 py-1 max-w-full w-[600px]"> <h3 className="text-secondary">Verify Domain</h3>
<div className="text-secondary text-sm flex flex-col gap-3"> <div className="flex flex-col gap-[6px]"> <div> To verify this domain, add the following record to your DNS provider for <strong>{props.domain}</strong>. </div>
{isSubdomain ? ( <div className="flex gap-3 p-1 border border-border-light rounded-md py-1"> <div className="flex flex-col "> <div className="text-tertiary">Type</div> <div>CNAME</div> </div> <div className="flex flex-col"> <div className="text-tertiary">Name</div> <div style={{ wordBreak: "break-word" }}> {props.domain.split(".").slice(0, -2).join(".")} </div> </div> <div className="flex flex-col"> <div className="text-tertiary">Value</div> <div style={{ wordBreak: "break-word" }}> cname.vercel-dns.com </div> </div> </div> ) : ( <div className="flex gap-3 p-1 border border-border-light rounded-md py-1"> <div className="flex flex-col "> <div className="text-tertiary">Type</div> <div>A</div> </div> <div className="flex flex-col"> <div className="text-tertiary">Name</div> <div>@</div> </div> <div className="flex flex-col"> <div className="text-tertiary">Value</div> <div>76.76.21.21</div> </div> </div> )} </div> <div> Once you do this, the status may be pending for up to a few hours. </div> <div>Check back later to see if verification was successful.</div> </div>
<div className="flex gap-3 justify-between items-center mt-2"> <button className="text-accent-contrast font-bold " onMouseDown={async () => { await deleteDomain({ domain: props.domain }); props.setDomainMenuState({ state: "default" }); }} > Delete Domain </button> <ButtonPrimary onMouseDown={() => { props.setDomainMenuState({ state: "default" }); }} > Back to Domains </ButtonPrimary> </div> </div> );};