forked from jamesp/sasa-membership
stuff changed:
- ui has been made 'kinda better' (after making it worse for a while lol - ESP rfid readers are now supported [ill upload the code for them in another repo later] - admin system has been secured a bit better and seems to be working well
This commit is contained in:
+1641
-890
File diff suppressed because it is too large
Load Diff
@@ -26,48 +26,70 @@ const ForgotPassword: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card">
|
||||
<h2>Forgot Password</h2>
|
||||
<p style={{ textAlign: 'center', marginBottom: '24px', color: '#666' }}>
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
<div className="auth-shell">
|
||||
<header className="auth-topbar">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-mark">S</div>
|
||||
<div className="portal-brand-text">
|
||||
<h1>SASA Member Portal</h1>
|
||||
<div className="portal-subtitle">Account recovery</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
{message && <div className="alert alert-success">{message}</div>}
|
||||
<main className="auth-container">
|
||||
<section className="auth-welcome-card">
|
||||
<div className="auth-kicker">Password Help</div>
|
||||
<h2>Recover access quickly</h2>
|
||||
<p>
|
||||
Enter the email address tied to your account and we'll send a secure password reset link if that account exists.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your email address"
|
||||
/>
|
||||
<section className="auth-card">
|
||||
<div className="auth-card-head">
|
||||
<h2>Forgot Password</h2>
|
||||
<span>Email reset link</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', marginTop: '16px' }}
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="auth-card-body">
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
{message && <div className="alert alert-success">{message}</div>}
|
||||
|
||||
<div className="form-footer">
|
||||
<Link to="/login" style={{ color: '#0066cc', textDecoration: 'none' }}>
|
||||
Back to login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary auth-submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="auth-footer">
|
||||
<div>
|
||||
<Link to="/login">Back to login</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
export default ForgotPassword;
|
||||
|
||||
@@ -43,84 +43,89 @@ const Login: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-container" style={{ gap: '40px', padding: '20px' }}>
|
||||
<div className="welcome-section" style={{
|
||||
flex: '1',
|
||||
maxWidth: '400px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
padding: '30px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<h1 style={{ color: '#333', marginBottom: '16px', fontSize: '2.2rem' }}>Welcome to SASA</h1>
|
||||
<p style={{ fontSize: '1.1rem', color: '#666', lineHeight: '1.6', marginBottom: '20px' }}>
|
||||
REPLACE WITH BOB WORDS: Swansea Airport Stakeholder's Association (SASA) is a community interest company run by volunteers, which holds the lease of Swansea Airport.
|
||||
</p>
|
||||
<p style={{ fontSize: '1rem', color: '#555', lineHeight: '1.5' }}>
|
||||
Join our community of aviation enthusiasts and support the future of Swansea Airport.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="auth-card" style={{ flex: '1', maxWidth: '400px' }}>
|
||||
<h2>SASA Member Portal</h2>
|
||||
<p style={{ textAlign: 'center', marginBottom: '24px', color: '#666' }}>
|
||||
Log in to your membership account
|
||||
</p>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<div className="auth-shell">
|
||||
<header className="auth-topbar">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-mark">S</div>
|
||||
<div className="portal-brand-text">
|
||||
<h1>SASA Member Portal</h1>
|
||||
<div className="portal-subtitle">Member access and admin control room</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', marginTop: '16px' }}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Log In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="form-footer">
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Link to="/forgot-password" style={{ color: '#0066cc', textDecoration: 'none' }}>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => navigate('/register')}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
Join SASA
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="auth-container">
|
||||
<section className="auth-welcome-card">
|
||||
<div className="auth-kicker">Community Access</div>
|
||||
<h2>Welcome to SASA</h2>
|
||||
<p>
|
||||
Swansea Airport Stakeholder's Association manages member access, events, and operations from one shared platform.
|
||||
</p>
|
||||
<div className="auth-feature-list">
|
||||
<div className="auth-feature-item">Manage your membership, payments, and events in one place</div>
|
||||
<div className="auth-feature-item">Keep profile and contact details current without admin help</div>
|
||||
<div className="auth-feature-item">Admin users can switch into a separate operations workspace after login</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="auth-card">
|
||||
<div className="auth-card-head">
|
||||
<h2>Sign In</h2>
|
||||
<span>Secure session</span>
|
||||
</div>
|
||||
|
||||
<div className="auth-card-body">
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary auth-submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Signing In...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="form-footer auth-footer">
|
||||
<div>
|
||||
<Link to="/forgot-password">Forgot your password?</Link>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary auth-submit"
|
||||
onClick={() => navigate('/register')}
|
||||
>
|
||||
Join SASA
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useConfirm } from '../contexts/ConfirmContext';
|
||||
|
||||
const MembershipTiers: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
const navigate = useNavigate();
|
||||
const [tiers, setTiers] = useState<MembershipTier[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -20,7 +24,7 @@ const MembershipTiers: React.FC = () => {
|
||||
setTiers(tierData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tiers:', error);
|
||||
alert('Failed to load membership tiers');
|
||||
toast.error('Failed to load membership tiers.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -32,7 +36,7 @@ const MembershipTiers: React.FC = () => {
|
||||
setShowCreateForm(false);
|
||||
loadTiers();
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.detail || 'Failed to create tier');
|
||||
toast.error(error.response?.data?.detail || 'Failed to create tier.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,12 +46,18 @@ const MembershipTiers: React.FC = () => {
|
||||
setEditingTier(null);
|
||||
loadTiers();
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.detail || 'Failed to update tier');
|
||||
toast.error(error.response?.data?.detail || 'Failed to update tier.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTier = async (tierId: number) => {
|
||||
if (!confirm('Are you sure you want to delete this membership tier? This action cannot be undone.')) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete membership tier',
|
||||
message: 'Are you sure you want to delete this membership tier? This action cannot be undone.',
|
||||
confirmLabel: 'Delete',
|
||||
tone: 'danger'
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +65,7 @@ const MembershipTiers: React.FC = () => {
|
||||
await membershipService.deleteTier(tierId);
|
||||
loadTiers();
|
||||
} catch (error: any) {
|
||||
alert(error.response?.data?.detail || 'Failed to delete tier');
|
||||
toast.error(error.response?.data?.detail || 'Failed to delete tier.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -393,4 +403,4 @@ const MembershipTierForm: React.FC<MembershipTierFormProps> = ({ tier, onSave, o
|
||||
);
|
||||
};
|
||||
|
||||
export default MembershipTiers;
|
||||
export default MembershipTiers;
|
||||
|
||||
+157
-135
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { authService, RegisterData } from '../services/membershipService';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
@@ -67,142 +67,164 @@ const Register: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card">
|
||||
<h2>Create Your Account</h2>
|
||||
<p style={{ textAlign: 'center', marginBottom: '24px', color: '#666' }}>
|
||||
Join Swansea Airport Stakeholders Alliance
|
||||
</p>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '40px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
{/* Left Column - Personal Information */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="first_name">First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="last_name">Last Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address *</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handlePasswordChange}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
<small style={{ color: '#666', fontSize: '12px' }}>
|
||||
Minimum 8 characters
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">Confirm Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
minLength={8}
|
||||
required
|
||||
style={{
|
||||
borderColor: confirmPassword && !passwordsMatch ? '#dc3545' : confirmPassword && passwordsMatch ? '#28a745' : undefined
|
||||
}}
|
||||
/>
|
||||
{confirmPassword && (
|
||||
<small style={{
|
||||
color: passwordsMatch ? '#28a745' : '#dc3545',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{passwordsMatch ? '✓ Passwords match' : '✗ Passwords do not match'}
|
||||
</small>
|
||||
)}
|
||||
{!confirmPassword && (
|
||||
<small style={{ color: '#666', fontSize: '12px' }}>
|
||||
Re-enter your password
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="auth-shell">
|
||||
<header className="auth-topbar">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-mark">S</div>
|
||||
<div className="portal-brand-text">
|
||||
<h1>SASA Member Portal</h1>
|
||||
<div className="portal-subtitle">Membership registration and profile setup</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Contact Information */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="phone">Phone (optional)</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="address">Address (optional)</label>
|
||||
<textarea
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button - Full Width */}
|
||||
<div style={{ gridColumn: '1 / -1', marginTop: '8px' }}>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account & Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="form-footer">
|
||||
Already have an account? <a href="/login">Log in</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="auth-container auth-container-wide">
|
||||
<section className="auth-welcome-card">
|
||||
<div className="auth-kicker">New Membership</div>
|
||||
<h2>Join the SASA community</h2>
|
||||
<p>
|
||||
Create your account to manage your membership, respond to events, and keep your contact details up to date.
|
||||
</p>
|
||||
<div className="auth-feature-list">
|
||||
<div className="auth-feature-item">Straightforward onboarding with automatic sign-in</div>
|
||||
<div className="auth-feature-item">Membership tiers, payments, and event RSVPs in one place</div>
|
||||
<div className="auth-feature-item">A separate admin workspace for staff users after login</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="auth-card auth-card-wide">
|
||||
<div className="auth-card-head">
|
||||
<h2>Create Account</h2>
|
||||
<span>Step 1 of 1</span>
|
||||
</div>
|
||||
|
||||
<div className="auth-card-body">
|
||||
<p className="auth-card-copy">
|
||||
Complete the essentials below. You can add or update the rest of your profile later from your dashboard.
|
||||
</p>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form-grid">
|
||||
<div className="form-group">
|
||||
<label htmlFor="first_name">First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
autoComplete="given-name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="last_name">Last Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
autoComplete="family-name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address *</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="phone">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
autoComplete="tel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handlePasswordChange}
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
<small className="form-hint">Minimum 8 characters.</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">Confirm Password *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
className={confirmPassword ? (passwordsMatch ? 'field-success' : 'field-error') : ''}
|
||||
required
|
||||
/>
|
||||
{confirmPassword ? (
|
||||
<small className={passwordsMatch ? 'form-hint hint-success' : 'form-hint hint-error'}>
|
||||
{passwordsMatch ? 'Passwords match.' : 'Passwords do not match.'}
|
||||
</small>
|
||||
) : (
|
||||
<small className="form-hint">Re-enter your password to confirm it.</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group form-group-full">
|
||||
<label htmlFor="address">Address</label>
|
||||
<textarea
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
rows={4}
|
||||
autoComplete="street-address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-full">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary auth-submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account & Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="auth-footer">
|
||||
<div>
|
||||
Already have an account? <Link to="/login">Log in</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,74 +55,119 @@ const ResetPassword: React.FC = () => {
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card">
|
||||
<h2>Invalid Reset Link</h2>
|
||||
<p style={{ textAlign: 'center', marginBottom: '24px', color: '#666' }}>
|
||||
This password reset link is invalid or has expired. Please request a new password reset.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/forgot-password')}
|
||||
className="btn btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
Request New Reset Link
|
||||
</button>
|
||||
</div>
|
||||
<div className="auth-shell">
|
||||
<header className="auth-topbar">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-mark">S</div>
|
||||
<div className="portal-brand-text">
|
||||
<h1>SASA Member Portal</h1>
|
||||
<div className="portal-subtitle">Account recovery</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="auth-container">
|
||||
<section className="auth-welcome-card">
|
||||
<div className="auth-kicker">Link Expired</div>
|
||||
<h2>This reset link can’t be used</h2>
|
||||
<p>
|
||||
The link is missing or no longer valid. Request a fresh reset email and try again from the newest message.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="auth-card">
|
||||
<div className="auth-card-head">
|
||||
<h2>Invalid Reset Link</h2>
|
||||
<span>Request a new one</span>
|
||||
</div>
|
||||
|
||||
<div className="auth-card-body">
|
||||
<button
|
||||
onClick={() => navigate('/forgot-password')}
|
||||
className="btn btn-primary auth-submit"
|
||||
>
|
||||
Request New Reset Link
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card">
|
||||
<h2>Reset Password</h2>
|
||||
<p style={{ textAlign: 'center', marginBottom: '24px', color: '#666' }}>
|
||||
Enter your new password below. Make sure it's at least 8 characters long.
|
||||
</p>
|
||||
<div className="auth-shell">
|
||||
<header className="auth-topbar">
|
||||
<div className="portal-brand">
|
||||
<div className="portal-mark">S</div>
|
||||
<div className="portal-brand-text">
|
||||
<h1>SASA Member Portal</h1>
|
||||
<div className="portal-subtitle">Choose a new password</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
{message && <div className="alert alert-success">{message}</div>}
|
||||
<main className="auth-container">
|
||||
<section className="auth-welcome-card">
|
||||
<div className="auth-kicker">Secure Reset</div>
|
||||
<h2>Set a fresh password</h2>
|
||||
<p>
|
||||
Use a password with at least 8 characters. After a successful reset, you'll be returned to the login screen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="newPassword">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
<section className="auth-card">
|
||||
<div className="auth-card-head">
|
||||
<h2>Reset Password</h2>
|
||||
<span>Secure update</span>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
<div className="auth-card-body">
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
{message && <div className="alert alert-success">{message}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{ width: '100%', marginTop: '16px' }}
|
||||
>
|
||||
{loading ? 'Resetting...' : 'Reset Password'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="newPassword">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary auth-submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Resetting...' : 'Reset Password'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
export default ResetPassword;
|
||||
|
||||
Reference in New Issue
Block a user