Auth Social Accounts
Auth Social Accounts is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Auth Social Accounts is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
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>
);
}