create-nexu 1.4.2 → 1.4.4
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/dist/index.js +31 -1
- package/package.json +1 -1
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var TEMPLATE_DIRS = {
|
|
|
38
38
|
changeset: ".changeset"
|
|
39
39
|
};
|
|
40
40
|
var SHARED_PACKAGES = [
|
|
41
|
+
"auth",
|
|
41
42
|
"cache",
|
|
42
43
|
"config",
|
|
43
44
|
"constants",
|
|
@@ -147,6 +148,22 @@ function getRunCommand(pm) {
|
|
|
147
148
|
return "npm run";
|
|
148
149
|
}
|
|
149
150
|
}
|
|
151
|
+
function getPackageManagerVersion(pm) {
|
|
152
|
+
try {
|
|
153
|
+
const version = execSync(`${pm} --version`, {
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
156
|
+
}).trim();
|
|
157
|
+
return version;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function getPackageManagerField(pm) {
|
|
163
|
+
const version = getPackageManagerVersion(pm);
|
|
164
|
+
if (!version) return null;
|
|
165
|
+
return `${pm}@${version}`;
|
|
166
|
+
}
|
|
150
167
|
function getInstallCommand(pm) {
|
|
151
168
|
switch (pm) {
|
|
152
169
|
case "pnpm":
|
|
@@ -427,6 +444,10 @@ async function init(projectName, options) {
|
|
|
427
444
|
fs3.copySync(templateDir, projectDir);
|
|
428
445
|
const dotfilesToRename = [
|
|
429
446
|
{ src: path3.join(projectDir, "gitignore"), dest: path3.join(projectDir, ".gitignore") },
|
|
447
|
+
{
|
|
448
|
+
src: path3.join(projectDir, "lintstagedrc.cjs"),
|
|
449
|
+
dest: path3.join(projectDir, ".lintstagedrc.cjs")
|
|
450
|
+
},
|
|
430
451
|
{
|
|
431
452
|
src: path3.join(projectDir, "apps", "gitkeep"),
|
|
432
453
|
dest: path3.join(projectDir, "apps", ".gitkeep")
|
|
@@ -450,7 +471,12 @@ async function init(projectName, options) {
|
|
|
450
471
|
const packageJsonPath = path3.join(projectDir, "package.json");
|
|
451
472
|
const packageJson2 = fs3.readJsonSync(packageJsonPath);
|
|
452
473
|
packageJson2.name = projectName;
|
|
453
|
-
|
|
474
|
+
const pmField = getPackageManagerField(packageManager);
|
|
475
|
+
if (pmField) {
|
|
476
|
+
packageJson2.packageManager = pmField;
|
|
477
|
+
} else {
|
|
478
|
+
delete packageJson2.packageManager;
|
|
479
|
+
}
|
|
454
480
|
if (!features.includes("changesets")) {
|
|
455
481
|
delete packageJson2.scripts["changeset"];
|
|
456
482
|
delete packageJson2.scripts["version-packages"];
|
|
@@ -1051,6 +1077,10 @@ ${change.relativePath}:`));
|
|
|
1051
1077
|
}
|
|
1052
1078
|
const dotfilesToRename = [
|
|
1053
1079
|
{ src: path4.join(projectDir, "gitignore"), dest: path4.join(projectDir, ".gitignore") },
|
|
1080
|
+
{
|
|
1081
|
+
src: path4.join(projectDir, "lintstagedrc.cjs"),
|
|
1082
|
+
dest: path4.join(projectDir, ".lintstagedrc.cjs")
|
|
1083
|
+
},
|
|
1054
1084
|
{
|
|
1055
1085
|
src: path4.join(projectDir, "apps", "gitkeep"),
|
|
1056
1086
|
dest: path4.join(projectDir, "apps", ".gitkeep")
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@repo/auth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./hooks": {
|
|
15
|
+
"types": "./dist/hooks/index.d.ts",
|
|
16
|
+
"import": "./dist/hooks/index.mjs",
|
|
17
|
+
"require": "./dist/hooks/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./components": {
|
|
20
|
+
"types": "./dist/components/index.d.ts",
|
|
21
|
+
"import": "./dist/components/index.mjs",
|
|
22
|
+
"require": "./dist/components/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./next": {
|
|
25
|
+
"types": "./dist/next/index.d.ts",
|
|
26
|
+
"import": "./dist/next/index.mjs",
|
|
27
|
+
"require": "./dist/next/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"lint": "eslint src/",
|
|
34
|
+
"lint:fix": "eslint src/ --fix",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"js-cookie": "^3.0.5",
|
|
39
|
+
"jwt-decode": "^4.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@repo/config": "workspace:*",
|
|
43
|
+
"@types/js-cookie": "^3.0.6",
|
|
44
|
+
"@types/node": "^22.0.0",
|
|
45
|
+
"@types/react": "^19.2.8",
|
|
46
|
+
"@types/react-dom": "^19.2.3",
|
|
47
|
+
"react": "^19.2.3",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"typescript": "^5.4.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
53
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
54
|
+
"next": ">=13.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"next": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRequireAuth } from '../hooks/useRequireAuth';
|
|
4
|
+
import type { ProtectedRouteProps } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Protected route component that requires authentication
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Basic usage
|
|
12
|
+
* <ProtectedRoute redirectTo="/login">
|
|
13
|
+
* <Dashboard />
|
|
14
|
+
* </ProtectedRoute>
|
|
15
|
+
*
|
|
16
|
+
* // With loading fallback
|
|
17
|
+
* <ProtectedRoute
|
|
18
|
+
* redirectTo="/login"
|
|
19
|
+
* fallback={<LoadingSpinner />}
|
|
20
|
+
* >
|
|
21
|
+
* <Dashboard />
|
|
22
|
+
* </ProtectedRoute>
|
|
23
|
+
*
|
|
24
|
+
* // With role-based access
|
|
25
|
+
* <ProtectedRoute
|
|
26
|
+
* redirectTo="/unauthorized"
|
|
27
|
+
* roles={['admin', 'moderator']}
|
|
28
|
+
* >
|
|
29
|
+
* <AdminPanel />
|
|
30
|
+
* </ProtectedRoute>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function ProtectedRoute({
|
|
34
|
+
children,
|
|
35
|
+
fallback,
|
|
36
|
+
redirectTo = '/login',
|
|
37
|
+
roles,
|
|
38
|
+
permissions,
|
|
39
|
+
}: ProtectedRouteProps) {
|
|
40
|
+
const { isAuthenticated, isLoading, user } = useRequireAuth({
|
|
41
|
+
redirectTo,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Show loading state
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return fallback ? <>{fallback}</> : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Not authenticated - useRequireAuth will handle redirect
|
|
50
|
+
if (!isAuthenticated) {
|
|
51
|
+
return fallback ? <>{fallback}</> : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check roles if specified
|
|
55
|
+
if (roles && roles.length > 0) {
|
|
56
|
+
const userRoles = (user?.metadata?.roles as string[]) || [];
|
|
57
|
+
const hasRole = roles.some(role => userRoles.includes(role));
|
|
58
|
+
|
|
59
|
+
if (!hasRole) {
|
|
60
|
+
return fallback ? <>{fallback}</> : null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check permissions if specified
|
|
65
|
+
if (permissions && permissions.length > 0) {
|
|
66
|
+
const userPermissions = (user?.metadata?.permissions as string[]) || [];
|
|
67
|
+
const hasPermission = permissions.every(perm => userPermissions.includes(perm));
|
|
68
|
+
|
|
69
|
+
if (!hasPermission) {
|
|
70
|
+
return fallback ? <>{fallback}</> : null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return <>{children}</>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, SignInFormProps } from '../types';
|
|
7
|
+
|
|
8
|
+
import { SocialButtons } from './SocialButtons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign in form component
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SignInForm
|
|
16
|
+
* providers={['google', 'github']}
|
|
17
|
+
* onSuccess={(response) => router.push('/dashboard')}
|
|
18
|
+
* showRememberMe
|
|
19
|
+
* showForgotPassword
|
|
20
|
+
* forgotPasswordUrl="/forgot-password"
|
|
21
|
+
* signUpUrl="/signup"
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function SignInForm({
|
|
26
|
+
onSuccess,
|
|
27
|
+
onError,
|
|
28
|
+
providers = [],
|
|
29
|
+
showRememberMe = true,
|
|
30
|
+
showForgotPassword = true,
|
|
31
|
+
forgotPasswordUrl = '/forgot-password',
|
|
32
|
+
signUpUrl = '/signup',
|
|
33
|
+
redirectUrl,
|
|
34
|
+
className,
|
|
35
|
+
}: SignInFormProps) {
|
|
36
|
+
const { signIn, isLoading, error } = useAuth();
|
|
37
|
+
|
|
38
|
+
const [email, setEmail] = useState('');
|
|
39
|
+
const [password, setPassword] = useState('');
|
|
40
|
+
const [remember, setRemember] = useState(false);
|
|
41
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setFormError(null);
|
|
46
|
+
|
|
47
|
+
if (!email || !password) {
|
|
48
|
+
setFormError('Please fill in all fields');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await signIn({ email, password, remember });
|
|
54
|
+
onSuccess?.(response);
|
|
55
|
+
|
|
56
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
57
|
+
window.location.href = redirectUrl;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const authError = err as AuthError;
|
|
61
|
+
setFormError(authError.message);
|
|
62
|
+
onError?.(authError);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const displayError = formError || error?.message;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={className}>
|
|
70
|
+
<form onSubmit={e => void handleSubmit(e)}>
|
|
71
|
+
{displayError && (
|
|
72
|
+
<div role="alert" aria-live="polite">
|
|
73
|
+
{displayError}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<label htmlFor="email">Email</label>
|
|
79
|
+
<input
|
|
80
|
+
id="email"
|
|
81
|
+
name="email"
|
|
82
|
+
type="email"
|
|
83
|
+
autoComplete="email"
|
|
84
|
+
required
|
|
85
|
+
value={email}
|
|
86
|
+
onChange={e => setEmail(e.target.value)}
|
|
87
|
+
disabled={isLoading}
|
|
88
|
+
placeholder="you@example.com"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<label htmlFor="password">Password</label>
|
|
94
|
+
<input
|
|
95
|
+
id="password"
|
|
96
|
+
name="password"
|
|
97
|
+
type="password"
|
|
98
|
+
autoComplete="current-password"
|
|
99
|
+
required
|
|
100
|
+
value={password}
|
|
101
|
+
onChange={e => setPassword(e.target.value)}
|
|
102
|
+
disabled={isLoading}
|
|
103
|
+
placeholder="••••••••"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
{showRememberMe && (
|
|
109
|
+
<label>
|
|
110
|
+
<input
|
|
111
|
+
type="checkbox"
|
|
112
|
+
checked={remember}
|
|
113
|
+
onChange={e => setRemember(e.target.checked)}
|
|
114
|
+
disabled={isLoading}
|
|
115
|
+
/>
|
|
116
|
+
<span>Remember me</span>
|
|
117
|
+
</label>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{showForgotPassword && <a href={forgotPasswordUrl}>Forgot password?</a>}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<button type="submit" disabled={isLoading}>
|
|
124
|
+
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
{providers.length > 0 && (
|
|
129
|
+
<>
|
|
130
|
+
<div>
|
|
131
|
+
<span>Or continue with</span>
|
|
132
|
+
</div>
|
|
133
|
+
<SocialButtons
|
|
134
|
+
providers={providers}
|
|
135
|
+
mode="signin"
|
|
136
|
+
onSuccess={() => {
|
|
137
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
138
|
+
window.location.href = redirectUrl;
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
onError={onError}
|
|
142
|
+
/>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{signUpUrl && (
|
|
147
|
+
<p>
|
|
148
|
+
Don't have an account? <a href={signUpUrl}>Sign up</a>
|
|
149
|
+
</p>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, SignUpFormProps } from '../types';
|
|
7
|
+
|
|
8
|
+
import { SocialButtons } from './SocialButtons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign up form component
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SignUpForm
|
|
16
|
+
* providers={['google', 'github']}
|
|
17
|
+
* onSuccess={(response) => router.push('/onboarding')}
|
|
18
|
+
* showName
|
|
19
|
+
* signInUrl="/signin"
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function SignUpForm({
|
|
24
|
+
onSuccess,
|
|
25
|
+
onError,
|
|
26
|
+
providers = [],
|
|
27
|
+
showName = true,
|
|
28
|
+
signInUrl = '/signin',
|
|
29
|
+
redirectUrl,
|
|
30
|
+
className,
|
|
31
|
+
}: SignUpFormProps) {
|
|
32
|
+
const { signUp, isLoading, error } = useAuth();
|
|
33
|
+
|
|
34
|
+
const [email, setEmail] = useState('');
|
|
35
|
+
const [password, setPassword] = useState('');
|
|
36
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
37
|
+
const [name, setName] = useState('');
|
|
38
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setFormError(null);
|
|
43
|
+
|
|
44
|
+
if (!email || !password) {
|
|
45
|
+
setFormError('Please fill in all required fields');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (password !== confirmPassword) {
|
|
50
|
+
setFormError('Passwords do not match');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (password.length < 8) {
|
|
55
|
+
setFormError('Password must be at least 8 characters');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await signUp({
|
|
61
|
+
email,
|
|
62
|
+
password,
|
|
63
|
+
name: showName ? name : undefined,
|
|
64
|
+
});
|
|
65
|
+
onSuccess?.(response);
|
|
66
|
+
|
|
67
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
68
|
+
window.location.href = redirectUrl;
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const authError = err as AuthError;
|
|
72
|
+
setFormError(authError.message);
|
|
73
|
+
onError?.(authError);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const displayError = formError || error?.message;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={className}>
|
|
81
|
+
<form onSubmit={e => void handleSubmit(e)}>
|
|
82
|
+
{displayError && (
|
|
83
|
+
<div role="alert" aria-live="polite">
|
|
84
|
+
{displayError}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{showName && (
|
|
89
|
+
<div>
|
|
90
|
+
<label htmlFor="name">Name</label>
|
|
91
|
+
<input
|
|
92
|
+
id="name"
|
|
93
|
+
name="name"
|
|
94
|
+
type="text"
|
|
95
|
+
autoComplete="name"
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={e => setName(e.target.value)}
|
|
98
|
+
disabled={isLoading}
|
|
99
|
+
placeholder="John Doe"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div>
|
|
105
|
+
<label htmlFor="email">Email</label>
|
|
106
|
+
<input
|
|
107
|
+
id="email"
|
|
108
|
+
name="email"
|
|
109
|
+
type="email"
|
|
110
|
+
autoComplete="email"
|
|
111
|
+
required
|
|
112
|
+
value={email}
|
|
113
|
+
onChange={e => setEmail(e.target.value)}
|
|
114
|
+
disabled={isLoading}
|
|
115
|
+
placeholder="you@example.com"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div>
|
|
120
|
+
<label htmlFor="password">Password</label>
|
|
121
|
+
<input
|
|
122
|
+
id="password"
|
|
123
|
+
name="password"
|
|
124
|
+
type="password"
|
|
125
|
+
autoComplete="new-password"
|
|
126
|
+
required
|
|
127
|
+
value={password}
|
|
128
|
+
onChange={e => setPassword(e.target.value)}
|
|
129
|
+
disabled={isLoading}
|
|
130
|
+
placeholder="••••••••"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<label htmlFor="confirmPassword">Confirm Password</label>
|
|
136
|
+
<input
|
|
137
|
+
id="confirmPassword"
|
|
138
|
+
name="confirmPassword"
|
|
139
|
+
type="password"
|
|
140
|
+
autoComplete="new-password"
|
|
141
|
+
required
|
|
142
|
+
value={confirmPassword}
|
|
143
|
+
onChange={e => setConfirmPassword(e.target.value)}
|
|
144
|
+
disabled={isLoading}
|
|
145
|
+
placeholder="••••••••"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<button type="submit" disabled={isLoading}>
|
|
150
|
+
{isLoading ? 'Creating account...' : 'Create account'}
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
|
|
154
|
+
{providers.length > 0 && (
|
|
155
|
+
<>
|
|
156
|
+
<div>
|
|
157
|
+
<span>Or continue with</span>
|
|
158
|
+
</div>
|
|
159
|
+
<SocialButtons
|
|
160
|
+
providers={providers}
|
|
161
|
+
mode="signup"
|
|
162
|
+
onSuccess={() => {
|
|
163
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
164
|
+
window.location.href = redirectUrl;
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
onError={onError}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{signInUrl && (
|
|
173
|
+
<p>
|
|
174
|
+
Already have an account? <a href={signInUrl}>Sign in</a>
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, AuthProvider, SocialButtonsProps } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Social provider icons (SVG)
|
|
10
|
+
*/
|
|
11
|
+
const ProviderIcons: Record<string, React.ReactNode> = {
|
|
12
|
+
google: (
|
|
13
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
14
|
+
<path
|
|
15
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
16
|
+
fill="#4285F4"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
20
|
+
fill="#34A853"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
24
|
+
fill="#FBBC05"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
28
|
+
fill="#EA4335"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
),
|
|
32
|
+
github: (
|
|
33
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
34
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
35
|
+
</svg>
|
|
36
|
+
),
|
|
37
|
+
facebook: (
|
|
38
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="#1877F2">
|
|
39
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
|
40
|
+
</svg>
|
|
41
|
+
),
|
|
42
|
+
apple: (
|
|
43
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
44
|
+
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
45
|
+
</svg>
|
|
46
|
+
),
|
|
47
|
+
twitter: (
|
|
48
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
49
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
50
|
+
</svg>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provider display names
|
|
56
|
+
*/
|
|
57
|
+
const ProviderNames: Record<string, string> = {
|
|
58
|
+
google: 'Google',
|
|
59
|
+
github: 'GitHub',
|
|
60
|
+
facebook: 'Facebook',
|
|
61
|
+
apple: 'Apple',
|
|
62
|
+
twitter: 'X (Twitter)',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface SocialButtonProps {
|
|
66
|
+
provider: AuthProvider;
|
|
67
|
+
onClick: () => void;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
mode?: 'signin' | 'signup';
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Individual social login button
|
|
75
|
+
*/
|
|
76
|
+
export function SocialButton({
|
|
77
|
+
provider,
|
|
78
|
+
onClick,
|
|
79
|
+
disabled,
|
|
80
|
+
mode = 'signin',
|
|
81
|
+
className,
|
|
82
|
+
}: SocialButtonProps) {
|
|
83
|
+
const actionText = mode === 'signin' ? 'Sign in' : 'Sign up';
|
|
84
|
+
const providerName = ProviderNames[provider] || provider;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={onClick}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
className={className}
|
|
92
|
+
aria-label={`${actionText} with ${providerName}`}
|
|
93
|
+
>
|
|
94
|
+
{ProviderIcons[provider]}
|
|
95
|
+
<span>
|
|
96
|
+
{actionText} with {providerName}
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Social login buttons component
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* <SocialButtons
|
|
108
|
+
* providers={['google', 'github']}
|
|
109
|
+
* mode="signin"
|
|
110
|
+
* onSuccess={(provider) => console.log(`Signed in with ${provider}`)}
|
|
111
|
+
* />
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function SocialButtons({
|
|
115
|
+
providers,
|
|
116
|
+
onSuccess,
|
|
117
|
+
onError,
|
|
118
|
+
mode = 'signin',
|
|
119
|
+
className,
|
|
120
|
+
}: SocialButtonsProps) {
|
|
121
|
+
const { signInWithProvider, isLoading } = useAuth();
|
|
122
|
+
|
|
123
|
+
const handleProviderClick = async (provider: AuthProvider) => {
|
|
124
|
+
try {
|
|
125
|
+
await signInWithProvider(provider);
|
|
126
|
+
onSuccess?.(provider);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
onError?.(error as AuthError);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (providers.length === 0) return null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={className}>
|
|
136
|
+
{providers.map(provider => (
|
|
137
|
+
<SocialButton
|
|
138
|
+
key={provider}
|
|
139
|
+
provider={provider}
|
|
140
|
+
onClick={() => void handleProviderClick(provider)}
|
|
141
|
+
disabled={isLoading}
|
|
142
|
+
mode={mode}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|