@liedsonc/core-auth-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +802 -759
- package/auth-ui/bin/init.js +126 -0
- package/auth-ui/components/auth-card.tsx +49 -49
- package/auth-ui/components/auth-form.tsx +53 -53
- package/auth-ui/components/error-message.tsx +24 -24
- package/auth-ui/components/form-field.tsx +39 -39
- package/auth-ui/components/loading-spinner.tsx +19 -19
- package/auth-ui/components/oauth-buttons.tsx +49 -49
- package/auth-ui/components/password-input.tsx +93 -93
- package/auth-ui/components/success-message.tsx +24 -24
- package/auth-ui/package.json +61 -55
- package/auth-ui/pages/forgot-password/index.tsx +83 -83
- package/auth-ui/pages/login/index.tsx +119 -119
- package/auth-ui/pages/register/index.tsx +149 -149
- package/auth-ui/pages/reset-password/index.tsx +133 -133
- package/auth-ui/pages/verify-email/index.tsx +143 -143
- package/auth-ui/templates/app/(auth)/forgot-password/page.tsx +5 -0
- package/auth-ui/templates/app/(auth)/layout.tsx +12 -0
- package/auth-ui/templates/app/(auth)/login/page.tsx +5 -0
- package/auth-ui/templates/app/(auth)/register/page.tsx +5 -0
- package/auth-ui/templates/app/(auth)/reset-password/page.tsx +5 -0
- package/auth-ui/templates/app/(auth)/verify-email/page.tsx +5 -0
- package/auth-ui/templates/app/api/auth/forgot-password/route.ts +16 -0
- package/auth-ui/templates/app/api/auth/login/route.ts +16 -0
- package/auth-ui/templates/app/api/auth/logout/route.ts +8 -0
- package/auth-ui/templates/app/api/auth/register/route.ts +16 -0
- package/auth-ui/templates/app/api/auth/reset-password/route.ts +16 -0
- package/auth-ui/templates/app/api/auth/session/route.ts +8 -0
- package/auth-ui/templates/app/api/auth/verify-email/route.ts +16 -0
- package/auth-ui/templates/env.example +25 -0
- package/auth-ui/templates/lib/auth-client.ts +20 -0
- package/auth-ui/templates/lib/auth-config.ts +43 -0
- package/auth-ui/tsconfig.json +4 -15
- package/package.json +17 -6
- package/tailwind.config.ts +41 -41
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const PKG_NAME = "@liedsonc/core-auth-kit";
|
|
8
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
9
|
+
const TEMPLATES_DIR = path.join(PKG_ROOT, "templates");
|
|
10
|
+
|
|
11
|
+
const TEMPLATE_MAP = [
|
|
12
|
+
{ template: "app/(auth)/layout.tsx", target: "app/(auth)/layout.tsx" },
|
|
13
|
+
{ template: "app/(auth)/login/page.tsx", target: "app/(auth)/login/page.tsx" },
|
|
14
|
+
{ template: "app/(auth)/register/page.tsx", target: "app/(auth)/register/page.tsx" },
|
|
15
|
+
{ template: "app/(auth)/forgot-password/page.tsx", target: "app/(auth)/forgot-password/page.tsx" },
|
|
16
|
+
{ template: "app/(auth)/reset-password/page.tsx", target: "app/(auth)/reset-password/page.tsx" },
|
|
17
|
+
{ template: "app/(auth)/verify-email/page.tsx", target: "app/(auth)/verify-email/page.tsx" },
|
|
18
|
+
{ template: "lib/auth-config.ts", target: "lib/auth-config.ts" },
|
|
19
|
+
{ template: "lib/auth-client.ts", target: "lib/auth-client.ts" },
|
|
20
|
+
{ template: "env.example", target: ".env.example" },
|
|
21
|
+
{ template: "app/api/auth/login/route.ts", target: "app/api/auth/login/route.ts" },
|
|
22
|
+
{ template: "app/api/auth/register/route.ts", target: "app/api/auth/register/route.ts" },
|
|
23
|
+
{ template: "app/api/auth/logout/route.ts", target: "app/api/auth/logout/route.ts" },
|
|
24
|
+
{ template: "app/api/auth/forgot-password/route.ts", target: "app/api/auth/forgot-password/route.ts" },
|
|
25
|
+
{ template: "app/api/auth/reset-password/route.ts", target: "app/api/auth/reset-password/route.ts" },
|
|
26
|
+
{ template: "app/api/auth/verify-email/route.ts", target: "app/api/auth/verify-email/route.ts" },
|
|
27
|
+
{ template: "app/api/auth/session/route.ts", target: "app/api/auth/session/route.ts" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function findProjectRoot(cwd) {
|
|
31
|
+
let dir = path.resolve(cwd);
|
|
32
|
+
for (let i = 0; i < 20; i++) {
|
|
33
|
+
const pkg = path.join(dir, "package.json");
|
|
34
|
+
const nextConfig =
|
|
35
|
+
fs.existsSync(path.join(dir, "next.config.js")) ||
|
|
36
|
+
fs.existsSync(path.join(dir, "next.config.mjs")) ||
|
|
37
|
+
fs.existsSync(path.join(dir, "next.config.ts"));
|
|
38
|
+
if (fs.existsSync(pkg) && nextConfig) return dir;
|
|
39
|
+
const parent = path.dirname(dir);
|
|
40
|
+
if (parent === dir) break;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectAppDir(root) {
|
|
47
|
+
if (fs.existsSync(path.join(root, "src", "app"))) return "src/app";
|
|
48
|
+
return "app";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectLibDir(root) {
|
|
52
|
+
if (fs.existsSync(path.join(root, "src", "lib"))) return "src/lib";
|
|
53
|
+
if (fs.existsSync(path.join(root, "src", "app"))) return "src/lib";
|
|
54
|
+
return "lib";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPackageInstalled(root) {
|
|
58
|
+
const pkgDir = path.join(root, "node_modules", PKG_NAME);
|
|
59
|
+
return fs.existsSync(path.join(pkgDir, "package.json"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function installPackage(root) {
|
|
63
|
+
console.log("Installing %s...", PKG_NAME);
|
|
64
|
+
execSync("npm install " + PKG_NAME, { cwd: root, stdio: "inherit" });
|
|
65
|
+
console.log("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function run() {
|
|
69
|
+
const force = process.argv.includes("--force");
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
const root = findProjectRoot(cwd);
|
|
72
|
+
|
|
73
|
+
if (!root) {
|
|
74
|
+
console.error(
|
|
75
|
+
"Could not find a Next.js project (package.json + next.config). Run this from your app root."
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isPackageInstalled(root)) {
|
|
81
|
+
installPackage(root);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const appDir = detectAppDir(root);
|
|
85
|
+
const libDir = detectLibDir(root);
|
|
86
|
+
|
|
87
|
+
let created = 0;
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
|
|
90
|
+
for (const { template, target } of TEMPLATE_MAP) {
|
|
91
|
+
const targetPath = target.startsWith("app/")
|
|
92
|
+
? path.join(root, appDir, target.slice("app/".length))
|
|
93
|
+
: target.startsWith("lib/")
|
|
94
|
+
? path.join(root, libDir, target.slice("lib/".length))
|
|
95
|
+
: target === ".env.example"
|
|
96
|
+
? path.join(root, ".env.example")
|
|
97
|
+
: path.join(root, target);
|
|
98
|
+
|
|
99
|
+
const templatePath = path.join(TEMPLATES_DIR, template);
|
|
100
|
+
if (!fs.existsSync(templatePath)) {
|
|
101
|
+
console.warn("Template not found:", template);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
106
|
+
console.log("Skip (exists):", path.relative(root, targetPath));
|
|
107
|
+
skipped++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const content = fs.readFileSync(templatePath, "utf8");
|
|
112
|
+
const dir = path.dirname(targetPath);
|
|
113
|
+
if (!fs.existsSync(dir)) {
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
117
|
+
console.log("Created:", path.relative(root, targetPath));
|
|
118
|
+
created++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log("Done. Created %d file(s), skipped %d (use --force to overwrite).", created, skipped);
|
|
123
|
+
console.log("Next: implement lib/auth-client.ts (or src/lib/auth-client.ts) with your backend.");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
run();
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardFooter,
|
|
8
|
-
CardHeader,
|
|
9
|
-
CardTitle,
|
|
10
|
-
} from "./ui/card";
|
|
11
|
-
import { useAuthUIConfig } from "../context";
|
|
12
|
-
import { cn } from "../utils";
|
|
13
|
-
import "../styles/index.css";
|
|
14
|
-
|
|
15
|
-
export interface AuthCardProps {
|
|
16
|
-
title: string;
|
|
17
|
-
subtitle?: string;
|
|
18
|
-
children: React.ReactNode;
|
|
19
|
-
footer?: React.ReactNode;
|
|
20
|
-
className?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function AuthCard({
|
|
24
|
-
title,
|
|
25
|
-
subtitle,
|
|
26
|
-
children,
|
|
27
|
-
footer,
|
|
28
|
-
className,
|
|
29
|
-
}: AuthCardProps) {
|
|
30
|
-
const config = useAuthUIConfig();
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<Card className={cn("auth-ui-card", className)}>
|
|
34
|
-
<CardHeader className="space-y-1">
|
|
35
|
-
{config.logo && (
|
|
36
|
-
<div className="flex justify-center mb-2" aria-hidden="true">
|
|
37
|
-
{config.logo}
|
|
38
|
-
</div>
|
|
39
|
-
)}
|
|
40
|
-
<CardTitle className="text-center">{title}</CardTitle>
|
|
41
|
-
{subtitle && (
|
|
42
|
-
<CardDescription className="text-center">{subtitle}</CardDescription>
|
|
43
|
-
)}
|
|
44
|
-
</CardHeader>
|
|
45
|
-
<CardContent>{children}</CardContent>
|
|
46
|
-
{footer && <CardFooter className="flex flex-col gap-2">{footer}</CardFooter>}
|
|
47
|
-
</Card>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardFooter,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "./ui/card";
|
|
11
|
+
import { useAuthUIConfig } from "../context";
|
|
12
|
+
import { cn } from "../utils";
|
|
13
|
+
import "../styles/index.css";
|
|
14
|
+
|
|
15
|
+
export interface AuthCardProps {
|
|
16
|
+
title: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
footer?: React.ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AuthCard({
|
|
24
|
+
title,
|
|
25
|
+
subtitle,
|
|
26
|
+
children,
|
|
27
|
+
footer,
|
|
28
|
+
className,
|
|
29
|
+
}: AuthCardProps) {
|
|
30
|
+
const config = useAuthUIConfig();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Card className={cn("auth-ui-card", className)}>
|
|
34
|
+
<CardHeader className="space-y-1">
|
|
35
|
+
{config.logo && (
|
|
36
|
+
<div className="flex justify-center mb-2" aria-hidden="true">
|
|
37
|
+
{config.logo}
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
<CardTitle className="text-center">{title}</CardTitle>
|
|
41
|
+
{subtitle && (
|
|
42
|
+
<CardDescription className="text-center">{subtitle}</CardDescription>
|
|
43
|
+
)}
|
|
44
|
+
</CardHeader>
|
|
45
|
+
<CardContent>{children}</CardContent>
|
|
46
|
+
{footer && <CardFooter className="flex flex-col gap-2">{footer}</CardFooter>}
|
|
47
|
+
</Card>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { cn } from "../utils";
|
|
5
|
-
|
|
6
|
-
export interface AuthFormProps
|
|
7
|
-
extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
8
|
-
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
9
|
-
loading?: boolean;
|
|
10
|
-
error?: string;
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
className?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function AuthForm({
|
|
16
|
-
onSubmit,
|
|
17
|
-
loading = false,
|
|
18
|
-
error,
|
|
19
|
-
children,
|
|
20
|
-
className,
|
|
21
|
-
...props
|
|
22
|
-
}: AuthFormProps) {
|
|
23
|
-
const [submitting, setSubmitting] = React.useState(false);
|
|
24
|
-
const isDisabled = loading || submitting;
|
|
25
|
-
|
|
26
|
-
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
27
|
-
e.preventDefault();
|
|
28
|
-
if (isDisabled) return;
|
|
29
|
-
setSubmitting(true);
|
|
30
|
-
try {
|
|
31
|
-
await onSubmit(e);
|
|
32
|
-
} finally {
|
|
33
|
-
setSubmitting(false);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<form
|
|
39
|
-
onSubmit={handleSubmit}
|
|
40
|
-
noValidate
|
|
41
|
-
className={cn("space-y-4", className)}
|
|
42
|
-
aria-busy={isDisabled}
|
|
43
|
-
{...props}
|
|
44
|
-
>
|
|
45
|
-
{error && (
|
|
46
|
-
<p role="alert" className="text-sm text-destructive">
|
|
47
|
-
{error}
|
|
48
|
-
</p>
|
|
49
|
-
)}
|
|
50
|
-
{children}
|
|
51
|
-
</form>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface AuthFormProps
|
|
7
|
+
extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
8
|
+
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AuthForm({
|
|
16
|
+
onSubmit,
|
|
17
|
+
loading = false,
|
|
18
|
+
error,
|
|
19
|
+
children,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: AuthFormProps) {
|
|
23
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
24
|
+
const isDisabled = loading || submitting;
|
|
25
|
+
|
|
26
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
if (isDisabled) return;
|
|
29
|
+
setSubmitting(true);
|
|
30
|
+
try {
|
|
31
|
+
await onSubmit(e);
|
|
32
|
+
} finally {
|
|
33
|
+
setSubmitting(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<form
|
|
39
|
+
onSubmit={handleSubmit}
|
|
40
|
+
noValidate
|
|
41
|
+
className={cn("space-y-4", className)}
|
|
42
|
+
aria-busy={isDisabled}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{error && (
|
|
46
|
+
<p role="alert" className="text-sm text-destructive">
|
|
47
|
+
{error}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
{children}
|
|
51
|
+
</form>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../utils";
|
|
4
|
-
|
|
5
|
-
export function ErrorMessage({
|
|
6
|
-
children,
|
|
7
|
-
className,
|
|
8
|
-
id,
|
|
9
|
-
}: {
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
className?: string;
|
|
12
|
-
id?: string;
|
|
13
|
-
}) {
|
|
14
|
-
if (!children) return null;
|
|
15
|
-
return (
|
|
16
|
-
<p
|
|
17
|
-
id={id}
|
|
18
|
-
role="alert"
|
|
19
|
-
className={cn("text-sm text-destructive", className)}
|
|
20
|
-
>
|
|
21
|
-
{children}
|
|
22
|
-
</p>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
export function ErrorMessage({
|
|
6
|
+
children,
|
|
7
|
+
className,
|
|
8
|
+
id,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
}) {
|
|
14
|
+
if (!children) return null;
|
|
15
|
+
return (
|
|
16
|
+
<p
|
|
17
|
+
id={id}
|
|
18
|
+
role="alert"
|
|
19
|
+
className={cn("text-sm text-destructive", className)}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</p>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Label } from "./ui/label";
|
|
4
|
-
import { cn } from "../utils";
|
|
5
|
-
|
|
6
|
-
export function FormField({
|
|
7
|
-
label,
|
|
8
|
-
htmlFor,
|
|
9
|
-
error,
|
|
10
|
-
children,
|
|
11
|
-
required,
|
|
12
|
-
className,
|
|
13
|
-
}: {
|
|
14
|
-
label: string;
|
|
15
|
-
htmlFor: string;
|
|
16
|
-
error?: string;
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
required?: boolean;
|
|
19
|
-
className?: string;
|
|
20
|
-
}) {
|
|
21
|
-
return (
|
|
22
|
-
<div className={cn("space-y-2", className)}>
|
|
23
|
-
<Label htmlFor={htmlFor}>
|
|
24
|
-
{label}
|
|
25
|
-
{required && (
|
|
26
|
-
<span className="text-destructive ml-0.5" aria-hidden="true">
|
|
27
|
-
*
|
|
28
|
-
</span>
|
|
29
|
-
)}
|
|
30
|
-
</Label>
|
|
31
|
-
{children}
|
|
32
|
-
{error && (
|
|
33
|
-
<p id={`${htmlFor}-error`} role="alert" className="text-sm text-destructive">
|
|
34
|
-
{error}
|
|
35
|
-
</p>
|
|
36
|
-
)}
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Label } from "./ui/label";
|
|
4
|
+
import { cn } from "../utils";
|
|
5
|
+
|
|
6
|
+
export function FormField({
|
|
7
|
+
label,
|
|
8
|
+
htmlFor,
|
|
9
|
+
error,
|
|
10
|
+
children,
|
|
11
|
+
required,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
label: string;
|
|
15
|
+
htmlFor: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={cn("space-y-2", className)}>
|
|
23
|
+
<Label htmlFor={htmlFor}>
|
|
24
|
+
{label}
|
|
25
|
+
{required && (
|
|
26
|
+
<span className="text-destructive ml-0.5" aria-hidden="true">
|
|
27
|
+
*
|
|
28
|
+
</span>
|
|
29
|
+
)}
|
|
30
|
+
</Label>
|
|
31
|
+
{children}
|
|
32
|
+
{error && (
|
|
33
|
+
<p id={`${htmlFor}-error`} role="alert" className="text-sm text-destructive">
|
|
34
|
+
{error}
|
|
35
|
+
</p>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { cn } from "../utils";
|
|
4
|
-
|
|
5
|
-
export function LoadingSpinner({
|
|
6
|
-
className,
|
|
7
|
-
"aria-label": ariaLabel = "Loading",
|
|
8
|
-
}: {
|
|
9
|
-
className?: string;
|
|
10
|
-
"aria-label"?: string;
|
|
11
|
-
}) {
|
|
12
|
-
return (
|
|
13
|
-
<span
|
|
14
|
-
role="status"
|
|
15
|
-
aria-label={ariaLabel}
|
|
16
|
-
className={cn("inline-block size-5 animate-spin rounded-full border-2 border-current border-t-transparent", className)}
|
|
17
|
-
/>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
export function LoadingSpinner({
|
|
6
|
+
className,
|
|
7
|
+
"aria-label": ariaLabel = "Loading",
|
|
8
|
+
}: {
|
|
9
|
+
className?: string;
|
|
10
|
+
"aria-label"?: string;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
role="status"
|
|
15
|
+
aria-label={ariaLabel}
|
|
16
|
+
className={cn("inline-block size-5 animate-spin rounded-full border-2 border-current border-t-transparent", className)}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Button } from "./ui/button";
|
|
4
|
-
import { useOAuth } from "../hooks/use-oauth";
|
|
5
|
-
import { cn } from "../utils";
|
|
6
|
-
import type { OAuthProvider } from "../types";
|
|
7
|
-
|
|
8
|
-
const providerLabels: Record<OAuthProvider, string> = {
|
|
9
|
-
google: "Continue with Google",
|
|
10
|
-
apple: "Continue with Apple",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function OAuthButtons({
|
|
14
|
-
providers,
|
|
15
|
-
loadingProvider,
|
|
16
|
-
onSignIn,
|
|
17
|
-
className,
|
|
18
|
-
}: {
|
|
19
|
-
providers: { provider: OAuthProvider; enabled: boolean }[];
|
|
20
|
-
loadingProvider: OAuthProvider | null;
|
|
21
|
-
onSignIn: (provider: OAuthProvider) => void | Promise<void>;
|
|
22
|
-
className?: string;
|
|
23
|
-
}) {
|
|
24
|
-
const enabled = providers.filter((p) => p.enabled);
|
|
25
|
-
|
|
26
|
-
if (enabled.length === 0) return null;
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div className={cn("space-y-2", className)}>
|
|
30
|
-
{enabled.map(({ provider }) => (
|
|
31
|
-
<Button
|
|
32
|
-
key={provider}
|
|
33
|
-
type="button"
|
|
34
|
-
variant="outline"
|
|
35
|
-
className="w-full"
|
|
36
|
-
disabled={!!loadingProvider}
|
|
37
|
-
onClick={() => onSignIn(provider)}
|
|
38
|
-
aria-busy={loadingProvider === provider}
|
|
39
|
-
>
|
|
40
|
-
{loadingProvider === provider ? (
|
|
41
|
-
<span className="inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
42
|
-
) : (
|
|
43
|
-
providerLabels[provider]
|
|
44
|
-
)}
|
|
45
|
-
</Button>
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "./ui/button";
|
|
4
|
+
import { useOAuth } from "../hooks/use-oauth";
|
|
5
|
+
import { cn } from "../utils";
|
|
6
|
+
import type { OAuthProvider } from "../types";
|
|
7
|
+
|
|
8
|
+
const providerLabels: Record<OAuthProvider, string> = {
|
|
9
|
+
google: "Continue with Google",
|
|
10
|
+
apple: "Continue with Apple",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function OAuthButtons({
|
|
14
|
+
providers,
|
|
15
|
+
loadingProvider,
|
|
16
|
+
onSignIn,
|
|
17
|
+
className,
|
|
18
|
+
}: {
|
|
19
|
+
providers: { provider: OAuthProvider; enabled: boolean }[];
|
|
20
|
+
loadingProvider: OAuthProvider | null;
|
|
21
|
+
onSignIn: (provider: OAuthProvider) => void | Promise<void>;
|
|
22
|
+
className?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const enabled = providers.filter((p) => p.enabled);
|
|
25
|
+
|
|
26
|
+
if (enabled.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={cn("space-y-2", className)}>
|
|
30
|
+
{enabled.map(({ provider }) => (
|
|
31
|
+
<Button
|
|
32
|
+
key={provider}
|
|
33
|
+
type="button"
|
|
34
|
+
variant="outline"
|
|
35
|
+
className="w-full"
|
|
36
|
+
disabled={!!loadingProvider}
|
|
37
|
+
onClick={() => onSignIn(provider)}
|
|
38
|
+
aria-busy={loadingProvider === provider}
|
|
39
|
+
>
|
|
40
|
+
{loadingProvider === provider ? (
|
|
41
|
+
<span className="inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
42
|
+
) : (
|
|
43
|
+
providerLabels[provider]
|
|
44
|
+
)}
|
|
45
|
+
</Button>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|