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:
2026-05-08 20:46:58 +01:00
parent 1a0b4dc25d
commit d024bf7fa3
32 changed files with 7480 additions and 2740 deletions
File diff suppressed because it is too large Load Diff
+58 -36
View File
@@ -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&apos;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;
+81 -76
View File
@@ -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&apos;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>
);
};
+16 -6
View File
@@ -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
View File
@@ -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>
);
};
+102 -57
View File
@@ -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 cant 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&apos;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;