@slyxup/ui 0.1.0 → 0.2.1
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/components/BillingPortal/BillingPortal.d.ts +22 -0
- package/dist/components/BillingPortal/BillingPortal.d.ts.map +1 -0
- package/dist/components/BillingPortal/BillingPortal.js +59 -0
- package/dist/components/BillingPortal/BillingPortal.js.map +1 -0
- package/dist/components/EmailVerification/EmailVerification.d.ts +9 -0
- package/dist/components/EmailVerification/EmailVerification.d.ts.map +1 -0
- package/dist/components/EmailVerification/EmailVerification.js +72 -0
- package/dist/components/EmailVerification/EmailVerification.js.map +1 -0
- package/dist/components/ForgotPassword/ForgotPassword.d.ts +8 -0
- package/dist/components/ForgotPassword/ForgotPassword.d.ts.map +1 -0
- package/dist/components/ForgotPassword/ForgotPassword.js +43 -0
- package/dist/components/ForgotPassword/ForgotPassword.js.map +1 -0
- package/dist/components/PricingTable/PricingTable.d.ts +19 -0
- package/dist/components/PricingTable/PricingTable.d.ts.map +1 -0
- package/dist/components/PricingTable/PricingTable.js +52 -0
- package/dist/components/PricingTable/PricingTable.js.map +1 -0
- package/dist/components/ResetPassword/ResetPassword.d.ts +9 -0
- package/dist/components/ResetPassword/ResetPassword.d.ts.map +1 -0
- package/dist/components/ResetPassword/ResetPassword.js +50 -0
- package/dist/components/ResetPassword/ResetPassword.js.map +1 -0
- package/dist/components/SignIn/SignIn.d.ts +11 -0
- package/dist/components/SignIn/SignIn.d.ts.map +1 -0
- package/dist/components/SignIn/SignIn.js +53 -0
- package/dist/components/SignIn/SignIn.js.map +1 -0
- package/dist/components/SignUp/SignUp.d.ts +8 -0
- package/dist/components/SignUp/SignUp.d.ts.map +1 -0
- package/dist/components/SignUp/SignUp.js +54 -0
- package/dist/components/SignUp/SignUp.js.map +1 -0
- package/dist/components/SocialButtons/SocialButtons.d.ts +9 -0
- package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -0
- package/dist/components/SocialButtons/SocialButtons.js +16 -0
- package/dist/components/SocialButtons/SocialButtons.js.map +1 -0
- package/dist/components/UserButton/UserButton.d.ts +3 -0
- package/dist/components/UserButton/UserButton.d.ts.map +1 -0
- package/dist/components/UserButton/UserButton.js +43 -0
- package/dist/components/UserButton/UserButton.js.map +1 -0
- package/dist/components/UserProfile/UserProfile.d.ts +3 -0
- package/dist/components/UserProfile/UserProfile.d.ts.map +1 -0
- package/dist/components/UserProfile/UserProfile.js +38 -0
- package/dist/components/UserProfile/UserProfile.js.map +1 -0
- package/dist/icons.d.ts +6 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +15 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.d.ts +9 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +219 -0
- package/dist/styles.js.map +1 -0
- package/package.json +35 -3
- package/src/components/BillingPortal/BillingPortal.tsx +182 -0
- package/src/components/EmailVerification/EmailVerification.tsx +180 -0
- package/src/components/ForgotPassword/ForgotPassword.tsx +122 -0
- package/src/components/PricingTable/PricingTable.tsx +155 -0
- package/src/components/ResetPassword/ResetPassword.tsx +120 -0
- package/src/components/SignIn/SignIn.tsx +168 -0
- package/src/components/SignUp/SignUp.tsx +181 -0
- package/src/components/SocialButtons/SocialButtons.tsx +39 -0
- package/src/components/UserButton/UserButton.tsx +89 -0
- package/src/components/UserProfile/UserProfile.tsx +93 -0
- package/src/icons.tsx +71 -0
- package/src/index.ts +37 -1
- package/src/styles.ts +221 -0
- package/test/components.test.tsx +135 -0
- package/test/icons.test.tsx +32 -0
- package/test/styles.test.ts +47 -0
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SlyxupClient } from '@slyxup/core';
|
|
2
|
+
import { type FormEvent, useEffect, useState } from 'react';
|
|
3
|
+
import { CheckIcon, KeyholeMark } from '../../icons';
|
|
4
|
+
|
|
5
|
+
export interface ForgotPasswordProps {
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
onBackToSignIn?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Request a password-reset email. */
|
|
12
|
+
export function ForgotPassword({
|
|
13
|
+
apiUrl,
|
|
14
|
+
onSuccess,
|
|
15
|
+
onBackToSignIn,
|
|
16
|
+
}: ForgotPasswordProps) {
|
|
17
|
+
const [email, setEmail] = useState('');
|
|
18
|
+
const [sent, setSent] = useState(false);
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (error) {
|
|
24
|
+
const t = setTimeout(() => setError(null), 4000);
|
|
25
|
+
return () => clearTimeout(t);
|
|
26
|
+
}
|
|
27
|
+
}, [error]);
|
|
28
|
+
|
|
29
|
+
async function onSubmit(e: FormEvent) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setBusy(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
try {
|
|
34
|
+
await fetch(
|
|
35
|
+
`${(apiUrl ?? process.env.NEXT_PUBLIC_SLYXUP_API_URL ?? 'https://auth.slyxup.online').replace(/\/$/, '')}/v1/verification/password/forgot`,
|
|
36
|
+
{
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ email }),
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
setSent(true); // always success — never reveal account existence
|
|
43
|
+
onSuccess?.();
|
|
44
|
+
} catch {
|
|
45
|
+
setError('Network problem. Check your connection and try again.');
|
|
46
|
+
} finally {
|
|
47
|
+
setBusy(false);
|
|
48
|
+
}
|
|
49
|
+
void SlyxupClient; // tree-shake guard
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (sent) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="slx-card">
|
|
55
|
+
<div className="slx-success-icon">
|
|
56
|
+
<CheckIcon />
|
|
57
|
+
</div>
|
|
58
|
+
<h1 className="slx-title" style={{ textAlign: 'center' }}>
|
|
59
|
+
Check your email
|
|
60
|
+
</h1>
|
|
61
|
+
<p className="slx-subtitle" style={{ textAlign: 'center' }}>
|
|
62
|
+
If an account exists for {email}, a reset link is on its way.
|
|
63
|
+
</p>
|
|
64
|
+
{onBackToSignIn && (
|
|
65
|
+
<p className="slx-footer">
|
|
66
|
+
<button type="button" className="slx-link" onClick={onBackToSignIn}>
|
|
67
|
+
Back to sign in
|
|
68
|
+
</button>
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
77
|
+
<div className="slx-mark">
|
|
78
|
+
<KeyholeMark />
|
|
79
|
+
</div>
|
|
80
|
+
<h1 className="slx-title">Reset your password</h1>
|
|
81
|
+
<p className="slx-subtitle">
|
|
82
|
+
Enter your email and we'll send you a reset link.
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
{error && (
|
|
86
|
+
<p className="slx-error-text" role="alert">
|
|
87
|
+
{error}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
<form onSubmit={onSubmit}>
|
|
92
|
+
<div className="slx-field">
|
|
93
|
+
<label className="slx-label" htmlFor="slx-forgot-email">
|
|
94
|
+
Email
|
|
95
|
+
</label>
|
|
96
|
+
<input
|
|
97
|
+
id="slx-forgot-email"
|
|
98
|
+
className="slx-input"
|
|
99
|
+
type="email"
|
|
100
|
+
autoComplete="email"
|
|
101
|
+
placeholder="you@example.com"
|
|
102
|
+
value={email}
|
|
103
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
104
|
+
required
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<button className="slx-btn" type="submit" disabled={busy}>
|
|
108
|
+
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
109
|
+
{busy ? 'Sending…' : 'Send reset link'}
|
|
110
|
+
</button>
|
|
111
|
+
</form>
|
|
112
|
+
|
|
113
|
+
{onBackToSignIn && (
|
|
114
|
+
<p className="slx-footer">
|
|
115
|
+
<button type="button" className="slx-link" onClick={onBackToSignIn}>
|
|
116
|
+
Back to sign in
|
|
117
|
+
</button>
|
|
118
|
+
</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Plan {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
amount: number;
|
|
9
|
+
currency: string;
|
|
10
|
+
interval: 'month' | 'year';
|
|
11
|
+
trialDays: number | null;
|
|
12
|
+
features: string[] | null;
|
|
13
|
+
isPopular: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PricingTableProps {
|
|
17
|
+
plans: Plan[];
|
|
18
|
+
onSelect?: (plan: Plan) => void;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Clean pricing grid with popular badge */
|
|
23
|
+
export function PricingTable({ plans, onSelect, loading }: PricingTableProps) {
|
|
24
|
+
if (loading) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className="slx-pricing"
|
|
28
|
+
style={{
|
|
29
|
+
display: 'grid',
|
|
30
|
+
gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
|
|
31
|
+
gap: 16,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{[1, 2, 3].map((i) => (
|
|
35
|
+
<div
|
|
36
|
+
key={i}
|
|
37
|
+
className="slx-card"
|
|
38
|
+
style={{ minHeight: 320 }}
|
|
39
|
+
aria-busy="true"
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
display: 'grid',
|
|
50
|
+
gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
|
|
51
|
+
gap: 16,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{plans.map((plan) => (
|
|
55
|
+
<div
|
|
56
|
+
key={plan.id}
|
|
57
|
+
className={`slx-card${plan.isPopular ? ' slx-card-popular' : ''}`}
|
|
58
|
+
style={{
|
|
59
|
+
position: plan.isPopular ? 'relative' : undefined,
|
|
60
|
+
borderColor: plan.isPopular ? 'var(--slx-accent)' : undefined,
|
|
61
|
+
display: 'flex',
|
|
62
|
+
flexDirection: 'column',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{plan.isPopular && (
|
|
66
|
+
<span
|
|
67
|
+
style={{
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
top: -11,
|
|
70
|
+
right: 20,
|
|
71
|
+
fontSize: 10.5,
|
|
72
|
+
fontFamily: 'var(--slx-mono)',
|
|
73
|
+
background:
|
|
74
|
+
'linear-gradient(135deg, var(--slx-accent), #8b5cf6)',
|
|
75
|
+
color: '#fff',
|
|
76
|
+
padding: '3px 10px',
|
|
77
|
+
borderRadius: 999,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
POPULAR
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
<h3
|
|
84
|
+
style={{ fontSize: 15, fontWeight: 550, color: 'var(--slx-muted)' }}
|
|
85
|
+
>
|
|
86
|
+
{plan.name}
|
|
87
|
+
</h3>
|
|
88
|
+
<div style={{ margin: '8px 0 2px' }}>
|
|
89
|
+
<span
|
|
90
|
+
style={{
|
|
91
|
+
fontSize: 38,
|
|
92
|
+
fontWeight: 750,
|
|
93
|
+
letterSpacing: '-0.03em',
|
|
94
|
+
fontFamily: '"Space Grotesk",sans-serif',
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
${(plan.amount / 100).toFixed(0)}
|
|
98
|
+
</span>
|
|
99
|
+
<span style={{ fontSize: 14, color: 'var(--slx-muted)' }}>
|
|
100
|
+
/{plan.interval}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
{plan.trialDays ? (
|
|
104
|
+
<p
|
|
105
|
+
style={{
|
|
106
|
+
fontSize: 12,
|
|
107
|
+
color: 'var(--slx-accent)',
|
|
108
|
+
marginBottom: 4,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{plan.trialDays} day free trial
|
|
112
|
+
</p>
|
|
113
|
+
) : (
|
|
114
|
+
<p style={{ fontSize: 12, color: 'transparent', marginBottom: 4 }}>
|
|
115
|
+
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
<ul style={{ listStyle: 'none', margin: '14px 0 22px', flex: 1 }}>
|
|
119
|
+
{((plan.features ?? []) as string[]).map((f: string) => (
|
|
120
|
+
<li
|
|
121
|
+
key={f}
|
|
122
|
+
style={{
|
|
123
|
+
fontSize: 13.5,
|
|
124
|
+
color: 'var(--slx-ink)',
|
|
125
|
+
padding: '5px 0 5px 24px',
|
|
126
|
+
position: 'relative',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<span
|
|
130
|
+
style={{
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
left: 0,
|
|
133
|
+
color: '#34d399',
|
|
134
|
+
fontWeight: 700,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
✓
|
|
138
|
+
</span>{' '}
|
|
139
|
+
{f}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
className="slx-btn"
|
|
146
|
+
onClick={() => onSelect?.(plan)}
|
|
147
|
+
disabled={loading}
|
|
148
|
+
>
|
|
149
|
+
Get started
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type FormEvent, useEffect, useState } from 'react';
|
|
2
|
+
import { CheckIcon, KeyholeMark } from '../../icons';
|
|
3
|
+
|
|
4
|
+
export interface ResetPasswordProps {
|
|
5
|
+
/** Reset token (from email link ?token=...) */
|
|
6
|
+
token: string;
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Set a new password using the emailed reset token. */
|
|
12
|
+
export function ResetPassword({
|
|
13
|
+
token,
|
|
14
|
+
apiUrl,
|
|
15
|
+
onSuccess,
|
|
16
|
+
}: ResetPasswordProps) {
|
|
17
|
+
const [password, setPassword] = useState('');
|
|
18
|
+
const [done, setDone] = useState(false);
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (error) {
|
|
24
|
+
const t = setTimeout(() => setError(null), 4000);
|
|
25
|
+
return () => clearTimeout(t);
|
|
26
|
+
}
|
|
27
|
+
}, [error]);
|
|
28
|
+
|
|
29
|
+
async function onSubmit(e: FormEvent) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setBusy(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
try {
|
|
34
|
+
const base = (
|
|
35
|
+
apiUrl ??
|
|
36
|
+
process.env.NEXT_PUBLIC_SLYXUP_API_URL ??
|
|
37
|
+
'https://auth.slyxup.online'
|
|
38
|
+
).replace(/\/$/, '');
|
|
39
|
+
const res = await fetch(`${base}/v1/verification/password/reset`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ token, password }),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const data = await res
|
|
46
|
+
.json()
|
|
47
|
+
.catch(() => ({ error: 'Invalid or expired link' }));
|
|
48
|
+
throw new Error(data.error ?? 'Invalid or expired link');
|
|
49
|
+
}
|
|
50
|
+
setDone(true);
|
|
51
|
+
onSuccess?.();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setError(err instanceof Error ? err.message : 'Something went wrong.');
|
|
54
|
+
} finally {
|
|
55
|
+
setBusy(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (done) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="slx-card">
|
|
62
|
+
<div className="slx-success-icon">
|
|
63
|
+
<CheckIcon />
|
|
64
|
+
</div>
|
|
65
|
+
<h1 className="slx-title" style={{ textAlign: 'center' }}>
|
|
66
|
+
Password updated
|
|
67
|
+
</h1>
|
|
68
|
+
<p className="slx-subtitle" style={{ textAlign: 'center' }}>
|
|
69
|
+
Your password has been changed. Use it to sign in.
|
|
70
|
+
</p>
|
|
71
|
+
{onSuccess && (
|
|
72
|
+
<button type="button" className="slx-btn" onClick={onSuccess}>
|
|
73
|
+
Continue to sign in
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
82
|
+
<div className="slx-mark">
|
|
83
|
+
<KeyholeMark />
|
|
84
|
+
</div>
|
|
85
|
+
<h1 className="slx-title">Choose a new password</h1>
|
|
86
|
+
<p className="slx-subtitle">
|
|
87
|
+
Pick something strong you haven't used before.
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
{error && (
|
|
91
|
+
<p className="slx-error-text" role="alert">
|
|
92
|
+
{error}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<form onSubmit={onSubmit}>
|
|
97
|
+
<div className="slx-field">
|
|
98
|
+
<label className="slx-label" htmlFor="slx-reset-password">
|
|
99
|
+
New password
|
|
100
|
+
</label>
|
|
101
|
+
<input
|
|
102
|
+
id="slx-reset-password"
|
|
103
|
+
className="slx-input"
|
|
104
|
+
type="password"
|
|
105
|
+
autoComplete="new-password"
|
|
106
|
+
placeholder="At least 8 characters"
|
|
107
|
+
value={password}
|
|
108
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
109
|
+
required
|
|
110
|
+
minLength={8}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<button className="slx-btn" type="submit" disabled={busy}>
|
|
114
|
+
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
115
|
+
{busy ? 'Updating…' : 'Update password'}
|
|
116
|
+
</button>
|
|
117
|
+
</form>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SlyxupError } from '@slyxup/core';
|
|
2
|
+
import { useAuth } from '@slyxup/react';
|
|
3
|
+
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
|
|
5
|
+
|
|
6
|
+
export interface SignInProps {
|
|
7
|
+
/** Show social buttons (default true) */
|
|
8
|
+
social?: boolean;
|
|
9
|
+
/** Called after successful sign in */
|
|
10
|
+
onSuccess?: () => void;
|
|
11
|
+
/** Switch to sign up */
|
|
12
|
+
onSignUpClick?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Email/password + OAuth sign-in card. */
|
|
16
|
+
export function SignIn({
|
|
17
|
+
social = true,
|
|
18
|
+
onSuccess,
|
|
19
|
+
onSignUpClick,
|
|
20
|
+
}: SignInProps) {
|
|
21
|
+
const { signIn, client } = useAuth() as unknown as {
|
|
22
|
+
signIn: ReturnType<typeof useAuth>['signIn'];
|
|
23
|
+
client: { publishableKey?: string };
|
|
24
|
+
};
|
|
25
|
+
const [email, setEmail] = useState('');
|
|
26
|
+
const [password, setPassword] = useState('');
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [error, setError] = useState<string | null>(null);
|
|
29
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (error) {
|
|
33
|
+
const t = setTimeout(() => setError(null), 4000);
|
|
34
|
+
return () => clearTimeout(t);
|
|
35
|
+
}
|
|
36
|
+
}, [error]);
|
|
37
|
+
|
|
38
|
+
async function onSubmit(e: FormEvent) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
setBusy(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
try {
|
|
43
|
+
await signIn({ email, password });
|
|
44
|
+
onSuccess?.();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
setError(
|
|
47
|
+
err instanceof SlyxupError
|
|
48
|
+
? err.message
|
|
49
|
+
: 'Something went wrong. Try again.'
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
setBusy(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function oauth(provider: 'google' | 'github') {
|
|
57
|
+
window.location.href = `/v1/oauth/${provider}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const missingKey =
|
|
61
|
+
!client.publishableKey ||
|
|
62
|
+
client.publishableKey === 'pk_test_missing' ||
|
|
63
|
+
client.publishableKey.includes('REPLACE');
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
67
|
+
{missingKey && (
|
|
68
|
+
<p
|
|
69
|
+
style={{
|
|
70
|
+
fontSize: 12,
|
|
71
|
+
background: '#fff3cd',
|
|
72
|
+
border: '1px solid #ffe69c',
|
|
73
|
+
borderRadius: 8,
|
|
74
|
+
padding: '8px 10px',
|
|
75
|
+
marginBottom: 14,
|
|
76
|
+
lineHeight: 1.4,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<strong>Setup:</strong> Add{' '}
|
|
80
|
+
<code>NEXT_PUBLIC_SLYXUP_PUBLISHABLE_KEY</code> to{' '}
|
|
81
|
+
<code>.env.local</code> — run <code>npx @slyxup/cli keys create</code>
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
<div className="slx-mark">
|
|
85
|
+
<KeyholeMark />
|
|
86
|
+
</div>
|
|
87
|
+
<h1 className="slx-title">Sign in</h1>
|
|
88
|
+
<p className="slx-subtitle">
|
|
89
|
+
Welcome back. Enter your details to continue.
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
{social && (
|
|
93
|
+
<>
|
|
94
|
+
<div className="slx-social">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="slx-social-btn"
|
|
98
|
+
onClick={() => oauth('google')}
|
|
99
|
+
>
|
|
100
|
+
<GoogleIcon /> Continue with Google
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="slx-social-btn"
|
|
105
|
+
onClick={() => oauth('github')}
|
|
106
|
+
>
|
|
107
|
+
<GitHubIcon /> Continue with GitHub
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="slx-divider">or</div>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{error && (
|
|
115
|
+
<p className="slx-error-text" role="alert">
|
|
116
|
+
{error}
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<form onSubmit={onSubmit} noValidate={false}>
|
|
121
|
+
<div className="slx-field">
|
|
122
|
+
<label className="slx-label" htmlFor="slx-signin-email">
|
|
123
|
+
Email
|
|
124
|
+
</label>
|
|
125
|
+
<input
|
|
126
|
+
id="slx-signin-email"
|
|
127
|
+
className="slx-input"
|
|
128
|
+
type="email"
|
|
129
|
+
autoComplete="email"
|
|
130
|
+
placeholder="you@example.com"
|
|
131
|
+
value={email}
|
|
132
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
133
|
+
required
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="slx-field">
|
|
137
|
+
<label className="slx-label" htmlFor="slx-signin-password">
|
|
138
|
+
Password
|
|
139
|
+
</label>
|
|
140
|
+
<input
|
|
141
|
+
id="slx-signin-password"
|
|
142
|
+
className="slx-input"
|
|
143
|
+
type="password"
|
|
144
|
+
autoComplete="current-password"
|
|
145
|
+
placeholder="••••••••"
|
|
146
|
+
value={password}
|
|
147
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
148
|
+
required
|
|
149
|
+
minLength={8}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<button className="slx-btn" type="submit" disabled={busy}>
|
|
153
|
+
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
154
|
+
{busy ? 'Signing in…' : 'Sign in'}
|
|
155
|
+
</button>
|
|
156
|
+
</form>
|
|
157
|
+
|
|
158
|
+
{onSignUpClick && (
|
|
159
|
+
<p className="slx-footer">
|
|
160
|
+
Don't have an account?{' '}
|
|
161
|
+
<button type="button" className="slx-link" onClick={onSignUpClick}>
|
|
162
|
+
Sign up
|
|
163
|
+
</button>
|
|
164
|
+
</p>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|