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.
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
"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'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>
);
}