defde2bca9
- Add AcceptInvitationParams and AcceptInvitationRequest schemas - Add AddDeployKeyParams, AddDomainParams, AddGpgKeyParams, AddMemberParams, AddReplyParams, AddRepoMemberParams, and AddSshKeyParams schemas - Add ApiEmptyResponse and ApiErrorResponse schemas - Add ApiResponse schemas for BranchMergeCheck, BranchProtectionRule, CaptchaResponse, ContextMe, CreateInvitationResponse, EmailResponse, Enable2FAResponse, Get2FAStatusResponse - Add ApiResponse schemas for Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone, IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, and IssueTemplate - Add ApiResponse schemas for Option_BranchProtectionRule, PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, and PrLabel
107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
import { createSignal, Show } from 'solid-js';
|
|
import { useNavigate, useLocation } from '@solidjs/router';
|
|
import AuthLayout from '@/app/auth/components/AuthLayout';
|
|
import { SubmitButton, ErrorMessage } from '@/app/auth/components/FormElements';
|
|
import { AuthService } from '@/client';
|
|
import { ApiError } from '@/client/core/ApiError';
|
|
|
|
function ArrowLeft(props: { size: number }) {
|
|
return (
|
|
<svg width={props.size} height={props.size} viewBox="0 0 256 256" fill="currentColor">
|
|
<path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function Envelope(props: { size: number; color: string }) {
|
|
return (
|
|
<svg width={props.size} height={props.size} viewBox="0 0 256 256" fill={props.color}>
|
|
<path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48Zm-8,27.31V192H40V75.19l84.42,73.87a8,8,0,0,0,10.52.05ZM40,64l88,77,88-77v0H40Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export default function ForgotPassword() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const [email, setEmail] = createSignal('');
|
|
const [error, setError] = createSignal('');
|
|
const [loading, setLoading] = createSignal(false);
|
|
const isSent = () => location.hash === '#sent';
|
|
|
|
function setStepSent() { navigate('/forgot-password#sent', { replace: true }); }
|
|
function setStepForm() { navigate('/forgot-password', { replace: true }); }
|
|
|
|
async function handleSubmit(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
if (loading()) return;
|
|
setError('');
|
|
if (!email().trim()) { setError('Please enter your email'); return; }
|
|
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRe.test(email().trim())) { setError('Invalid email format'); return; }
|
|
|
|
setLoading(true);
|
|
try {
|
|
await AuthService.authRequestPasswordReset({ requestBody: { email: email().trim() } });
|
|
setStepSent();
|
|
} catch (err) {
|
|
if (err instanceof ApiError && err.status === 429) {
|
|
setError('Too many requests, please try again later');
|
|
} else {
|
|
setError('Unable to request password reset, please try again');
|
|
}
|
|
} finally { setLoading(false); }
|
|
}
|
|
|
|
return (
|
|
<Show
|
|
when={!isSent()}
|
|
fallback={
|
|
<AuthLayout eyebrow="Reset Password">
|
|
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', gap: '16px', 'text-align': 'center' }}>
|
|
<div class="status-icon status-icon--accent">
|
|
<Envelope size={32} color="var(--accent)" />
|
|
</div>
|
|
<h1 class="form-header">Email Sent</h1>
|
|
<p style={{ 'font-family': 'var(--font-body)', 'font-size': '15px', color: 'var(--fg)', 'line-height': '1.5', margin: '0' }}>
|
|
If {email() || 'the email address'} is registered, you will receive a password reset email.
|
|
</p>
|
|
<p style={{ 'font-family': 'var(--font-body)', 'font-size': '13px', color: 'var(--meta)', 'line-height': '1.45', margin: '0' }}>
|
|
Please check your inbox and spam folder. The link is valid for 1 hour.
|
|
</p>
|
|
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px', width: '100%', 'margin-top': '4px' }}>
|
|
<a href="/login" style={{ 'text-decoration': 'none' }}>
|
|
<button type="button" class="btn-primary btn-block">Back to Sign In</button>
|
|
</a>
|
|
<button type="button" class="btn-secondary btn-block"
|
|
onClick={() => { setStepForm(); setError(''); }}>Change email</button>
|
|
</div>
|
|
</div>
|
|
</AuthLayout>
|
|
}
|
|
>
|
|
<AuthLayout eyebrow="Reset Password">
|
|
<form onSubmit={handleSubmit} class="form-stack" noValidate>
|
|
<h1 class="form-header">Forgot Password</h1>
|
|
<p class="form-desc">Enter your registered email to receive a password reset link.</p>
|
|
|
|
{error() && <ErrorMessage>{error()}</ErrorMessage>}
|
|
|
|
<div class="field">
|
|
<label for="email-input">Email</label>
|
|
<input id="email-input" type="email" autocomplete="email" placeholder="your@email.com"
|
|
value={email()} onInput={(e) => setEmail(e.currentTarget.value)} autofocus />
|
|
</div>
|
|
|
|
<SubmitButton loading={loading()} loadingText="Sending…">Send Reset Link</SubmitButton>
|
|
|
|
<div style={{ display: 'flex', 'justify-content': 'center' }}>
|
|
<a href="/login" class="back-link"><ArrowLeft size={14} /> Back to Sign In</a>
|
|
</div>
|
|
</form>
|
|
</AuthLayout>
|
|
</Show>
|
|
);
|
|
}
|