@slyxup/ui 0.2.15 → 0.3.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/package.json +6 -3
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -70
- package/src/components/AdminPanel/AdminPanel.tsx +0 -1081
- package/src/components/BillingPortal/BillingPortal.tsx +0 -184
- package/src/components/EmailVerification/EmailVerification.tsx +0 -182
- package/src/components/ForgotPassword/ForgotPassword.tsx +0 -124
- package/src/components/PricingTable/PricingTable.tsx +0 -157
- package/src/components/ResetPassword/ResetPassword.tsx +0 -122
- package/src/components/SignIn/SignIn.tsx +0 -239
- package/src/components/SignUp/SignUp.tsx +0 -211
- package/src/components/SocialButtons/SocialButtons.tsx +0 -47
- package/src/components/UserButton/UserButton.tsx +0 -101
- package/src/components/UserProfile/UserProfile.tsx +0 -1776
- package/src/icons.tsx +0 -71
- package/src/index.ts +0 -56
- package/src/lib/paddle.ts +0 -131
- package/src/styles.ts +0 -619
- package/test/components.test.tsx +0 -135
- package/test/icons.test.tsx +0 -32
- package/test/styles.test.ts +0 -47
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -2
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { injectStyles } from '../../styles';
|
|
5
|
-
|
|
6
|
-
interface Subscription {
|
|
7
|
-
id: string;
|
|
8
|
-
status: string;
|
|
9
|
-
currentPeriodEnd: string | null;
|
|
10
|
-
cancelAtPeriodEnd: boolean;
|
|
11
|
-
}
|
|
12
|
-
interface Invoice {
|
|
13
|
-
id: string;
|
|
14
|
-
amount: number;
|
|
15
|
-
currency: string;
|
|
16
|
-
status: 'paid' | 'pending' | 'overdue' | 'refunded';
|
|
17
|
-
billedAt: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface BillingPortalProps {
|
|
21
|
-
subscription: Subscription | null;
|
|
22
|
-
invoices: Invoice[];
|
|
23
|
-
onCancel?: () => void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Current plan + invoices table */
|
|
27
|
-
export function BillingPortal({
|
|
28
|
-
subscription,
|
|
29
|
-
invoices,
|
|
30
|
-
onCancel,
|
|
31
|
-
}: BillingPortalProps) {
|
|
32
|
-
injectStyles();
|
|
33
|
-
if (!subscription) {
|
|
34
|
-
return (
|
|
35
|
-
<div className="slx-card">
|
|
36
|
-
<h3 style={{ fontSize: 16, fontWeight: 600 }}>
|
|
37
|
-
No active subscription
|
|
38
|
-
</h3>
|
|
39
|
-
<p style={{ fontSize: 13.5, color: 'var(--slx-muted)', marginTop: 6 }}>
|
|
40
|
-
You don't have a subscription yet. Choose a plan to get started.
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
|
48
|
-
{/* Plan card */}
|
|
49
|
-
<div className="slx-card">
|
|
50
|
-
<div
|
|
51
|
-
style={{
|
|
52
|
-
display: 'flex',
|
|
53
|
-
justifyContent: 'space-between',
|
|
54
|
-
alignItems: 'center',
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
<div>
|
|
58
|
-
<h3 style={{ fontSize: 15, fontWeight: 600 }}>Current plan</h3>
|
|
59
|
-
<p
|
|
60
|
-
style={{ fontSize: 13, color: 'var(--slx-muted)', marginTop: 4 }}
|
|
61
|
-
>
|
|
62
|
-
Status:{' '}
|
|
63
|
-
<span
|
|
64
|
-
style={{
|
|
65
|
-
fontWeight: 650,
|
|
66
|
-
color:
|
|
67
|
-
subscription.status === 'active' ||
|
|
68
|
-
subscription.status === 'trialing'
|
|
69
|
-
? '#34d399'
|
|
70
|
-
: subscription.status === 'past_due'
|
|
71
|
-
? 'var(--slx-danger)'
|
|
72
|
-
: 'var(--slx-muted)',
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{subscription.status}
|
|
76
|
-
</span>
|
|
77
|
-
</p>
|
|
78
|
-
{subscription.currentPeriodEnd && (
|
|
79
|
-
<p
|
|
80
|
-
style={{
|
|
81
|
-
fontSize: 12.5,
|
|
82
|
-
color: 'var(--slx-muted)',
|
|
83
|
-
marginTop: 4,
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
Renews{' '}
|
|
87
|
-
{new Date(subscription.currentPeriodEnd).toLocaleDateString()}
|
|
88
|
-
{subscription.cancelAtPeriodEnd
|
|
89
|
-
? ' (cancels at period end)'
|
|
90
|
-
: ''}
|
|
91
|
-
</p>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
{subscription.status !== 'canceled' &&
|
|
95
|
-
!subscription.cancelAtPeriodEnd &&
|
|
96
|
-
onCancel && (
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={onCancel}
|
|
100
|
-
style={{
|
|
101
|
-
fontSize: 13,
|
|
102
|
-
fontWeight: 550,
|
|
103
|
-
background: 'transparent',
|
|
104
|
-
border: '1px solid var(--slx-border)',
|
|
105
|
-
borderRadius: 'var(--slx-radius)',
|
|
106
|
-
padding: '8px 14px',
|
|
107
|
-
cursor: 'pointer',
|
|
108
|
-
color: 'var(--slx-danger)',
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
Cancel
|
|
112
|
-
</button>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
{/* Invoices */}
|
|
118
|
-
{invoices.length > 0 && (
|
|
119
|
-
<div className="slx-card">
|
|
120
|
-
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12 }}>
|
|
121
|
-
Invoices
|
|
122
|
-
</h3>
|
|
123
|
-
<table
|
|
124
|
-
style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}
|
|
125
|
-
>
|
|
126
|
-
<thead>
|
|
127
|
-
<tr
|
|
128
|
-
style={{
|
|
129
|
-
borderBottom: '1px solid var(--slx-border)',
|
|
130
|
-
textAlign: 'left',
|
|
131
|
-
}}
|
|
132
|
-
>
|
|
133
|
-
<th style={{ padding: '6px 0', fontWeight: 550 }}>Date</th>
|
|
134
|
-
<th style={{ padding: '6px 0', fontWeight: 550 }}>Amount</th>
|
|
135
|
-
<th style={{ padding: '6px 0', fontWeight: 550 }}>Status</th>
|
|
136
|
-
</tr>
|
|
137
|
-
</thead>
|
|
138
|
-
<tbody>
|
|
139
|
-
{invoices.map((inv) => (
|
|
140
|
-
<tr
|
|
141
|
-
key={inv.id}
|
|
142
|
-
style={{ borderBottom: '1px solid var(--slx-border)' }}
|
|
143
|
-
>
|
|
144
|
-
<td style={{ padding: '8px 0' }}>
|
|
145
|
-
{inv.billedAt
|
|
146
|
-
? new Date(inv.billedAt).toLocaleDateString()
|
|
147
|
-
: '—'}
|
|
148
|
-
</td>
|
|
149
|
-
<td style={{ padding: '8px 0' }}>
|
|
150
|
-
${(inv.amount / 100).toFixed(2)} {inv.currency}
|
|
151
|
-
</td>
|
|
152
|
-
<td style={{ padding: '8px 0' }}>
|
|
153
|
-
<span
|
|
154
|
-
style={{
|
|
155
|
-
fontSize: 11.5,
|
|
156
|
-
fontWeight: 600,
|
|
157
|
-
padding: '2px 8px',
|
|
158
|
-
borderRadius: 999,
|
|
159
|
-
background:
|
|
160
|
-
inv.status === 'paid'
|
|
161
|
-
? 'rgba(52,211,153,.1)'
|
|
162
|
-
: inv.status === 'overdue'
|
|
163
|
-
? 'rgba(214,69,80,.1)'
|
|
164
|
-
: 'rgba(108,108,232,.08)',
|
|
165
|
-
color:
|
|
166
|
-
inv.status === 'paid'
|
|
167
|
-
? '#34d399'
|
|
168
|
-
: inv.status === 'overdue'
|
|
169
|
-
? 'var(--slx-danger)'
|
|
170
|
-
: 'var(--slx-accent)',
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
{inv.status}
|
|
174
|
-
</span>
|
|
175
|
-
</td>
|
|
176
|
-
</tr>
|
|
177
|
-
))}
|
|
178
|
-
</tbody>
|
|
179
|
-
</table>
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { type FormEvent, useEffect, useState } from 'react';
|
|
2
|
-
import { CheckIcon, KeyholeMark } from '../../icons';
|
|
3
|
-
import { injectStyles } from '../../styles';
|
|
4
|
-
|
|
5
|
-
export interface EmailVerificationProps {
|
|
6
|
-
/** Verification token (from email link ?token=...). If absent, shows resend form. */
|
|
7
|
-
token?: string;
|
|
8
|
-
apiUrl?: string;
|
|
9
|
-
onSuccess?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Verify an email with the emailed token, or request a new link. */
|
|
13
|
-
export function EmailVerification({
|
|
14
|
-
token,
|
|
15
|
-
apiUrl,
|
|
16
|
-
onSuccess,
|
|
17
|
-
}: EmailVerificationProps) {
|
|
18
|
-
injectStyles();
|
|
19
|
-
const [status, setStatus] = useState<
|
|
20
|
-
'verifying' | 'success' | 'error' | 'resend'
|
|
21
|
-
>(token ? 'verifying' : 'resend');
|
|
22
|
-
const [message, setMessage] = useState<string>('');
|
|
23
|
-
const [email, setEmail] = useState('');
|
|
24
|
-
const [busy, setBusy] = useState(false);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!token) return;
|
|
28
|
-
let cancelled = false;
|
|
29
|
-
(async () => {
|
|
30
|
-
try {
|
|
31
|
-
const base = (
|
|
32
|
-
apiUrl ??
|
|
33
|
-
process.env.NEXT_PUBLIC_SLYXUP_API_URL ??
|
|
34
|
-
'https://auth.slyxup.online'
|
|
35
|
-
).replace(/\/$/, '');
|
|
36
|
-
const res = await fetch(`${base}/v1/verification/verify`, {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ token }),
|
|
40
|
-
});
|
|
41
|
-
if (cancelled) return;
|
|
42
|
-
if (res.ok) {
|
|
43
|
-
setStatus('success');
|
|
44
|
-
setTimeout(() => onSuccess?.(), 1800);
|
|
45
|
-
} else {
|
|
46
|
-
const data = await res
|
|
47
|
-
.json()
|
|
48
|
-
.catch(() => ({ error: 'Invalid or expired link' }));
|
|
49
|
-
setMessage(data.error ?? 'Invalid or expired link');
|
|
50
|
-
setStatus('error');
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
if (!cancelled) {
|
|
54
|
-
setMessage('Network problem. Try again.');
|
|
55
|
-
setStatus('error');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
})();
|
|
59
|
-
return () => {
|
|
60
|
-
cancelled = true;
|
|
61
|
-
};
|
|
62
|
-
}, [token, apiUrl, onSuccess]);
|
|
63
|
-
|
|
64
|
-
async function resend(e: FormEvent) {
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
setBusy(true);
|
|
67
|
-
try {
|
|
68
|
-
const base = (
|
|
69
|
-
apiUrl ??
|
|
70
|
-
process.env.NEXT_PUBLIC_SLYXUP_API_URL ??
|
|
71
|
-
'https://auth.slyxup.online'
|
|
72
|
-
).replace(/\/$/, '');
|
|
73
|
-
await fetch(`${base}/v1/verification/resend`, {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: { 'Content-Type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ email }),
|
|
77
|
-
});
|
|
78
|
-
setMessage(
|
|
79
|
-
'If that account exists, a new verification link is on its way.'
|
|
80
|
-
);
|
|
81
|
-
} catch {
|
|
82
|
-
setMessage('Network problem. Try again.');
|
|
83
|
-
} finally {
|
|
84
|
-
setBusy(false);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<div className="slx-card">
|
|
90
|
-
<div className="slx-mark">
|
|
91
|
-
<KeyholeMark />
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
{status === 'verifying' && (
|
|
95
|
-
<>
|
|
96
|
-
<h1 className="slx-title">Verifying your email…</h1>
|
|
97
|
-
<p className="slx-subtitle">One moment while we confirm.</p>
|
|
98
|
-
</>
|
|
99
|
-
)}
|
|
100
|
-
|
|
101
|
-
{status === 'success' && (
|
|
102
|
-
<>
|
|
103
|
-
<div className="slx-success-icon">
|
|
104
|
-
<CheckIcon />
|
|
105
|
-
</div>
|
|
106
|
-
<h1 className="slx-title" style={{ textAlign: 'center' }}>
|
|
107
|
-
Email verified
|
|
108
|
-
</h1>
|
|
109
|
-
<p className="slx-subtitle" style={{ textAlign: 'center' }}>
|
|
110
|
-
You're all set. Redirecting…
|
|
111
|
-
</p>
|
|
112
|
-
</>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
{status === 'error' && (
|
|
116
|
-
<>
|
|
117
|
-
<h1 className="slx-title">Link didn't work</h1>
|
|
118
|
-
{message && (
|
|
119
|
-
<p className="slx-error-text" role="alert">
|
|
120
|
-
{message}
|
|
121
|
-
</p>
|
|
122
|
-
)}
|
|
123
|
-
<p className="slx-subtitle">Request a fresh link below.</p>
|
|
124
|
-
<form onSubmit={resend}>
|
|
125
|
-
<div className="slx-field">
|
|
126
|
-
<label className="slx-label" htmlFor="slx-verify-email">
|
|
127
|
-
Email
|
|
128
|
-
</label>
|
|
129
|
-
<input
|
|
130
|
-
id="slx-verify-email"
|
|
131
|
-
className="slx-input"
|
|
132
|
-
type="email"
|
|
133
|
-
placeholder="you@example.com"
|
|
134
|
-
value={email}
|
|
135
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
136
|
-
required
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
<button className="slx-btn" type="submit" disabled={busy}>
|
|
140
|
-
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
141
|
-
{busy ? 'Sending…' : 'Resend verification email'}
|
|
142
|
-
</button>
|
|
143
|
-
</form>
|
|
144
|
-
</>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
{status === 'resend' && (
|
|
148
|
-
<>
|
|
149
|
-
<h1 className="slx-title">Verify your email</h1>
|
|
150
|
-
<p className="slx-subtitle">
|
|
151
|
-
Enter your email and we'll send a verification link.
|
|
152
|
-
</p>
|
|
153
|
-
{message && (
|
|
154
|
-
<p className="slx-hint" style={{ marginBottom: 12 }}>
|
|
155
|
-
{message}
|
|
156
|
-
</p>
|
|
157
|
-
)}
|
|
158
|
-
<form onSubmit={resend}>
|
|
159
|
-
<div className="slx-field">
|
|
160
|
-
<label className="slx-label" htmlFor="slx-verify-email2">
|
|
161
|
-
Email
|
|
162
|
-
</label>
|
|
163
|
-
<input
|
|
164
|
-
id="slx-verify-email2"
|
|
165
|
-
className="slx-input"
|
|
166
|
-
type="email"
|
|
167
|
-
placeholder="you@example.com"
|
|
168
|
-
value={email}
|
|
169
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
170
|
-
required
|
|
171
|
-
/>
|
|
172
|
-
</div>
|
|
173
|
-
<button className="slx-btn" type="submit" disabled={busy}>
|
|
174
|
-
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
175
|
-
{busy ? 'Sending…' : 'Send verification link'}
|
|
176
|
-
</button>
|
|
177
|
-
</form>
|
|
178
|
-
</>
|
|
179
|
-
)}
|
|
180
|
-
</div>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { SlyxupClient } from '@slyxup/core';
|
|
2
|
-
import { type FormEvent, useEffect, useState } from 'react';
|
|
3
|
-
import { CheckIcon, KeyholeMark } from '../../icons';
|
|
4
|
-
import { injectStyles } from '../../styles';
|
|
5
|
-
|
|
6
|
-
export interface ForgotPasswordProps {
|
|
7
|
-
apiUrl?: string;
|
|
8
|
-
onSuccess?: () => void;
|
|
9
|
-
onBackToSignIn?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Request a password-reset email. */
|
|
13
|
-
export function ForgotPassword({
|
|
14
|
-
apiUrl,
|
|
15
|
-
onSuccess,
|
|
16
|
-
onBackToSignIn,
|
|
17
|
-
}: ForgotPasswordProps) {
|
|
18
|
-
injectStyles();
|
|
19
|
-
const [email, setEmail] = useState('');
|
|
20
|
-
const [sent, setSent] = useState(false);
|
|
21
|
-
const [busy, setBusy] = useState(false);
|
|
22
|
-
const [error, setError] = useState<string | null>(null);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (error) {
|
|
26
|
-
const t = setTimeout(() => setError(null), 4000);
|
|
27
|
-
return () => clearTimeout(t);
|
|
28
|
-
}
|
|
29
|
-
}, [error]);
|
|
30
|
-
|
|
31
|
-
async function onSubmit(e: FormEvent) {
|
|
32
|
-
e.preventDefault();
|
|
33
|
-
setBusy(true);
|
|
34
|
-
setError(null);
|
|
35
|
-
try {
|
|
36
|
-
await fetch(
|
|
37
|
-
`${(apiUrl ?? process.env.NEXT_PUBLIC_SLYXUP_API_URL ?? 'https://auth.slyxup.online').replace(/\/$/, '')}/v1/verification/password/forgot`,
|
|
38
|
-
{
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
|
-
body: JSON.stringify({ email }),
|
|
42
|
-
}
|
|
43
|
-
);
|
|
44
|
-
setSent(true); // always success — never reveal account existence
|
|
45
|
-
onSuccess?.();
|
|
46
|
-
} catch {
|
|
47
|
-
setError('Network problem. Check your connection and try again.');
|
|
48
|
-
} finally {
|
|
49
|
-
setBusy(false);
|
|
50
|
-
}
|
|
51
|
-
void SlyxupClient; // tree-shake guard
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (sent) {
|
|
55
|
-
return (
|
|
56
|
-
<div className="slx-card">
|
|
57
|
-
<div className="slx-success-icon">
|
|
58
|
-
<CheckIcon />
|
|
59
|
-
</div>
|
|
60
|
-
<h1 className="slx-title" style={{ textAlign: 'center' }}>
|
|
61
|
-
Check your email
|
|
62
|
-
</h1>
|
|
63
|
-
<p className="slx-subtitle" style={{ textAlign: 'center' }}>
|
|
64
|
-
If an account exists for {email}, a reset link is on its way.
|
|
65
|
-
</p>
|
|
66
|
-
{onBackToSignIn && (
|
|
67
|
-
<p className="slx-footer">
|
|
68
|
-
<button type="button" className="slx-link" onClick={onBackToSignIn}>
|
|
69
|
-
Back to sign in
|
|
70
|
-
</button>
|
|
71
|
-
</p>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
79
|
-
<div className="slx-mark">
|
|
80
|
-
<KeyholeMark />
|
|
81
|
-
</div>
|
|
82
|
-
<h1 className="slx-title">Reset your password</h1>
|
|
83
|
-
<p className="slx-subtitle">
|
|
84
|
-
Enter your email and we'll send you a reset link.
|
|
85
|
-
</p>
|
|
86
|
-
|
|
87
|
-
{error && (
|
|
88
|
-
<p className="slx-error-text" role="alert">
|
|
89
|
-
{error}
|
|
90
|
-
</p>
|
|
91
|
-
)}
|
|
92
|
-
|
|
93
|
-
<form onSubmit={onSubmit}>
|
|
94
|
-
<div className="slx-field">
|
|
95
|
-
<label className="slx-label" htmlFor="slx-forgot-email">
|
|
96
|
-
Email
|
|
97
|
-
</label>
|
|
98
|
-
<input
|
|
99
|
-
id="slx-forgot-email"
|
|
100
|
-
className="slx-input"
|
|
101
|
-
type="email"
|
|
102
|
-
autoComplete="email"
|
|
103
|
-
placeholder="you@example.com"
|
|
104
|
-
value={email}
|
|
105
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
106
|
-
required
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
109
|
-
<button className="slx-btn" type="submit" disabled={busy}>
|
|
110
|
-
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
111
|
-
{busy ? 'Sending…' : 'Send reset link'}
|
|
112
|
-
</button>
|
|
113
|
-
</form>
|
|
114
|
-
|
|
115
|
-
{onBackToSignIn && (
|
|
116
|
-
<p className="slx-footer">
|
|
117
|
-
<button type="button" className="slx-link" onClick={onBackToSignIn}>
|
|
118
|
-
Back to sign in
|
|
119
|
-
</button>
|
|
120
|
-
</p>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { injectStyles } from '../../styles';
|
|
5
|
-
|
|
6
|
-
interface Plan {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
amount: number;
|
|
10
|
-
currency: string;
|
|
11
|
-
interval: 'month' | 'year';
|
|
12
|
-
trialDays: number | null;
|
|
13
|
-
features: string[] | null;
|
|
14
|
-
isPopular: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PricingTableProps {
|
|
18
|
-
plans: Plan[];
|
|
19
|
-
onSelect?: (plan: Plan) => void;
|
|
20
|
-
loading?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Clean pricing grid with popular badge */
|
|
24
|
-
export function PricingTable({ plans, onSelect, loading }: PricingTableProps) {
|
|
25
|
-
injectStyles();
|
|
26
|
-
if (loading) {
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
className="slx-pricing"
|
|
30
|
-
style={{
|
|
31
|
-
display: 'grid',
|
|
32
|
-
gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
|
|
33
|
-
gap: 16,
|
|
34
|
-
}}
|
|
35
|
-
>
|
|
36
|
-
{[1, 2, 3].map((i) => (
|
|
37
|
-
<div
|
|
38
|
-
key={i}
|
|
39
|
-
className="slx-card"
|
|
40
|
-
style={{ minHeight: 320 }}
|
|
41
|
-
aria-busy="true"
|
|
42
|
-
/>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div
|
|
50
|
-
style={{
|
|
51
|
-
display: 'grid',
|
|
52
|
-
gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
|
|
53
|
-
gap: 16,
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
{plans.map((plan) => (
|
|
57
|
-
<div
|
|
58
|
-
key={plan.id}
|
|
59
|
-
className={`slx-card${plan.isPopular ? ' slx-card-popular' : ''}`}
|
|
60
|
-
style={{
|
|
61
|
-
position: plan.isPopular ? 'relative' : undefined,
|
|
62
|
-
borderColor: plan.isPopular ? 'var(--slx-accent)' : undefined,
|
|
63
|
-
display: 'flex',
|
|
64
|
-
flexDirection: 'column',
|
|
65
|
-
}}
|
|
66
|
-
>
|
|
67
|
-
{plan.isPopular && (
|
|
68
|
-
<span
|
|
69
|
-
style={{
|
|
70
|
-
position: 'absolute',
|
|
71
|
-
top: -11,
|
|
72
|
-
right: 20,
|
|
73
|
-
fontSize: 10.5,
|
|
74
|
-
fontFamily: 'var(--slx-mono)',
|
|
75
|
-
background:
|
|
76
|
-
'linear-gradient(135deg, var(--slx-accent), #8b5cf6)',
|
|
77
|
-
color: '#fff',
|
|
78
|
-
padding: '3px 10px',
|
|
79
|
-
borderRadius: 999,
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
POPULAR
|
|
83
|
-
</span>
|
|
84
|
-
)}
|
|
85
|
-
<h3
|
|
86
|
-
style={{ fontSize: 15, fontWeight: 550, color: 'var(--slx-muted)' }}
|
|
87
|
-
>
|
|
88
|
-
{plan.name}
|
|
89
|
-
</h3>
|
|
90
|
-
<div style={{ margin: '8px 0 2px' }}>
|
|
91
|
-
<span
|
|
92
|
-
style={{
|
|
93
|
-
fontSize: 38,
|
|
94
|
-
fontWeight: 750,
|
|
95
|
-
letterSpacing: '-0.03em',
|
|
96
|
-
fontFamily: 'var(--slx-display)',
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
${(plan.amount / 100).toFixed(0)}
|
|
100
|
-
</span>
|
|
101
|
-
<span style={{ fontSize: 14, color: 'var(--slx-muted)' }}>
|
|
102
|
-
/{plan.interval}
|
|
103
|
-
</span>
|
|
104
|
-
</div>
|
|
105
|
-
{plan.trialDays ? (
|
|
106
|
-
<p
|
|
107
|
-
style={{
|
|
108
|
-
fontSize: 12,
|
|
109
|
-
color: 'var(--slx-accent)',
|
|
110
|
-
marginBottom: 4,
|
|
111
|
-
}}
|
|
112
|
-
>
|
|
113
|
-
{plan.trialDays} day free trial
|
|
114
|
-
</p>
|
|
115
|
-
) : (
|
|
116
|
-
<p style={{ fontSize: 12, color: 'transparent', marginBottom: 4 }}>
|
|
117
|
-
|
|
118
|
-
</p>
|
|
119
|
-
)}
|
|
120
|
-
<ul style={{ listStyle: 'none', margin: '14px 0 22px', flex: 1 }}>
|
|
121
|
-
{((plan.features ?? []) as string[]).map((f: string) => (
|
|
122
|
-
<li
|
|
123
|
-
key={f}
|
|
124
|
-
style={{
|
|
125
|
-
fontSize: 13.5,
|
|
126
|
-
color: 'var(--slx-ink)',
|
|
127
|
-
padding: '5px 0 5px 24px',
|
|
128
|
-
position: 'relative',
|
|
129
|
-
}}
|
|
130
|
-
>
|
|
131
|
-
<span
|
|
132
|
-
style={{
|
|
133
|
-
position: 'absolute',
|
|
134
|
-
left: 0,
|
|
135
|
-
color: '#34d399',
|
|
136
|
-
fontWeight: 700,
|
|
137
|
-
}}
|
|
138
|
-
>
|
|
139
|
-
✓
|
|
140
|
-
</span>{' '}
|
|
141
|
-
{f}
|
|
142
|
-
</li>
|
|
143
|
-
))}
|
|
144
|
-
</ul>
|
|
145
|
-
<button
|
|
146
|
-
type="button"
|
|
147
|
-
className="slx-btn"
|
|
148
|
-
onClick={() => onSelect?.(plan)}
|
|
149
|
-
disabled={loading}
|
|
150
|
-
>
|
|
151
|
-
Get started
|
|
152
|
-
</button>
|
|
153
|
-
</div>
|
|
154
|
-
))}
|
|
155
|
-
</div>
|
|
156
|
-
);
|
|
157
|
-
}
|