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.
Files changed (27) hide show
  1. package/dist/index.js +31 -1
  2. package/package.json +1 -1
  3. package/templates/default/lintstagedrc.cjs +4 -0
  4. package/templates/default/packages/auth/package.json +61 -0
  5. package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
  6. package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
  7. package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
  8. package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
  9. package/templates/default/packages/auth/src/components/index.ts +4 -0
  10. package/templates/default/packages/auth/src/hooks/index.ts +4 -0
  11. package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
  12. package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
  13. package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
  14. package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
  15. package/templates/default/packages/auth/src/index.ts +45 -0
  16. package/templates/default/packages/auth/src/next/index.ts +18 -0
  17. package/templates/default/packages/auth/src/next/middleware.ts +183 -0
  18. package/templates/default/packages/auth/src/next/server.ts +219 -0
  19. package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
  20. package/templates/default/packages/auth/src/providers/index.ts +1 -0
  21. package/templates/default/packages/auth/src/types/index.ts +284 -0
  22. package/templates/default/packages/auth/src/utils/api.ts +228 -0
  23. package/templates/default/packages/auth/src/utils/index.ts +3 -0
  24. package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
  25. package/templates/default/packages/auth/src/utils/token.ts +204 -0
  26. package/templates/default/packages/auth/tsconfig.json +14 -0
  27. 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
- delete packageJson2.packageManager;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nexu",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "CLI to create and update Nexu monorepo projects",
5
5
  "author": "Nexu Team",
6
6
  "license": "MIT",
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ '*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'],
3
+ '*.{json,md,yml,yaml}': ['prettier --write'],
4
+ };
@@ -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&apos;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
+ }
@@ -0,0 +1,4 @@
1
+ export { SignInForm } from './SignInForm';
2
+ export { SignUpForm } from './SignUpForm';
3
+ export { SocialButtons, SocialButton } from './SocialButtons';
4
+ export { ProtectedRoute } from './ProtectedRoute';