Custom Domain

PreviousNext

Vercel Platform custom domain block.

Docs
vercel-platformblock

Preview

Loading preview…
registry/default/blocks/custom-domain.tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { AlertCircle, CheckCircle2, LoaderCircle, XCircle } from "lucide-react";
import { type HTMLAttributes, useState } from "react";
import useSWR, { preload } from "swr";
import { addDomain, getDomainStatus } from "../actions/add-custom-domain";
import { DNSTable } from "./dns-table";

export type InlineSnippetProps = HTMLAttributes<HTMLSpanElement>;

export const InlineSnippet = ({ className, ...props }: InlineSnippetProps) => (
  <span
    className={cn(
      "rounded-md bg-muted px-1 py-0.2 font-mono text-sm",
      className
    )}
    {...props}
  />
);

export const useDomainStatus = (domain: string) =>
  useSWR(`domain-status-${domain}`, () => getDomainStatus(domain), {
    refreshInterval: 20_000,
  });

export const preloadDomainStatus = (domain: string) =>
  preload(`domain-status-${domain}`, () => getDomainStatus(domain));

export type DomainConfigurationProps = HTMLAttributes<HTMLDivElement> & {
  domain: string;
};

export const DomainConfiguration = ({
  domain,
  className,
  ...props
}: DomainConfigurationProps) => {
  const { data, isLoading, mutate, isValidating } = useDomainStatus(domain);

  if (isLoading || !data) {
    return null;
  }

  if (data.status === "Valid Configuration") {
    return (
      <div
        className={cn(
          "flex w-full justify-start gap-2 text-muted-foreground",
          className
        )}
        {...props}
      >
        <DomainStatusIcon domain={domain} />
        <p>Domain is configured correctly.</p>
      </div>
    );
  }

  if (data.status === "Domain is not added") {
    return (
      <div
        className={cn(
          "w-full text-left text-muted-foreground text-sm",
          className
        )}
      >
        <p>Save the domain to add it to your site.</p>
      </div>
    );
  }

  return (
    <div className={cn("w-full space-y-4", className)} {...props}>
      {data.status === "Pending Verification" ? (
        <div className="w-full text-left text-muted-foreground text-sm">
          Please set the following TXT record on{" "}
          <InlineSnippet>{data.dnsRecordsToSet?.name}</InlineSnippet> to prove
          ownership of <InlineSnippet>{domain}</InlineSnippet>:
        </div>
      ) : (
        <div
          className={cn(
            "w-full text-left text-muted-foreground text-sm",
            className
          )}
        >
          Set the following DNS records to your domain provider:
        </div>
      )}

      {data.dnsRecordsToSet && <DNSTable records={[data.dnsRecordsToSet]} />}

      <div className="flex w-full justify-end">
        <Button
          disabled={isValidating}
          onClick={() => mutate()}
          size="sm"
          variant="outline"
        >
          {isValidating ? (
            <>
              <LoaderCircle className="animate-spin" /> Refresh
            </>
          ) : (
            "Refresh"
          )}
        </Button>
      </div>
    </div>
  );
};

export type DomainStatusIconProps = {
  domain: string;
};

export const DomainStatusIcon = ({ domain }: DomainStatusIconProps) => {
  const { data, isLoading } = useDomainStatus(domain);

  if (isLoading) {
    return <LoaderCircle className="animate-spin text-black dark:text-white" />;
  }

  if (data?.status === "Valid Configuration") {
    return (
      <CheckCircle2
        className="text-white dark:text-white"
        fill="#2563EB"
        stroke="currentColor"
      />
    );
  }

  if (data?.status === "Pending Verification") {
    return (
      <AlertCircle
        className="text-white dark:text-black"
        fill="#FBBF24"
        stroke="currentColor"
      />
    );
  }

  if (data?.status === "Domain is not added") {
    return (
      <XCircle
        className="text-white dark:text-black"
        fill="#DC2626"
        stroke="currentColor"
      />
    );
  }

  if (data?.status === "Invalid Configuration") {
    return (
      <XCircle
        className="text-white dark:text-black"
        fill="#DC2626"
        stroke="currentColor"
      />
    );
  }

  return null;
};

export type CustomDomainProps = {
  defaultDomain?: string;
};

export const CustomDomain = (props: CustomDomainProps) => {
  const [domain, setDomain] = useState<string | null>(
    props.defaultDomain ?? null
  );
  const [submitting, setSubmitting] = useState(false);

  return (
    <form
      className="@container w-full max-w-[620px]"
      onSubmit={async (event) => {
        event.preventDefault();
        setSubmitting(true);
        const data = new FormData(event.currentTarget);
        const customDomain = (data.get("customDomain") as string).toLowerCase();
        await addDomain(customDomain);
        await preloadDomainStatus(customDomain);
        setDomain(customDomain);
        setSubmitting(false);
      }}
    >
      <Card className="flex flex-col">
        <CardHeader className="flex flex-col text-left">
          <CardTitle>Custom Domain</CardTitle>
          <CardDescription>The custom domain for your site.</CardDescription>
        </CardHeader>
        <CardContent className="relative flex w-full @sm:flex-row flex-col items-center justify-start @sm:justify-between gap-2">
          <Input
            defaultValue={props.defaultDomain}
            maxLength={64}
            name="customDomain"
            onChange={(e) => {
              e.target.value = e.target.value.toLowerCase();
            }}
            placeholder={"example.com"}
            type="text"
          />
          <Button className="@sm:w-16 w-full" type="submit" variant="outline">
            {submitting ? <LoaderCircle className="animate-spin" /> : "Save"}
          </Button>
        </CardContent>
        {domain && (
          <CardFooter className="flex flex-col gap-4 border-muted border-t-2 pt-4 text-sm">
            <DomainConfiguration domain={domain} />
          </CardFooter>
        )}
      </Card>
    </form>
  );
};

Installation

npx shadcn@latest add @vercel-platform/custom-domain

Usage

import { CustomDomain } from "@/components/custom-domain"
<CustomDomain />