Compare commits
4 Commits
jazz-run@0
...
emil/auth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccbe849569 | ||
|
|
8323267573 | ||
|
|
a3814532c1 | ||
|
|
f35fdea153 |
21
examples/betterauth/components.json
Normal file
21
examples/betterauth/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -18,15 +18,21 @@
|
||||
"@react-email/components": "^0.0.38",
|
||||
"better-auth": "^1.2.4",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"jazz-betterauth-server-plugin": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jazz-betterauth-client-plugin": "workspace:*",
|
||||
"jazz-betterauth-server-plugin": "workspace:*",
|
||||
"jazz-cloud-ui": "workspace:*",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-betterauth": "workspace:*",
|
||||
"jazz-tools": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"next": "15.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react-dom": "^18.0.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
@@ -38,6 +44,7 @@
|
||||
"@types/react-dom": "^18",
|
||||
"react-email": "^4.0.11",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAccount } from "jazz-react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const { logOut } = useAccount({ resolve: { profile: true } });
|
||||
logOut();
|
||||
redirect("/");
|
||||
}
|
||||
@@ -1,27 +1,124 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss/utilities";
|
||||
@import "tw-animate-css";
|
||||
@source "../../node_modules/jazz-cloud-ui/dist";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { JazzAndAuth } from "@/components/JazzAndAuth";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -24,11 +25,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<JazzAndAuth>{children}</JazzAndAuth>
|
||||
<ThemeProvider attribute="class" defaultTheme="system">
|
||||
<JazzAndAuth>{children}</JazzAndAuth>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import MagicLinkSignIn from "@/components/routes/magic-link/signIn/page";
|
||||
|
||||
export default MagicLinkSignIn;
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import MagicLinkSignUp from "@/components/routes/magic-link/logIn/page";
|
||||
|
||||
export default MagicLinkSignUp;
|
||||
@@ -1,19 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { LogoutButton, UserButton } from "jazz-cloud-ui";
|
||||
import { useAccount, useIsAuthenticated } from "jazz-react";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import Image from "next/image";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const { authClient, account, state } = useAuth();
|
||||
const { account, state } = useAuth();
|
||||
const hasCredentials = state !== "anonymous";
|
||||
const { me, logOut } = useAccount({ resolve: { profile: {} } });
|
||||
const { me } = useAccount({ resolve: { profile: {} } });
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const signOut = useCallback(() => {
|
||||
authClient.signOut().catch(console.error).finally(logOut);
|
||||
}, [logOut, authClient]);
|
||||
console.log("me", me);
|
||||
console.log("account", account);
|
||||
console.log("state", state);
|
||||
@@ -25,10 +22,11 @@ export default function Home() {
|
||||
<header className="absolute p-4 top-0 left-0 w-full z-10 flex items-center justify-between gap-4">
|
||||
<div className="float-start flex gap-4">
|
||||
{me && hasCredentials && isAuthenticated && (
|
||||
<>
|
||||
<Button onClick={signOut}>Sign out</Button>
|
||||
<Button href="/settings">Settings</Button>
|
||||
</>
|
||||
// <>
|
||||
// <LogoutButton className="w-fit" />
|
||||
// <Button href="/settings">Settings</Button>
|
||||
// </>
|
||||
<UserButton settingsUrl="/settings" />
|
||||
)}
|
||||
</div>
|
||||
<div className="float-end flex gap-4">
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import ResetForm from "@/components/forms/Reset";
|
||||
import { ResetPasswordForm } from "jazz-cloud-ui";
|
||||
|
||||
export default function ResetPage() {
|
||||
return <ResetForm />;
|
||||
const props = {
|
||||
signInUrl: "/sign-in",
|
||||
} as Parameters<typeof ResetPasswordForm>["0"];
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<ResetPasswordForm {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import SettingsForm from "@/components/forms/Settings";
|
||||
import { LogoutButton, SettingsForm } from "jazz-cloud-ui";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return <SettingsForm providers={["github"]} />;
|
||||
const props = {
|
||||
providers: ["github"],
|
||||
deleteAccountRedirectUrl: "/",
|
||||
} as Parameters<typeof SettingsForm>["0"];
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<header className="absolute p-4 top-0 left-0 w-fit z-10 flex items-center justify-between gap-4">
|
||||
<LogoutButton className="float-start w-fit" redirectUrl="/" />
|
||||
</header>
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<SettingsForm {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import SignInForm from "@/components/forms/SignIn";
|
||||
import { LoginForm } from "jazz-cloud-ui";
|
||||
|
||||
export default function SignInPage() {
|
||||
return <SignInForm providers={["github"]} />;
|
||||
const props = {
|
||||
redirectUrl: "/",
|
||||
operation: "sign-in",
|
||||
supportOtp: true,
|
||||
supportMagicLink: true,
|
||||
providers: ["github"],
|
||||
signUpUrl: "/sign-up",
|
||||
forgotPasswordUrl: "/forgot",
|
||||
ssoCallbackUrl: `${window.location.origin}`,
|
||||
magicLinkCallbackUrl: `${window.location.origin}`,
|
||||
} as Parameters<typeof LoginForm>["0"];
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<LoginForm {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import SignUpForm from "@/components/forms/SignUp";
|
||||
import { LoginForm } from "jazz-cloud-ui";
|
||||
|
||||
export default function SignUpPage() {
|
||||
return <SignUpForm providers={["github"]} />;
|
||||
const props = {
|
||||
redirectUrl: "/",
|
||||
operation: "sign-up",
|
||||
supportOtp: true,
|
||||
supportMagicLink: true,
|
||||
providers: ["github"],
|
||||
signInUrl: "/sign-in",
|
||||
forgotPasswordUrl: "/forgot",
|
||||
ssoCallbackUrl: `${window.location.origin}`,
|
||||
magicLinkCallbackUrl: `${window.location.origin}`,
|
||||
} as Parameters<typeof LoginForm>["0"];
|
||||
return (
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<LoginForm {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const { logIn } = useAuth();
|
||||
logIn().then(redirect("/"));
|
||||
return null;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const { signIn } = useAuth();
|
||||
signIn().then(redirect("/"));
|
||||
return null;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { AccountsType, useAuth } from "jazz-react-auth-betterauth";
|
||||
|
||||
export const AccountProviders = ({
|
||||
setLoading,
|
||||
setError,
|
||||
accounts,
|
||||
}: {
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
accounts: AccountsType | undefined;
|
||||
}) => {
|
||||
const auth = useAuth();
|
||||
return (
|
||||
<table className="w-full text-sm border-full border-collapse">
|
||||
<thead className="text-xs">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Provider
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Updated
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Scopes
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!accounts?.data?.length && "No authentication providers found"}
|
||||
{accounts?.data &&
|
||||
accounts.data.map((account) => (
|
||||
<tr key={account.id} className="border-b">
|
||||
<th
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium whitespace-nowrap"
|
||||
>
|
||||
{account.provider}
|
||||
</th>
|
||||
<td className="px-6 py-4">
|
||||
{account.createdAt.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{account.updatedAt.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4">{account.scopes.join(", ")}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await auth.authClient.unlinkAccount({
|
||||
providerId: account.provider,
|
||||
accountId: account.id,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
Unlink
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
import { AccountProviders } from "@/components/AccountProviders";
|
||||
import { Button } from "@/components/Button";
|
||||
import { DeleteAccountButton } from "@/components/DeleteAccountButton";
|
||||
import { Loading } from "@/components/Loading";
|
||||
import { SSOButton } from "@/components/SSOButton";
|
||||
import { useAccount, useIsAuthenticated } from "jazz-react";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import type {
|
||||
AccountsType,
|
||||
FullAuthClient,
|
||||
SSOProviderType,
|
||||
} from "jazz-react-auth-betterauth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
export default function SettingsForm({
|
||||
providers,
|
||||
}: {
|
||||
providers?: SSOProviderType[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { authClient, account, state } = useAuth();
|
||||
const hasCredentials = state !== "anonymous";
|
||||
|
||||
const [accounts, setAccounts] = useState<AccountsType | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
return authClient.useSession.subscribe(() => {
|
||||
authClient.listAccounts().then((x) => setAccounts(x));
|
||||
});
|
||||
}, [authClient]);
|
||||
|
||||
const [status, setStatus] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [otpSentStatus, setOtpSentStatus] = useState<boolean>(false);
|
||||
const [otpStatus, setOtpStatus] = useState<boolean>(false);
|
||||
const [otp, setOtp] = useState<string>("");
|
||||
|
||||
const { me, logOut } = useAccount({ resolve: { profile: true } });
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const signOut = useCallback(() => {
|
||||
authClient
|
||||
.signOut()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
logOut();
|
||||
router.push("/");
|
||||
});
|
||||
}, [logOut, authClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="absolute p-4 top-0 left-0 w-full z-10 flex items-center justify-between gap-4">
|
||||
<div className="float-start">
|
||||
{me && hasCredentials && account && isAuthenticated && (
|
||||
<Button className="float-start" onClick={signOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-screen flex flex-col justify-center font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
|
||||
<h1 className="text-stone-950 dark:text-white font-display text-5xl lg:text-6xl font-medium tracking-tighter mb-2">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{status && account && !account?.emailVerified && (
|
||||
<div>
|
||||
Instructions to verify your account have been sent to{" "}
|
||||
{account.email}, if an account with that email address exists.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(status || otpStatus) && account && account.emailVerified && (
|
||||
<div>Your account has been successfully verified.</div>
|
||||
)}
|
||||
|
||||
{error && <div>{error.message}</div>}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
<AccountProviders
|
||||
accounts={accounts}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
{accounts?.data &&
|
||||
providers?.map((x) => {
|
||||
return (
|
||||
accounts.data.find((y) => y.provider === x) === undefined && (
|
||||
<SSOButton
|
||||
link={true}
|
||||
provider={x}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
{account && account.emailVerified && <p>Account verified.</p>}
|
||||
{account && !account.emailVerified && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } =
|
||||
await authClient.sendVerificationEmail({
|
||||
email: account.email,
|
||||
callbackURL: `${window.location.origin}`,
|
||||
});
|
||||
setStatus(data?.status ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
Send verification link
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } = await (
|
||||
authClient as FullAuthClient
|
||||
).emailOtp.sendVerificationOtp({
|
||||
email: account.email,
|
||||
type: "email-verification",
|
||||
});
|
||||
setStatus(data?.success ?? false);
|
||||
setOtpSentStatus(data?.success ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
Send verification one-time password
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{otpSentStatus && account && !account.emailVerified && (
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } = await (
|
||||
authClient as FullAuthClient
|
||||
).emailOtp.verifyEmail({
|
||||
email: account.email,
|
||||
otp: otp,
|
||||
});
|
||||
setOtpStatus(data?.status ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="otp">One-time password</label>
|
||||
<input
|
||||
id="otp"
|
||||
value={otp}
|
||||
disabled={loading}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type={"submit"} disabled={loading}>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
<DeleteAccountButton
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
callbackURL={`${window.location.origin}/delete-account`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Loading } from "@/components/Loading";
|
||||
import { SSOButton } from "@/components/SSOButton";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import type {
|
||||
FullAuthClient,
|
||||
SSOProviderType,
|
||||
} from "jazz-react-auth-betterauth";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
const title = "Sign In";
|
||||
|
||||
export default function SignInForm({
|
||||
providers,
|
||||
}: {
|
||||
providers?: SSOProviderType[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [otp, setOtp] = useState<string>("");
|
||||
const [otpStatus, setOtpStatus] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center">
|
||||
<h1 className="sr-only">{title}</h1>
|
||||
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
|
||||
{otpStatus && (
|
||||
<div>A one-time password has been sent to your email.</div>
|
||||
)}
|
||||
|
||||
{error && <div>{error.message}</div>}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (!otpStatus) {
|
||||
await auth.authClient.signIn.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
rememberMe,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await auth.logIn();
|
||||
router.push("/");
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.emailOtp({
|
||||
email: email,
|
||||
otp: otp,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
if (data) {
|
||||
await auth.logIn();
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="email-address">Email address</label>
|
||||
<input
|
||||
id="email-address"
|
||||
value={email}
|
||||
disabled={loading}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{!otpStatus && (
|
||||
<div>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
disabled={loading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{otpStatus && (
|
||||
<div>
|
||||
<label htmlFor="otp">One-time password</label>
|
||||
<input
|
||||
id="otp"
|
||||
value={otp}
|
||||
disabled={loading}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="items-center">
|
||||
<div>
|
||||
<label htmlFor="remember-me">Remember me</label>
|
||||
<input
|
||||
id="remember-me"
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
disabled={loading}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<Link href="/forgot" className="text-sm float-right">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading}>
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<hr className="flex-1" />
|
||||
<p className="text-center">or</p>
|
||||
<hr className="flex-1" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{providers?.map((x) => {
|
||||
return (
|
||||
<SSOButton
|
||||
callbackURL={`${window.location.origin}/social/logIn`}
|
||||
provider={x}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.magicLink({
|
||||
email: email,
|
||||
callbackURL: `${window.location.origin}/magic-link/logIn`,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/link.svg"
|
||||
alt="Link icon"
|
||||
className="absolute left-3"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Sign in with magic link
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).emailOtp.sendVerificationOtp({
|
||||
email: email,
|
||||
type: "sign-in",
|
||||
});
|
||||
setOtpStatus(data?.success ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/mail.svg"
|
||||
alt="Mail icon"
|
||||
className="absolute left-3"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Sign in with one-time password
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm">
|
||||
Don't have an account? <Link href="/sign-up">Sign up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Loading } from "@/components/Loading";
|
||||
import { SSOButton } from "@/components/SSOButton";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import type {
|
||||
FullAuthClient,
|
||||
SSOProviderType,
|
||||
} from "jazz-react-auth-betterauth";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
const title = "Sign Up";
|
||||
|
||||
export default function SignUpForm({
|
||||
providers,
|
||||
}: {
|
||||
providers?: SSOProviderType[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const [name, setName] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
const [otp, setOtp] = useState<string>("");
|
||||
const [otpStatus, setOtpStatus] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center">
|
||||
<h1 className="sr-only">{title}</h1>
|
||||
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
|
||||
{otpStatus && (
|
||||
<div>A one-time password has been sent to your email.</div>
|
||||
)}
|
||||
|
||||
{error && <div>{error.message}</div>}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
<form
|
||||
className="flex flex-col gap-6"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (password !== confirmPassword) {
|
||||
setError(new Error("Passwords do not match"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!otpStatus) {
|
||||
await auth.authClient.signUp.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await auth.signIn();
|
||||
router.push("/");
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.emailOtp({
|
||||
email: email,
|
||||
otp: otp,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
if (data) {
|
||||
await auth.signIn();
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="full-name">Full name</label>
|
||||
<input
|
||||
id="full-name"
|
||||
value={name}
|
||||
disabled={loading}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email-address">Email address</label>
|
||||
<input
|
||||
id="email-address"
|
||||
value={email}
|
||||
disabled={loading}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{!otpStatus && (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
disabled={loading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="confirm-password">Confirm password</label>
|
||||
<input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
disabled={loading}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{otpStatus && (
|
||||
<div>
|
||||
<label htmlFor="otp">One-time password</label>
|
||||
<input
|
||||
id="otp"
|
||||
value={otp}
|
||||
disabled={loading}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" disabled={loading}>
|
||||
Sign up
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<hr className="flex-1" />
|
||||
<p className="text-center">or</p>
|
||||
<hr className="flex-1" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{providers?.map((x) => {
|
||||
return (
|
||||
<SSOButton
|
||||
callbackURL={`${window.location.origin}/social/signIn`}
|
||||
provider={x}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.magicLink({
|
||||
email: email,
|
||||
callbackURL: `${window.location.origin}/magic-link/signIn`,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/link.svg"
|
||||
alt="Link icon"
|
||||
className="absolute left-3"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Sign up with magic link
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).emailOtp.sendVerificationOtp({
|
||||
email: email,
|
||||
type: "sign-in",
|
||||
});
|
||||
setOtpStatus(data?.success ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/mail.svg"
|
||||
alt="Mail icon"
|
||||
className="absolute left-3"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Sign up with one-time password
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm">
|
||||
Already have an account? <Link href="/sign-in">Sign in</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const error = searchParams.get("error");
|
||||
if (!error) {
|
||||
auth.logIn().then(() => router.push("/"));
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center">
|
||||
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
|
||||
<div>{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const error = searchParams.get("error");
|
||||
if (!error) {
|
||||
auth.signIn().then(() => router.push("/"));
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center">
|
||||
<div className="max-w-md flex flex-col gap-8 w-full px-6 py-12 mx-auto">
|
||||
<div>{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
11
examples/betterauth/src/components/theme-provider.tsx
Normal file
11
examples/betterauth/src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import * as React from "react";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
6
examples/betterauth/src/lib/utils.ts
Normal file
6
examples/betterauth/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
176
homepage/pnpm-lock.yaml
generated
176
homepage/pnpm-lock.yaml
generated
@@ -13,7 +13,7 @@ importers:
|
||||
version: 1.4.0
|
||||
'@headlessui/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@icons-pack/react-simple-icons':
|
||||
specifier: ^9.1.0
|
||||
version: 9.7.0(react@18.3.1)
|
||||
@@ -71,7 +71,7 @@ importers:
|
||||
version: 3.0.19(postcss@8.5.3)
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.17.46
|
||||
version: 20.17.47
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.3.21
|
||||
@@ -159,7 +159,7 @@ importers:
|
||||
version: 1.9.4
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.17.46
|
||||
version: 20.17.47
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.3.21
|
||||
@@ -192,7 +192,7 @@ importers:
|
||||
version: link:../design-system
|
||||
'@headlessui/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@icons-pack/react-simple-icons':
|
||||
specifier: ^9.1.0
|
||||
version: 9.7.0(react@18.3.1)
|
||||
@@ -210,10 +210,10 @@ importers:
|
||||
version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.21))(@types/react@18.3.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@shikijs/transformers':
|
||||
specifier: ^3.2.1
|
||||
version: 3.4.0
|
||||
version: 3.4.1
|
||||
'@shikijs/twoslash':
|
||||
specifier: ^3.2.1
|
||||
version: 3.4.0(typescript@5.7.3)
|
||||
version: 3.4.1(typescript@5.7.3)
|
||||
'@stefanprobst/rehype-extract-toc':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.1
|
||||
@@ -309,7 +309,7 @@ importers:
|
||||
version: 18.3.1(react@18.3.1)
|
||||
shiki:
|
||||
specifier: ^3.2.1
|
||||
version: 3.4.0
|
||||
version: 3.4.1
|
||||
tailwind-merge:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
@@ -328,7 +328,7 @@ importers:
|
||||
version: 7946.0.16
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.17.46
|
||||
version: 20.17.47
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.3.21
|
||||
@@ -497,8 +497,8 @@ packages:
|
||||
'@gerrit0/mini-shiki@1.27.2':
|
||||
resolution: {integrity: sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==}
|
||||
|
||||
'@headlessui/react@2.2.2':
|
||||
resolution: {integrity: sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==}
|
||||
'@headlessui/react@2.2.3':
|
||||
resolution: {integrity: sha512-hgOJGXPifPlOczIeSwX8OjLWRJ5XdYApZFf7DeCbCrO1PXHkPhNTRrA9ZwJsgAG7SON1i2JcvIreF/kbgtJeaQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
@@ -1128,37 +1128,37 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@shikijs/core@3.4.0':
|
||||
resolution: {integrity: sha512-0YOzTSRDn/IAfQWtK791gs1u8v87HNGToU6IwcA3K7nPoVOrS2Dh6X6A6YfXgPTSkTwR5y6myk0MnI0htjnwrA==}
|
||||
'@shikijs/core@3.4.1':
|
||||
resolution: {integrity: sha512-GCqSd3KXRTKX1sViP7fIyyyf6do2QVg+fTd4IT00ucYCVSKiSN8HbFbfyjGsoZePNKWcQqXe4U4rrz2IVldG5A==}
|
||||
|
||||
'@shikijs/engine-javascript@3.4.0':
|
||||
resolution: {integrity: sha512-1ywDoe+z/TPQKj9Jw0eU61B003J9DqUFRfH+DVSzdwPUFhR7yOmfyLzUrFz0yw8JxFg/NgzXoQyyykXgO21n5Q==}
|
||||
'@shikijs/engine-javascript@3.4.1':
|
||||
resolution: {integrity: sha512-oGvRqN3Bsk+cGzmCb/5Kt/LfD7uyA8vCUUawyqmLti/AYNV7++zIZFEW8JwW5PrpPNWWx9RcZ/chnYLedzlVIQ==}
|
||||
|
||||
'@shikijs/engine-oniguruma@1.29.2':
|
||||
resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==}
|
||||
|
||||
'@shikijs/engine-oniguruma@3.4.0':
|
||||
resolution: {integrity: sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==}
|
||||
'@shikijs/engine-oniguruma@3.4.1':
|
||||
resolution: {integrity: sha512-p8I5KWgEDUcXRif9JjJUZtNeqCyxZ8xcslecDJMigsqSZfokwqQIsH4aGpdjzmDf8LIWvT+C3TCxnJQVaPmCbQ==}
|
||||
|
||||
'@shikijs/langs@3.4.0':
|
||||
resolution: {integrity: sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==}
|
||||
'@shikijs/langs@3.4.1':
|
||||
resolution: {integrity: sha512-v5A5ApJYcrcPLHcwAi0bViUU+Unh67UaXU9gGX3qfr2z3AqlqSZbC00W/3J4+tfGJASzwrWDro2R1er6SsCL1Q==}
|
||||
|
||||
'@shikijs/themes@3.4.0':
|
||||
resolution: {integrity: sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==}
|
||||
'@shikijs/themes@3.4.1':
|
||||
resolution: {integrity: sha512-XOJgs55mVVMZtNVJx1NVmdcfXG9HIyZGh7qpCw/Ok5UMjWgkmb8z15TgcmF3ItvHItijiIMl9BLcNO/tFSGl1w==}
|
||||
|
||||
'@shikijs/transformers@3.4.0':
|
||||
resolution: {integrity: sha512-GrGaOj1/I6h75IU0VvjdWDpqGCynx0bqHzd1rErBTGxrcmusYIBhrV7aEySWyJ6HHb9figeXfcNxUFS1HKUfBw==}
|
||||
'@shikijs/transformers@3.4.1':
|
||||
resolution: {integrity: sha512-Z3lbVQXHiXLC0bjDuGqsF3AAvvv4lQMoAXqIINZiOBgsk6CrnPZy0E2A+QUqv/MUaqp5qtYGsKDUJWbFE+buXw==}
|
||||
|
||||
'@shikijs/twoslash@3.4.0':
|
||||
resolution: {integrity: sha512-RM15Q6XK+renUX7tN/iUYR2W1qSojTm6kcJwD1FEP0YQoMn7E6Ogr9CqHNYfdDpT7EZBJvx0N96E/pTymWpSuQ==}
|
||||
'@shikijs/twoslash@3.4.1':
|
||||
resolution: {integrity: sha512-i6C+sxAK5age1ifiVqyUQSVEj+l4NdMnqc2QN0wN4hsNM43Q/ogYbNxXQHvqWKwN+TwwuTMCaT15ziPaUbfj4A==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.5.0'
|
||||
|
||||
'@shikijs/types@1.29.2':
|
||||
resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==}
|
||||
|
||||
'@shikijs/types@3.4.0':
|
||||
resolution: {integrity: sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==}
|
||||
'@shikijs/types@3.4.1':
|
||||
resolution: {integrity: sha512-4flT+pToGqRBb0UhGqXTV7rCqUS3fhc8z3S2Djc3E5USKhXwadeKGFVNB2rKXfohlrEozNJMtMiZaN8lfdj/ZQ==}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
@@ -1590,8 +1590,8 @@ packages:
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/node@20.17.46':
|
||||
resolution: {integrity: sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==}
|
||||
'@types/node@20.17.47':
|
||||
resolution: {integrity: sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==}
|
||||
|
||||
'@types/prop-types@15.7.14':
|
||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||
@@ -1852,8 +1852,8 @@ packages:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001717:
|
||||
resolution: {integrity: sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==}
|
||||
caniuse-lite@1.0.30001718:
|
||||
resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
@@ -1939,8 +1939,8 @@ packages:
|
||||
d3-voronoi@1.1.2:
|
||||
resolution: {integrity: sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==}
|
||||
|
||||
debug@4.4.0:
|
||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
@@ -2005,8 +2005,8 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
electron-to-chromium@1.5.151:
|
||||
resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==}
|
||||
electron-to-chromium@1.5.155:
|
||||
resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
@@ -3029,8 +3029,8 @@ packages:
|
||||
shiki@0.14.7:
|
||||
resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==}
|
||||
|
||||
shiki@3.4.0:
|
||||
resolution: {integrity: sha512-Ni80XHcqhOEXv5mmDAvf5p6PAJqbUc/RzFeaOqk+zP5DLvTPS3j0ckvA+MI87qoxTQ5RGJDVTbdl/ENLSyyAnQ==}
|
||||
shiki@3.4.1:
|
||||
resolution: {integrity: sha512-PSnoczt+iWIOB4iRQ+XVPFtTuN1FcmuYzPgUBZTSv5pC6CozssIx2M4O5n4S9gJlUu9A3FxMU0ZPaHflky/6LA==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
@@ -3167,8 +3167,8 @@ packages:
|
||||
uglify-js:
|
||||
optional: true
|
||||
|
||||
terser@5.39.0:
|
||||
resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==}
|
||||
terser@5.39.2:
|
||||
resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3377,9 +3377,9 @@ packages:
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
yaml@2.7.1:
|
||||
resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
|
||||
engines: {node: '>= 14'}
|
||||
yaml@2.8.0:
|
||||
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
@@ -3525,7 +3525,7 @@ snapshots:
|
||||
'@shikijs/types': 1.29.2
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@headlessui/react@2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
'@headlessui/react@2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@react-aria/focus': 3.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -4066,16 +4066,16 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@shikijs/core@3.4.0':
|
||||
'@shikijs/core@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/types': 3.4.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@3.4.0':
|
||||
'@shikijs/engine-javascript@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/types': 3.4.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 4.3.3
|
||||
|
||||
@@ -4084,28 +4084,28 @@ snapshots:
|
||||
'@shikijs/types': 1.29.2
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/engine-oniguruma@3.4.0':
|
||||
'@shikijs/engine-oniguruma@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/types': 3.4.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@3.4.0':
|
||||
'@shikijs/langs@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/types': 3.4.1
|
||||
|
||||
'@shikijs/themes@3.4.0':
|
||||
'@shikijs/themes@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/types': 3.4.1
|
||||
|
||||
'@shikijs/transformers@3.4.0':
|
||||
'@shikijs/transformers@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/core': 3.4.0
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/core': 3.4.1
|
||||
'@shikijs/types': 3.4.1
|
||||
|
||||
'@shikijs/twoslash@3.4.0(typescript@5.7.3)':
|
||||
'@shikijs/twoslash@3.4.1(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@shikijs/core': 3.4.0
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/core': 3.4.1
|
||||
'@shikijs/types': 3.4.1
|
||||
twoslash: 0.3.1(typescript@5.7.3)
|
||||
typescript: 5.7.3
|
||||
transitivePeerDependencies:
|
||||
@@ -4116,7 +4116,7 @@ snapshots:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/types@3.4.0':
|
||||
'@shikijs/types@3.4.1':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
@@ -5334,7 +5334,7 @@ snapshots:
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node@20.17.46':
|
||||
'@types/node@20.17.47':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
@@ -5365,26 +5365,26 @@ snapshots:
|
||||
'@typescript/twoslash@3.1.0':
|
||||
dependencies:
|
||||
'@typescript/vfs': 1.3.5
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
lz-string: 1.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript/vfs@1.3.4':
|
||||
dependencies:
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript/vfs@1.3.5':
|
||||
dependencies:
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript/vfs@1.6.1(typescript@5.7.3)':
|
||||
dependencies:
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
typescript: 5.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -5547,7 +5547,7 @@ snapshots:
|
||||
autoprefixer@10.4.21(postcss@8.5.3):
|
||||
dependencies:
|
||||
browserslist: 4.24.5
|
||||
caniuse-lite: 1.0.30001717
|
||||
caniuse-lite: 1.0.30001718
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.1.1
|
||||
@@ -5594,8 +5594,8 @@ snapshots:
|
||||
|
||||
browserslist@4.24.5:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001717
|
||||
electron-to-chromium: 1.5.151
|
||||
caniuse-lite: 1.0.30001718
|
||||
electron-to-chromium: 1.5.155
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.24.5)
|
||||
|
||||
@@ -5609,7 +5609,7 @@ snapshots:
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001717: {}
|
||||
caniuse-lite@1.0.30001718: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
@@ -5698,7 +5698,7 @@ snapshots:
|
||||
|
||||
d3-voronoi@1.1.2: {}
|
||||
|
||||
debug@4.4.0:
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
@@ -5751,7 +5751,7 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
electron-to-chromium@1.5.151: {}
|
||||
electron-to-chromium@1.5.155: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
@@ -6038,7 +6038,7 @@ snapshots:
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 20.17.46
|
||||
'@types/node': 20.17.47
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
@@ -6690,7 +6690,7 @@ snapshots:
|
||||
micromark@3.2.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
decode-named-character-reference: 1.1.0
|
||||
micromark-core-commonmark: 1.1.0
|
||||
micromark-factory-space: 1.1.0
|
||||
@@ -6712,7 +6712,7 @@ snapshots:
|
||||
micromark@4.0.2:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.0
|
||||
debug: 4.4.1
|
||||
decode-named-character-reference: 1.1.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
@@ -6787,7 +6787,7 @@ snapshots:
|
||||
'@next/env': 14.2.15
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001717
|
||||
caniuse-lite: 1.0.30001718
|
||||
graceful-fs: 4.2.11
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
@@ -6812,7 +6812,7 @@ snapshots:
|
||||
'@next/env': 14.2.7
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001717
|
||||
caniuse-lite: 1.0.30001718
|
||||
graceful-fs: 4.2.11
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
@@ -6838,7 +6838,7 @@ snapshots:
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001717
|
||||
caniuse-lite: 1.0.30001718
|
||||
postcss: 8.4.31
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
@@ -6957,7 +6957,7 @@ snapshots:
|
||||
postcss-load-config@4.0.2(postcss@8.5.3):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
yaml: 2.7.1
|
||||
yaml: 2.8.0
|
||||
optionalDependencies:
|
||||
postcss: 8.5.3
|
||||
|
||||
@@ -7203,14 +7203,14 @@ snapshots:
|
||||
vscode-oniguruma: 1.7.0
|
||||
vscode-textmate: 8.0.0
|
||||
|
||||
shiki@3.4.0:
|
||||
shiki@3.4.1:
|
||||
dependencies:
|
||||
'@shikijs/core': 3.4.0
|
||||
'@shikijs/engine-javascript': 3.4.0
|
||||
'@shikijs/engine-oniguruma': 3.4.0
|
||||
'@shikijs/langs': 3.4.0
|
||||
'@shikijs/themes': 3.4.0
|
||||
'@shikijs/types': 3.4.0
|
||||
'@shikijs/core': 3.4.1
|
||||
'@shikijs/engine-javascript': 3.4.1
|
||||
'@shikijs/engine-oniguruma': 3.4.1
|
||||
'@shikijs/langs': 3.4.1
|
||||
'@shikijs/themes': 3.4.1
|
||||
'@shikijs/types': 3.4.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
@@ -7342,10 +7342,10 @@ snapshots:
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.2
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.39.0
|
||||
terser: 5.39.2
|
||||
webpack: 5.99.8
|
||||
|
||||
terser@5.39.0:
|
||||
terser@5.39.2:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
acorn: 8.14.1
|
||||
@@ -7407,7 +7407,7 @@ snapshots:
|
||||
markdown-it: 14.1.0
|
||||
minimatch: 9.0.5
|
||||
typescript: 5.7.3
|
||||
yaml: 2.7.1
|
||||
yaml: 2.8.0
|
||||
|
||||
typescript@5.7.3: {}
|
||||
|
||||
@@ -7605,7 +7605,7 @@ snapshots:
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
yaml@2.7.1: {}
|
||||
yaml@2.8.0: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
|
||||
41
packages/jazz-cloud-ui/.gitignore
vendored
Normal file
41
packages/jazz-cloud-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
2
packages/jazz-cloud-ui/.npmignore
Normal file
2
packages/jazz-cloud-ui/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
coverage
|
||||
node_modules
|
||||
19
packages/jazz-cloud-ui/LICENSE.txt
Normal file
19
packages/jazz-cloud-ui/LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2025, Garden Computing, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
16
packages/jazz-cloud-ui/eslint.config.mjs
Normal file
16
packages/jazz-cloud-ui/eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
57
packages/jazz-cloud-ui/package.json
Normal file
57
packages/jazz-cloud-ui/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "jazz-cloud-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist",
|
||||
"dev": " tsc --sourceMap --outDir dist --watch",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@icons-pack/react-simple-icons": "^12.8.0",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-betterauth": "workspace:*",
|
||||
"lucide-react": "^0.510.0",
|
||||
"next": "^15",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@icons-pack/react-simple-icons": "^12.8.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.510.0",
|
||||
"next": "^15",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
||||
"@types/react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
371
packages/jazz-cloud-ui/src/components/login-form.tsx
Normal file
371
packages/jazz-cloud-ui/src/components/login-form.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
"use client";
|
||||
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import {
|
||||
FullAuthClient,
|
||||
SSOProviderType,
|
||||
useAuth,
|
||||
} from "jazz-react-auth-betterauth";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useId, useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert.js";
|
||||
import { Button } from "../components/ui/button.js";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../components/ui/card.js";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "../components/ui/input-otp.js";
|
||||
import { Input } from "../components/ui/input.js";
|
||||
import { Label } from "../components/ui/label.js";
|
||||
import { SSOButton } from "../components/ui/sso-button.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { MagicLinkButton } from "./ui/magic-link-button.js";
|
||||
import { SendOtpButton } from "./ui/send-otp-button.js";
|
||||
|
||||
type LoginFormProps = {
|
||||
operation: "sign-in" | "sign-up";
|
||||
supportOtp: boolean;
|
||||
supportMagicLink: boolean;
|
||||
providers?: SSOProviderType[];
|
||||
redirectUrl?: string;
|
||||
footer?: React.ReactNode;
|
||||
ssoCallbackUrl?: string;
|
||||
magicLinkCallbackUrl?: string;
|
||||
signUpUrl?: string;
|
||||
signInUrl?: string;
|
||||
forgotPasswordUrl?: string;
|
||||
};
|
||||
|
||||
export function LoginForm({
|
||||
className,
|
||||
operation,
|
||||
supportOtp = false,
|
||||
supportMagicLink = false,
|
||||
providers,
|
||||
redirectUrl,
|
||||
footer,
|
||||
ssoCallbackUrl,
|
||||
magicLinkCallbackUrl,
|
||||
signUpUrl,
|
||||
signInUrl,
|
||||
forgotPasswordUrl,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & LoginFormProps) {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [otp, setOtp] = useState<string>("");
|
||||
const [otpStatus, setOtpStatus] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
|
||||
const submitSignIn = async () => {
|
||||
if (supportOtp && !otpStatus) {
|
||||
await auth.authClient.signIn.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
rememberMe,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
if (redirectUrl) router.push(redirectUrl);
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.emailOtp({
|
||||
email: email,
|
||||
otp: otp,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
if (data && redirectUrl) {
|
||||
router.push(redirectUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submitSignUp = async () => {
|
||||
if (password !== confirmPassword) {
|
||||
setError(new Error("Passwords do not match"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (supportOtp && !otpStatus) {
|
||||
await auth.authClient.signUp.email(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
if (redirectUrl) router.push(redirectUrl);
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.error);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.emailOtp({
|
||||
email: email,
|
||||
otp: otp,
|
||||
});
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
if (data && redirectUrl) {
|
||||
router.push(redirectUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
{otpStatus && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>OTP</AlertTitle>
|
||||
<AlertDescription>
|
||||
A one-time password has been sent to your email.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">
|
||||
{operation === "sign-in" ? "Welcome back" : "Greetings"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (operation === "sign-in") {
|
||||
await submitSignIn();
|
||||
} else if (operation === "sign-up") {
|
||||
await submitSignUp();
|
||||
}
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-6">
|
||||
{(supportOtp || providers || supportMagicLink) && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{supportOtp && (
|
||||
<SendOtpButton
|
||||
operation={operation}
|
||||
email={email}
|
||||
setOtpStatus={setOtpStatus}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
{supportMagicLink && (
|
||||
<MagicLinkButton
|
||||
callbackURL={magicLinkCallbackUrl}
|
||||
operation={operation}
|
||||
email={email}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
{providers?.map((x) => {
|
||||
return (
|
||||
<SSOButton
|
||||
key={useId()}
|
||||
callbackURL={ssoCallbackUrl}
|
||||
operation={operation}
|
||||
provider={x}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
||||
<span className="bg-card text-muted-foreground relative z-10 px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
{operation === "sign-up" && (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
disabled={loading}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
value={email}
|
||||
disabled={loading}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
{!otpStatus && <Label htmlFor="password">Password</Label>}
|
||||
{supportOtp && otpStatus && (
|
||||
<Label htmlFor="otp">One-time password</Label>
|
||||
)}
|
||||
{!otpStatus &&
|
||||
forgotPasswordUrl &&
|
||||
operation === "sign-in" && (
|
||||
<a
|
||||
href={forgotPasswordUrl}
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{!otpStatus && (
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
disabled={loading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{supportOtp && otpStatus && (
|
||||
<InputOTP
|
||||
id="otp"
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
value={otp}
|
||||
disabled={loading}
|
||||
onChange={(value) => setOtp(value)}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
)}
|
||||
{operation === "sign-in" && (
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="remember-me">Remember me</Label>
|
||||
<Input
|
||||
id="remember-me"
|
||||
type="checkbox"
|
||||
className="w-1/6 ml-auto"
|
||||
checked={rememberMe}
|
||||
disabled={loading}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!otpStatus && operation === "sign-up" && (
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirm-password">Confirm password</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
disabled={loading}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{operation === "sign-in" ? "Login" : "Register"}
|
||||
</Button>
|
||||
</div>
|
||||
{operation === "sign-in" && signUpUrl && (
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href={signUpUrl} className="underline underline-offset-4">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{operation === "sign-up" && signInUrl && (
|
||||
<div className="text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<a href={signInUrl} className="underline underline-offset-4">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{footer && (
|
||||
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
packages/jazz-cloud-ui/src/components/reset-password-form.tsx
Normal file
146
packages/jazz-cloud-ui/src/components/reset-password-form.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert.js";
|
||||
import { Button } from "../components/ui/button.js";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../components/ui/card.js";
|
||||
import { Input } from "../components/ui/input.js";
|
||||
import { Label } from "../components/ui/label.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
|
||||
type ResetPasswordFormProps = {
|
||||
signInUrl?: string;
|
||||
};
|
||||
|
||||
export function ResetPasswordForm({
|
||||
className,
|
||||
signInUrl,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & ResetPasswordFormProps) {
|
||||
const auth = useAuth();
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const token = searchParams.get("token");
|
||||
const initialError = searchParams.get("error");
|
||||
const [error, setError] = useState<Error | undefined>(
|
||||
initialError
|
||||
? {
|
||||
name: initialError,
|
||||
message: initialError,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
||||
const [status, setStatus] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
{status && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Password Reset</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your password has been reset. You may now{" "}
|
||||
{signInUrl ? (
|
||||
<span>
|
||||
<Link href={signInUrl}>sign in</Link>.
|
||||
</span>
|
||||
) : (
|
||||
"sign in."
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Reset your password</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (password !== confirmPassword) {
|
||||
setError(new Error("Passwords do not match"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
setError(new Error("No password reset token provided"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const { data, error } = await auth.authClient.resetPassword({
|
||||
newPassword: password,
|
||||
token,
|
||||
});
|
||||
setStatus(data?.status ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
disabled={loading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="confirm-password">Confirm password</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
disabled={loading}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
packages/jazz-cloud-ui/src/components/settings-form.tsx
Normal file
65
packages/jazz-cloud-ui/src/components/settings-form.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { SSOProviderType } from "jazz-react-auth-betterauth";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert.js";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../components/ui/card.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { DeleteAccountButton } from "./ui/delete-account-button.js";
|
||||
import { ProvidersCard } from "./ui/providers-card.js";
|
||||
|
||||
type SettingsFormProps = {
|
||||
providers?: SSOProviderType[];
|
||||
deleteAccountRedirectUrl?: string;
|
||||
};
|
||||
|
||||
export function SettingsForm({
|
||||
className,
|
||||
providers,
|
||||
deleteAccountRedirectUrl,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & SettingsFormProps) {
|
||||
const setLoading = useState(false)[1];
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-3">
|
||||
<ProvidersCard
|
||||
providers={providers}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
<DeleteAccountButton
|
||||
redirectUrl={deleteAccountRedirectUrl}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
packages/jazz-cloud-ui/src/components/ui/alert.tsx
Normal file
65
packages/jazz-cloud-ui/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
49
packages/jazz-cloud-ui/src/components/ui/avatar.tsx
Normal file
49
packages/jazz-cloud-ui/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
58
packages/jazz-cloud-ui/src/components/ui/button.tsx
Normal file
58
packages/jazz-cloud-ui/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
91
packages/jazz-cloud-ui/src/components/ui/card.tsx
Normal file
91
packages/jazz-cloud-ui/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
31
packages/jazz-cloud-ui/src/components/ui/checkbox.tsx
Normal file
31
packages/jazz-cloud-ui/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,35 +1,46 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
import { Button } from "../../components/ui/button.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children?: React.ReactNode;
|
||||
src?: InstanceType<typeof Image>["src"];
|
||||
alt?: InstanceType<typeof Image>["alt"];
|
||||
callbackURL: string;
|
||||
redirectUrl?: string;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
}
|
||||
|
||||
export const DeleteAccountButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ callbackURL, setLoading, setError }) => {
|
||||
const router = useRouter();
|
||||
({ redirectUrl, setLoading, setError }) => {
|
||||
const { logOut } = useAccount();
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const signOut = useCallback(() => {
|
||||
auth.authClient
|
||||
.signOut()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
logOut();
|
||||
if (redirectUrl) router.push(redirectUrl);
|
||||
});
|
||||
}, [logOut, router, auth.authClient]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="relative"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await auth.authClient.deleteUser(
|
||||
{
|
||||
callbackURL: callbackURL,
|
||||
callbackURL: undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.replace(callbackURL);
|
||||
signOut();
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -54,3 +65,4 @@ export const DeleteAccountButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
DeleteAccountButton.displayName = "DeleteAccountButton";
|
||||
200
packages/jazz-cloud-ui/src/components/ui/dropdown-menu.tsx
Normal file
200
packages/jazz-cloud-ui/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
76
packages/jazz-cloud-ui/src/components/ui/input-otp.tsx
Normal file
76
packages/jazz-cloud-ui/src/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
20
packages/jazz-cloud-ui/src/components/ui/input.tsx
Normal file
20
packages/jazz-cloud-ui/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
23
packages/jazz-cloud-ui/src/components/ui/label.tsx
Normal file
23
packages/jazz-cloud-ui/src/components/ui/label.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
40
packages/jazz-cloud-ui/src/components/ui/logout-button.tsx
Normal file
40
packages/jazz-cloud-ui/src/components/ui/logout-button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
import { Button } from "../../components/ui/button.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export const LogoutButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ redirectUrl }) => {
|
||||
const { logOut } = useAccount();
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const signOut = useCallback(() => {
|
||||
auth.authClient
|
||||
.signOut()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
logOut();
|
||||
if (redirectUrl) router.push(redirectUrl);
|
||||
});
|
||||
}, [logOut, router, auth.authClient]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={signOut}
|
||||
>
|
||||
<LogOut />
|
||||
Sign out
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
LogoutButton.displayName = "LogoutButton";
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FullAuthClient, useAuth } from "jazz-react-auth-betterauth";
|
||||
import { Link } from "lucide-react";
|
||||
import { forwardRef } from "react";
|
||||
import { Button } from "../../components/ui/button.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
operation: "sign-up" | "sign-in";
|
||||
email: string;
|
||||
callbackURL?: string;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
}
|
||||
|
||||
export const MagicLinkButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ operation, email, callbackURL, setLoading, setError }) => {
|
||||
const auth = useAuth();
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).signIn.magicLink({
|
||||
email: email,
|
||||
callbackURL: callbackURL,
|
||||
});
|
||||
if (error) {
|
||||
setError({
|
||||
...error,
|
||||
name: error.message ?? error.statusText,
|
||||
message: error.message ?? error.statusText,
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<Link />
|
||||
{operation === "sign-in"
|
||||
? "Sign in with magic link"
|
||||
: "Sign up with magic link"}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
MagicLinkButton.displayName = "MagicLinkButton";
|
||||
65
packages/jazz-cloud-ui/src/components/ui/providers-card.tsx
Normal file
65
packages/jazz-cloud-ui/src/components/ui/providers-card.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
AccountsType,
|
||||
SSOProviderType,
|
||||
useAuth,
|
||||
} from "jazz-react-auth-betterauth";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./card.js";
|
||||
import { SSOButton } from "./sso-button.js";
|
||||
|
||||
type ProvidersCardProps = {
|
||||
providers?: SSOProviderType[];
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
};
|
||||
|
||||
export const ProvidersCard = ({
|
||||
providers,
|
||||
setLoading,
|
||||
setError,
|
||||
}: ProvidersCardProps) => {
|
||||
const auth = useAuth();
|
||||
const [accounts, setAccounts] = useState<AccountsType | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
return auth.authClient.useSession.subscribe(() => {
|
||||
auth.authClient.listAccounts().then((x) => setAccounts(x));
|
||||
});
|
||||
}, [auth.authClient]);
|
||||
|
||||
const linkedProviders =
|
||||
providers?.filter((x) => accounts?.data?.some((y) => y.provider === x)) ??
|
||||
[];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Single Sign-On Providers</CardTitle>
|
||||
<CardDescription>
|
||||
Connect your account to an external authentication provider.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!providers?.length && "No SSO providers configured."}
|
||||
{providers?.map((x) => {
|
||||
return (
|
||||
<SSOButton
|
||||
key={useId()}
|
||||
operation={linkedProviders.includes(x) ? "unlink" : "link"}
|
||||
provider={x}
|
||||
setLoading={setLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
ProvidersCard.displayName = "ProvidersCard";
|
||||
71
packages/jazz-cloud-ui/src/components/ui/send-otp-button.tsx
Normal file
71
packages/jazz-cloud-ui/src/components/ui/send-otp-button.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { FullAuthClient, useAuth } from "jazz-react-auth-betterauth";
|
||||
import { RectangleEllipsis } from "lucide-react";
|
||||
import { forwardRef } from "react";
|
||||
import { Button } from "../../components/ui/button.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
operation: "sign-up" | "sign-in" | "verify" | "reset";
|
||||
email: string;
|
||||
setOtpStatus: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
}
|
||||
|
||||
export const SendOtpButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ operation, email, setOtpStatus, setLoading, setError }) => {
|
||||
const auth = useAuth();
|
||||
const otpType =
|
||||
(() => {
|
||||
if (operation === "sign-up" || operation === "sign-in") {
|
||||
return "sign-in";
|
||||
} else if (operation === "verify") {
|
||||
return "email-verification";
|
||||
} else if (operation === "reset") {
|
||||
return "forget-password";
|
||||
}
|
||||
})() ?? "sign-in";
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { data, error } = await (
|
||||
auth.authClient as FullAuthClient
|
||||
).emailOtp.sendVerificationOtp({
|
||||
email: email,
|
||||
type: otpType,
|
||||
});
|
||||
setOtpStatus(data?.success ?? false);
|
||||
const errorMessage = error?.message ?? error?.statusText;
|
||||
setError(
|
||||
error
|
||||
? {
|
||||
...error,
|
||||
name: error.statusText,
|
||||
message:
|
||||
errorMessage && errorMessage.length > 0
|
||||
? errorMessage
|
||||
: "An error occurred",
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
<RectangleEllipsis />
|
||||
{(() => {
|
||||
if (operation === "sign-in") return "Sign in with one-time password";
|
||||
if (operation === "sign-up") return "Sign up with one-time password";
|
||||
if (operation === "verify")
|
||||
return "Verify account using one-time code";
|
||||
if (operation === "reset")
|
||||
return "Reset password using one-time code";
|
||||
})()}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
SendOtpButton.displayName = "SendOtpButton";
|
||||
@@ -1,54 +1,49 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { SSOProviderType, useAuth } from "jazz-react-auth-betterauth";
|
||||
import { socialProviderNames } from "jazz-react-auth-betterauth";
|
||||
import { forwardRef } from "react";
|
||||
import { Button } from "../../components/ui/button.js";
|
||||
import { ssoIcons } from "../../lib/sso.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children?: React.ReactNode;
|
||||
src?: InstanceType<typeof Image>["src"];
|
||||
alt?: InstanceType<typeof Image>["alt"];
|
||||
provider: SSOProviderType;
|
||||
link?: boolean;
|
||||
operation: "sign-in" | "sign-up" | "link" | "unlink";
|
||||
accountId?: string;
|
||||
callbackURL?: string;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setError: React.Dispatch<React.SetStateAction<Error | undefined>>;
|
||||
}
|
||||
|
||||
export const SSOButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
provider,
|
||||
link = false,
|
||||
callbackURL,
|
||||
setLoading,
|
||||
setError,
|
||||
...buttonProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
({ provider, operation, accountId, callbackURL, setLoading, setError }) => {
|
||||
const auth = useAuth();
|
||||
const providerName = socialProviderNames[provider];
|
||||
const providerIcon = ssoIcons[provider];
|
||||
return (
|
||||
<Button
|
||||
src={`/social/${provider}.svg`}
|
||||
alt={`${providerName} logo`}
|
||||
imageClassName="absolute left-3 dark:invert"
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const { error } = await (async () => {
|
||||
if (link) {
|
||||
if (operation === "link") {
|
||||
return await auth.authClient.linkSocial({
|
||||
provider: provider,
|
||||
});
|
||||
} else {
|
||||
} else if (operation === "sign-in" || operation === "sign-up") {
|
||||
return await auth.authClient.signIn.social({
|
||||
provider: provider,
|
||||
callbackURL: callbackURL,
|
||||
});
|
||||
} else {
|
||||
return await auth.authClient.unlinkAccount({
|
||||
providerId: provider,
|
||||
accountId: accountId,
|
||||
});
|
||||
}
|
||||
})();
|
||||
if (error) {
|
||||
@@ -60,14 +55,16 @@ export const SSOButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
}
|
||||
setLoading(false);
|
||||
}}
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
>
|
||||
{link
|
||||
? `Link ${providerName} account`
|
||||
: `Continue with ${providerName}`}
|
||||
{children}
|
||||
{providerIcon}
|
||||
{(() => {
|
||||
if (operation === "sign-in") return `Login with ${providerName}`;
|
||||
if (operation === "sign-up") return `Register with ${providerName}`;
|
||||
if (operation === "link") return "Link";
|
||||
if (operation === "unlink") return "Unlink";
|
||||
})()}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
);
|
||||
SSOButton.displayName = "SSOButton";
|
||||
109
packages/jazz-cloud-ui/src/components/ui/user-button.tsx
Normal file
109
packages/jazz-cloud-ui/src/components/ui/user-button.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useAccount } from "jazz-react";
|
||||
import { useAuth } from "jazz-react-auth-betterauth";
|
||||
import { ChevronsUpDown, LogOut, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
import { Avatar, AvatarFallback } from "./avatar.js";
|
||||
import { Button } from "./button.js";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./dropdown-menu.js";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
logoutRedirectUrl?: string;
|
||||
settingsUrl?: string;
|
||||
}
|
||||
|
||||
export const UserButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ logoutRedirectUrl, settingsUrl }) => {
|
||||
const { logOut } = useAccount();
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const signOut = useCallback(() => {
|
||||
auth.authClient
|
||||
.signOut()
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
logOut();
|
||||
if (logoutRedirectUrl) router.push(logoutRedirectUrl);
|
||||
});
|
||||
}, [logOut, router, auth.authClient]);
|
||||
const avatarFallback = auth.account?.name
|
||||
.split(" ")
|
||||
.map((x) => x.charAt(0).toUpperCase())
|
||||
.join("");
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{auth.account?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{auth.account?.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
// side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{auth.account?.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{auth.account?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
{settingsUrl && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<Link href={settingsUrl}>
|
||||
<DropdownMenuItem>
|
||||
<Settings />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={signOut}>
|
||||
<LogOut />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
UserButton.displayName = "UserButton";
|
||||
20
packages/jazz-cloud-ui/src/index.ts
Normal file
20
packages/jazz-cloud-ui/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// UI
|
||||
export * from "./components/ui/alert.js";
|
||||
export * from "./components/ui/button.js";
|
||||
export * from "./components/ui/card.js";
|
||||
export * from "./components/ui/checkbox.js";
|
||||
export * from "./components/ui/delete-account-button.js";
|
||||
export * from "./components/ui/input.js";
|
||||
export * from "./components/ui/input-otp.js";
|
||||
export * from "./components/ui/label.js";
|
||||
export * from "./components/ui/logout-button.js";
|
||||
export * from "./components/ui/magic-link-button.js";
|
||||
export * from "./components/ui/providers-card.js";
|
||||
export * from "./components/ui/send-otp-button.js";
|
||||
export * from "./components/ui/sso-button.js";
|
||||
export * from "./components/ui/user-button.js";
|
||||
|
||||
// Forms
|
||||
export * from "./components/login-form.js";
|
||||
export * from "./components/reset-password-form.js";
|
||||
export * from "./components/settings-form.js";
|
||||
40
packages/jazz-cloud-ui/src/lib/sso.tsx
Normal file
40
packages/jazz-cloud-ui/src/lib/sso.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
SiApple,
|
||||
SiDiscord,
|
||||
SiDropbox,
|
||||
SiFacebook,
|
||||
SiGithub,
|
||||
SiGitlab,
|
||||
SiGoogle,
|
||||
SiKick,
|
||||
SiReddit,
|
||||
SiRoblox,
|
||||
SiSpotify,
|
||||
SiTiktok,
|
||||
SiTwitch,
|
||||
SiVk,
|
||||
SiX,
|
||||
SiZoom,
|
||||
} from "@icons-pack/react-simple-icons";
|
||||
import { SSOProviderType } from "jazz-react-auth-betterauth";
|
||||
|
||||
export const ssoIcons: Record<SSOProviderType, React.ReactNode> = {
|
||||
github: <SiGithub />,
|
||||
google: <SiGoogle />,
|
||||
apple: <SiApple />,
|
||||
discord: <SiDiscord />,
|
||||
facebook: <SiFacebook />,
|
||||
microsoft: undefined,
|
||||
twitter: <SiX />,
|
||||
dropbox: <SiDropbox />,
|
||||
linkedin: undefined,
|
||||
gitlab: <SiGitlab />,
|
||||
kick: <SiKick />,
|
||||
tiktok: <SiTiktok />,
|
||||
twitch: <SiTwitch />,
|
||||
vk: <SiVk />,
|
||||
zoom: <SiZoom />,
|
||||
roblox: <SiRoblox />,
|
||||
reddit: <SiReddit />,
|
||||
spotify: <SiSpotify />,
|
||||
};
|
||||
6
packages/jazz-cloud-ui/src/lib/utils.ts
Normal file
6
packages/jazz-cloud-ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
17
packages/jazz-cloud-ui/tsconfig.json
Normal file
17
packages/jazz-cloud-ui/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"incremental": false,
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13429
pnpm-lock.yaml
generated
13429
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user