Todos os componentes

Auth Social Accounts

Auth Social Accounts é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

HextaUI MIT other

O que é

Auth Social Accounts é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Não precisa de pacotes além do que o projeto já tem

Licença

Este componente vem de HextaUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Auth Social Accounts

registry/new-york/blocks/auth/auth-social-accounts.tsx
"use client";

import { IconBrandGithub } from "@tabler/icons-react";
import {
  CheckCircle2,
  ExternalLink,
  Loader2,
  Unlink,
  XCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/registry/new-york/ui/alert-dialog";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";

export type SocialProvider =
  | "google"
  | "github"
  | "apple"
  | "microsoft"
  | "twitter";

export interface SocialAccount {
  provider: SocialProvider;
  email?: string;
  name?: string;
  isConnected: boolean;
  isVerified?: boolean;
  isPrimary?: boolean;
  connectedAt?: Date;
}

function formatConnectedDate(date: Date): string {
  const year = date.getFullYear();
  const month = date.toLocaleString("en-US", { month: "short" });
  const day = date.getDate();
  return `${month} ${day}, ${year}`;
}

export interface AuthSocialAccountsProps {
  accounts?: SocialAccount[];
  onConnect?: (provider: SocialProvider) => void;
  onDisconnect?: (provider: SocialProvider) => void;
  onSetPrimary?: (provider: SocialProvider) => void;
  className?: string;
  isLoading?: boolean;
  errors?: {
    general?: string;
  };
}

const GoogleIcon = ({ className }: { className?: string }) => (
  <svg
    className={className}
    viewBox="0 0 24 24"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      d="M12 2a9.96 9.96 0 0 1 6.29 2.226a1 1 0 0 1 .04 1.52l-1.51 1.362a1 1 0 0 1-1.265.06a6 6 0 1 0 2.103 6.836l.001-.004h-3.66a1 1 0 0 1-.992-.883L13 13v-2a1 1 0 0 1 1-1h6.945a1 1 0 0 1 .994.89q.06.55.061 1.11c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2"
      fill="currentColor"
    />
  </svg>
);

const PROVIDER_CONFIG: Record<
  SocialProvider,
  {
    name: string;
    icon: React.ComponentType<{ className?: string }>;
    color: string;
  }
> = {
  google: {
    name: "Google",
    icon: GoogleIcon,
    color: "text-blue-600",
  },
  github: {
    name: "GitHub",
    icon: IconBrandGithub,
    color: "text-foreground",
  },
  apple: {
    name: "Apple",
    icon: ({ className }: { className?: string }) => (
      <svg
        className={className}
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.08-1.85 3.8-3.74 4.25z"
          fill="currentColor"
        />
      </svg>
    ),
    color: "text-foreground",
  },
  microsoft: {
    name: "Microsoft",
    icon: ({ className }: { className?: string }) => (
      <svg
        className={className}
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d="M1 1h10v10H1z" fill="#f25022" />
        <path d="M13 1h10v10H13z" fill="#00a4ef" />
        <path d="M1 13h10v10H1z" fill="#7fba00" />
        <path d="M13 13h10v10H13z" fill="#ffb900" />
      </svg>
    ),
    color: "text-foreground",
  },
  twitter: {
    name: "Twitter",
    icon: ({ className }: { className?: string }) => (
      <svg
        className={className}
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
          fill="currentColor"
        />
      </svg>
    ),
    color: "text-blue-500",
  },
};

interface ErrorAlertProps {
  message: string;
}

function ErrorAlert({ message }: ErrorAlertProps) {
  return (
    <div
      aria-live="polite"
      className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm"
      role="alert"
    >
      {message}
    </div>
  );
}

interface EmptyStateProps {
  message?: string;
}

function EmptyState({
  message = "No social accounts available",
}: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-8 text-center">
      <p className="text-muted-foreground text-sm">{message}</p>
    </div>
  );
}

interface DisconnectDialogProps {
  isLoading: boolean;
  onDisconnect: (provider: SocialProvider) => void;
  providerName: string;
  provider: SocialProvider;
}

