@slyxup/ui 0.2.14 → 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.
@@ -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&apos;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&apos;re all set. Redirecting…
111
- </p>
112
- </>
113
- )}
114
-
115
- {status === 'error' && (
116
- <>
117
- <h1 className="slx-title">Link didn&apos;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&apos;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&apos;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
- &nbsp;
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
- &#10003;
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
- }