function DisconnectDialog({
  isLoading,
  onDisconnect,
  providerName,
  provider,
}: DisconnectDialogProps) {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button
          aria-label={`Disconnect ${providerName}`}
          className="min-h-[32px] min-w-[32px] touch-manipulation"
          disabled={isLoading}
          size="icon"
          type="button"
          variant="ghost"
        >
          <Unlink aria-hidden="true" className="size-4" />
        </Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Disconnect {providerName}?</AlertDialogTitle>
          <AlertDialogDescription>
            This will disconnect your {providerName} account. You&apos;ll need
            to reconnect it to use it for sign-in again.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction
            className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
            onClick={() => onDisconnect(provider)}
          >
            Disconnect
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

interface ConnectButtonProps {
  isLoading: boolean;
  onConnect: (provider: SocialProvider) => void;
  provider: SocialProvider;
  providerName: string;
}

function ConnectButton({
  isLoading,
  onConnect,
  provider,
  providerName,
}: ConnectButtonProps) {
  return (
    <Button
      aria-label={`Connect ${providerName}`}
      className="min-h-[44px] w-full touch-manipulation sm:w-auto"
      disabled={isLoading}
      onClick={() => onConnect(provider)}
      type="button"
    >
      {isLoading ? (
        <>
          <Loader2 aria-hidden="true" className="size-4 animate-spin" />
          Connecting…
        </>
      ) : (
        <>
          <ExternalLink aria-hidden="true" className="size-4" />
          Connect
        </>
      )}
    </Button>
  );
}

interface SetPrimaryButtonProps {
  isLoading: boolean;
  onSetPrimary: (provider: SocialProvider) => void;
  provider: SocialProvider;
  providerName: string;
}

function SetPrimaryButton({
  isLoading,
  onSetPrimary,
  provider,
  providerName,
}: SetPrimaryButtonProps) {
  return (
    <Button
      aria-label={`Set ${providerName} as primary`}
      className="min-h-[44px] w-full touch-manipulation sm:w-auto"
      disabled={isLoading}
      onClick={() => onSetPrimary(provider)}
      type="button"
      variant="outline"
    >
      Set primary
    </Button>
  );
}

interface SocialAccountItemProps {
  account: SocialAccount;
  isLoading: boolean;
  onConnect?: (provider: SocialProvider) => void;
  onDisconnect?: (provider: SocialProvider) => void;
  onSetPrimary?: (provider: SocialProvider) => void;
}

function SocialAccountItem({
  account,
  onConnect,
  onDisconnect,
  onSetPrimary,
  isLoading,
}: SocialAccountItemProps) {
  const config = PROVIDER_CONFIG[account.provider];
  const Icon = config.icon;

  return (
    <div className="flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
      <div className="flex min-w-0 flex-1 items-center gap-3">
        <div
          className={cn(
            "flex size-10 shrink-0 items-center justify-center rounded-lg",
            account.isConnected ? "bg-muted" : "bg-muted/50"
          )}
        >
          <Icon aria-hidden="true" className={cn("size-5", config.color)} />
        </div>
        <div className="flex min-w-0 flex-1 flex-col gap-2">
          <div className="flex flex-wrap items-center gap-2">
            <h4 className="font-medium text-sm">{config.name}</h4>
            {account.isPrimary && (
              <Badge className="text-xs" variant="secondary">
                Primary
              </Badge>
            )}
            {account.isVerified && account.isConnected && (
              <CheckCircle2
                aria-hidden="true"
                className="size-3.5 text-primary"
              />
            )}
            {account.isConnected && !account.isVerified && (
              <XCircle
                aria-hidden="true"
                className="size-3.5 text-yellow-500"
              />
            )}
          </div>
          {account.email && (
            <p className="text-muted-foreground text-xs">{account.email}</p>
          )}
          {account.connectedAt && (
            <p className="text-muted-foreground text-xs">
              Connected {formatConnectedDate(account.connectedAt)}
            </p>
          )}
        </div>
      </div>
      <div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center">
        {account.isConnected ? (
          <>
            {!account.isPrimary && onSetPrimary && (
              <SetPrimaryButton
                isLoading={isLoading}
                onSetPrimary={onSetPrimary}
                provider={account.provider}
                providerName={config.name}
              />
            )}
            {onDisconnect && (
              <DisconnectDialog
                isLoading={isLoading}
                onDisconnect={onDisconnect}
                provider={account.provider}
                providerName={config.name}
              />
            )}
          </>
        ) : (
          onConnect && (
            <ConnectButton
              isLoading={isLoading}
              onConnect={onConnect}
              provider={account.provider}
              providerName={config.name}
            />
          )
        )}
      </div>
    </div>
  );
}

const DEFAULT_ACCOUNTS: SocialAccount[] = [
  { provider: "google", isConnected: false },
  { provider: "github", isConnected: false },
  { provider: "apple", isConnected: false },
  { provider: "microsoft", isConnected: false },
];

function getDisplayAccounts(accounts: SocialAccount[]): SocialAccount[] {
  if (accounts.length > 0) {
    return accounts;
  }
  return DEFAULT_ACCOUNTS.filter(
    (acc) => !accounts.some((a) => a.provider === acc.provider)
  );
}

export default function AuthSocialAccounts({
  accounts = [],
  onConnect,
  onDisconnect,
  onSetPrimary,
  className,
  isLoading = false,
  errors,
}: AuthSocialAccountsProps) {
  const displayAccounts = getDisplayAccounts(accounts);

  return (
    <Card className={cn("w-full max-w-sm shadow-xs", className)}>
      <CardHeader>
        <CardTitle>Connected accounts</CardTitle>
        <CardDescription>
          Manage your social account connections for sign-in
        </CardDescription>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-4">
          {errors?.general && <ErrorAlert message={errors.general} />}

          {displayAccounts.length === 0 ? (
            <EmptyState />
          ) : (
            <div className="flex flex-col gap-3">
              {displayAccounts.map((account) => (
                <SocialAccountItem
                  account={account}
                  isLoading={isLoading}
                  key={account.provider}
                  onConnect={onConnect}
                  onDisconnect={onDisconnect}
                  onSetPrimary={onSetPrimary}
                />
              ))}
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

Componentes parecidos