741 lines
24 KiB
TypeScript
741 lines
24 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonButtons,
|
|
IonContent,
|
|
IonHeader,
|
|
IonIcon,
|
|
IonInput,
|
|
IonList,
|
|
IonModal,
|
|
IonPage,
|
|
IonTitle,
|
|
IonToolbar,
|
|
} from '@ionic/react';
|
|
import {
|
|
cameraOutline,
|
|
chevronBackOutline,
|
|
closeOutline,
|
|
} from 'ionicons/icons';
|
|
import { useHistory, useParams } from 'react-router-dom';
|
|
import { supabase } from '../supabase';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { buildCacheKey, removeCache } from '../utils/localCache';
|
|
import AvatarPicker from '../components/AvatarPicker';
|
|
import momImage from '../assets/mom.jpg';
|
|
import dadImage from '../assets/dad.jpg';
|
|
import '../styles/recipients.css';
|
|
|
|
type Params = { id?: string };
|
|
|
|
type RecipientRow = {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
relationship: string;
|
|
country: string;
|
|
city: string;
|
|
mobile_number: string;
|
|
photo_path: string | null;
|
|
};
|
|
|
|
const countryOptions = [
|
|
{ name: 'Zimbabwe', flag: '🇿🇼', dialCode: '+263' },
|
|
{ name: 'South Africa', flag: '🇿🇦', dialCode: '+27' },
|
|
{ name: 'Zambia', flag: '🇿🇲', dialCode: '+260' },
|
|
{ name: 'Botswana', flag: '🇧🇼', dialCode: '+267' },
|
|
{ name: 'Mozambique', flag: '🇲🇿', dialCode: '+258' },
|
|
{ name: 'United Kingdom', flag: '🇬🇧', dialCode: '+44' },
|
|
{ name: 'United States', flag: '🇺🇸', dialCode: '+1' },
|
|
{ name: 'Canada', flag: '🇨🇦', dialCode: '+1' },
|
|
{ name: 'Australia', flag: '🇦🇺', dialCode: '+61' },
|
|
];
|
|
|
|
const locationOptionsByCountry: Record<
|
|
string,
|
|
Array<{
|
|
group: string;
|
|
options: Array<{ name: string; description: string }>;
|
|
}>
|
|
> = {
|
|
Zimbabwe: [
|
|
{
|
|
group: 'Major cities',
|
|
options: [
|
|
{ name: 'Harare', description: 'Capital city coverage' },
|
|
{ name: 'Bulawayo', description: 'City merchants and pharmacies' },
|
|
{ name: 'Mutare', description: 'Eastern Highlands coverage' },
|
|
{ name: 'Gweru', description: 'Midlands city coverage' },
|
|
],
|
|
},
|
|
{
|
|
group: 'Towns',
|
|
options: [
|
|
{ name: 'Chitungwiza', description: 'Harare metro support' },
|
|
{ name: 'Masvingo', description: 'Town and surrounding areas' },
|
|
{ name: 'Kwekwe', description: 'Supported collection points' },
|
|
{ name: 'Kadoma', description: 'Supported collection points' },
|
|
{ name: 'Victoria Falls', description: 'Town coverage' },
|
|
],
|
|
},
|
|
{
|
|
group: 'Rural districts',
|
|
options: [
|
|
{ name: 'Murehwa District', description: 'Rural collection support' },
|
|
{ name: 'Gokwe District', description: 'Rural collection support' },
|
|
{ name: 'Buhera District', description: 'Rural collection support' },
|
|
{ name: 'Zaka District', description: 'Rural collection support' },
|
|
{ name: 'Guruve District', description: 'Rural collection support' },
|
|
{
|
|
name: 'Tsholotsho District',
|
|
description: 'Rural collection support',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
Zambia: [
|
|
{
|
|
group: 'Major cities',
|
|
options: [
|
|
{ name: 'Lusaka', description: 'Capital city coverage' },
|
|
{ name: 'Ndola', description: 'Copperbelt coverage' },
|
|
{ name: 'Kitwe', description: 'Copperbelt coverage' },
|
|
{ name: 'Livingstone', description: 'Town coverage' },
|
|
],
|
|
},
|
|
{
|
|
group: 'Rural districts',
|
|
options: [
|
|
{ name: 'Chongwe District', description: 'Rural collection support' },
|
|
{ name: 'Monze District', description: 'Rural collection support' },
|
|
],
|
|
},
|
|
],
|
|
Botswana: [
|
|
{
|
|
group: 'Cities and towns',
|
|
options: [
|
|
{ name: 'Gaborone', description: 'Capital city coverage' },
|
|
{ name: 'Francistown', description: 'Town coverage' },
|
|
{ name: 'Maun', description: 'Town coverage' },
|
|
],
|
|
},
|
|
{
|
|
group: 'Rural districts',
|
|
options: [
|
|
{ name: 'Kweneng District', description: 'Rural collection support' },
|
|
{ name: 'Central District', description: 'Rural collection support' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const fallbackLocationGroups = [
|
|
{
|
|
group: 'Supported areas',
|
|
options: [
|
|
{ name: 'Main city', description: 'Available merchant coverage' },
|
|
{ name: 'Nearby town', description: 'Supported collection points' },
|
|
{ name: 'Rural district', description: 'Rural collection support' },
|
|
],
|
|
},
|
|
];
|
|
|
|
const getCountryMeta = (countryName: string) =>
|
|
countryOptions.find((option) => option.name === countryName) ??
|
|
countryOptions[0];
|
|
|
|
const getLocationGroups = (countryName: string) =>
|
|
locationOptionsByCountry[countryName] ?? fallbackLocationGroups;
|
|
|
|
const getSeededRecipientImage = (
|
|
firstName?: string | null,
|
|
photoPath?: string | null
|
|
) => {
|
|
const normalizedPath = photoPath?.trim().toLowerCase();
|
|
if (normalizedPath === 'mom.jpg' || normalizedPath === 'mum.jpg') {
|
|
return momImage;
|
|
}
|
|
if (normalizedPath === 'dad.jpg' || normalizedPath === 'father.jpg') {
|
|
return dadImage;
|
|
}
|
|
|
|
const normalizedName = firstName?.trim().toLowerCase();
|
|
if (normalizedName === 'mum' || normalizedName === 'mom') {
|
|
return momImage;
|
|
}
|
|
if (normalizedName === 'dad' || normalizedName === 'father') {
|
|
return dadImage;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const stripDialCode = (value: string, dialCode: string) => {
|
|
const normalizedValue = value.trim();
|
|
if (normalizedValue.startsWith(dialCode)) {
|
|
return normalizedValue.slice(dialCode.length).trimStart();
|
|
}
|
|
return normalizedValue;
|
|
};
|
|
|
|
const sanitizePhoneInput = (value: string) =>
|
|
value.replace(/[^\d\s()-]/g, '').replace(/\s{2,}/g, ' ');
|
|
|
|
const normalizePhoneInput = (value: string) =>
|
|
sanitizePhoneInput(value).replace(/^0+/, '');
|
|
|
|
const formatNameInput = (value: string) =>
|
|
value
|
|
.toLowerCase()
|
|
.replace(
|
|
/(^|[\s'-])([a-z])/g,
|
|
(_match, separator: string, letter: string) =>
|
|
`${separator}${letter.toUpperCase()}`
|
|
);
|
|
|
|
const RecipientFormPage: React.FC = () => {
|
|
const history = useHistory();
|
|
const { id } = useParams<Params>();
|
|
const { user } = useAuth();
|
|
const isEdit = Boolean(id);
|
|
|
|
const [firstName, setFirstName] = useState('');
|
|
const [lastName, setLastName] = useState('');
|
|
const [relationship, setRelationship] = useState('');
|
|
const [country, setCountry] = useState('Zimbabwe');
|
|
const [city, setCity] = useState('');
|
|
const [mobileNumber, setMobileNumber] = useState('');
|
|
const [showCountrySheet, setShowCountrySheet] = useState(false);
|
|
const [showLocationSheet, setShowLocationSheet] = useState(false);
|
|
const selectedCountry = getCountryMeta(country);
|
|
const locationGroups = getLocationGroups(country);
|
|
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
|
const previewObjectUrlRef = useRef<string | null>(null);
|
|
const [existingPhotoPath, setExistingPhotoPath] = useState<string | null>(
|
|
null
|
|
);
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const firstNameInputRef = useRef<HTMLIonInputElement>(null);
|
|
const lastNameInputRef = useRef<HTMLIonInputElement>(null);
|
|
const relationshipInputRef = useRef<HTMLIonInputElement>(null);
|
|
const mobileInputRef = useRef<HTMLIonInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isEdit) {
|
|
void loadRecipient();
|
|
}
|
|
}, [id, isEdit]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (previewObjectUrlRef.current) {
|
|
URL.revokeObjectURL(previewObjectUrlRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleGoBack = () => {
|
|
if (history.length > 1) {
|
|
history.goBack();
|
|
} else {
|
|
history.replace('/recipients');
|
|
}
|
|
};
|
|
|
|
const showError = (message: string) => {
|
|
setError(message);
|
|
setTimeout(() => setError(null), 4000);
|
|
};
|
|
|
|
const applyRecipientToForm = async (recipient: RecipientRow) => {
|
|
setFirstName(recipient.first_name);
|
|
setLastName(recipient.last_name);
|
|
setRelationship(recipient.relationship);
|
|
setCountry(recipient.country);
|
|
setCity(recipient.city);
|
|
const recipientCountry = getCountryMeta(recipient.country);
|
|
setMobileNumber(
|
|
stripDialCode(recipient.mobile_number, recipientCountry.dialCode)
|
|
);
|
|
setExistingPhotoPath(recipient.photo_path);
|
|
|
|
const seededImage = getSeededRecipientImage(
|
|
recipient.first_name,
|
|
recipient.photo_path
|
|
);
|
|
if (seededImage) {
|
|
setPhotoPreview(seededImage);
|
|
return;
|
|
}
|
|
|
|
if (!recipient.photo_path) {
|
|
setPhotoPreview(null);
|
|
return;
|
|
}
|
|
|
|
const { data: signed } = await supabase.storage
|
|
.from('recipient-photos')
|
|
.createSignedUrl(recipient.photo_path, 3600);
|
|
setPhotoPreview(
|
|
signed?.signedUrl
|
|
? `${signed.signedUrl}&v=${encodeURIComponent(recipient.photo_path)}`
|
|
: null
|
|
);
|
|
};
|
|
|
|
const loadRecipient = async () => {
|
|
if (!id) return;
|
|
setLoading(true);
|
|
|
|
const ownerId = user?.id ?? '00000000-0000-0000-0000-000000000000';
|
|
|
|
const { data, error: loadError } = await supabase
|
|
.from('recipients')
|
|
.select(
|
|
'id,first_name,last_name,relationship,country,city,mobile_number,photo_path'
|
|
)
|
|
.eq('id', id)
|
|
.eq('user_id', ownerId)
|
|
.single();
|
|
|
|
if (loadError || !data) {
|
|
showError(loadError?.message ?? 'Recipient not found');
|
|
setLoading(false);
|
|
history.replace('/recipients');
|
|
return;
|
|
}
|
|
|
|
const recipient = data as RecipientRow;
|
|
await applyRecipientToForm(recipient);
|
|
setLoading(false);
|
|
};
|
|
|
|
const handlePhotoChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
if (previewObjectUrlRef.current) {
|
|
URL.revokeObjectURL(previewObjectUrlRef.current);
|
|
}
|
|
|
|
const objectUrl = URL.createObjectURL(file);
|
|
previewObjectUrlRef.current = objectUrl;
|
|
setPhotoFile(file);
|
|
setPhotoPreview(objectUrl);
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (!user) {
|
|
showError('Please sign in again to save this loved one');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
first_name: firstName.trim(),
|
|
last_name: lastName.trim(),
|
|
relationship: relationship.trim(),
|
|
country: country.trim(),
|
|
city: city.trim(),
|
|
mobile_number:
|
|
`${selectedCountry.dialCode} ${mobileNumber.trim()}`.trim(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
|
|
if (
|
|
!payload.first_name ||
|
|
!payload.last_name ||
|
|
!payload.relationship ||
|
|
!payload.country ||
|
|
!payload.city ||
|
|
!payload.mobile_number
|
|
) {
|
|
showError('Please complete all required fields');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
setError(null);
|
|
|
|
let photoPath = existingPhotoPath;
|
|
if (photoFile) {
|
|
const extension = photoFile.name.split('.').pop() || 'jpg';
|
|
photoPath = `${user.id}/${Date.now()}.${extension}`;
|
|
const { error: uploadError } = await supabase.storage
|
|
.from('recipient-photos')
|
|
.upload(photoPath, photoFile);
|
|
|
|
if (uploadError) {
|
|
showError(uploadError.message);
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const recipientsCacheKey = buildCacheKey(user.id, 'recipientsList');
|
|
|
|
if (isEdit && id) {
|
|
const { error: updateError } = await supabase
|
|
.from('recipients')
|
|
.update({ ...payload, photo_path: photoPath })
|
|
.eq('id', id)
|
|
.eq('user_id', user.id);
|
|
|
|
if (updateError) {
|
|
showError(updateError.message);
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
|
|
await Promise.all([
|
|
removeCache(recipientsCacheKey),
|
|
removeCache(buildCacheKey(user.id, `recipient_${id}`)),
|
|
]);
|
|
setSaving(false);
|
|
history.push(`/recipients/${id}`);
|
|
return;
|
|
}
|
|
|
|
const { data, error: insertError } = await supabase
|
|
.from('recipients')
|
|
.insert({
|
|
user_id: user.id,
|
|
...payload,
|
|
photo_path: photoPath,
|
|
})
|
|
.select('id')
|
|
.single();
|
|
|
|
if (insertError || !data) {
|
|
showError(insertError?.message ?? 'Could not create recipient');
|
|
setSaving(false);
|
|
return;
|
|
}
|
|
|
|
await removeCache(recipientsCacheKey);
|
|
setSaving(false);
|
|
history.replace(`/recipients/${data.id}`);
|
|
};
|
|
|
|
const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
|
|
|
return (
|
|
<IonPage style={{ backgroundColor: '#fafafa' }}>
|
|
<IonHeader className="ion-no-border">
|
|
<IonToolbar
|
|
style={
|
|
{
|
|
'--background': '#fafafa',
|
|
'--border-width': '0px',
|
|
'--color': '#111827',
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<IonButtons slot="start">
|
|
<IonButton
|
|
className="recipients-back-button"
|
|
fill="clear"
|
|
onClick={handleGoBack}
|
|
aria-label="Go back"
|
|
>
|
|
<IonIcon icon={chevronBackOutline} slot="icon-only" />
|
|
</IonButton>
|
|
</IonButtons>
|
|
<IonTitle style={{ fontSize: '18px', fontWeight: 700 }}>
|
|
{isEdit ? 'Edit loved one' : 'Add loved one'}
|
|
</IonTitle>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
|
|
<IonContent
|
|
style={
|
|
{
|
|
'--background': '#fafafa',
|
|
'--padding-start': '0px',
|
|
'--padding-end': '0px',
|
|
'--padding-top': '8px',
|
|
'--padding-bottom': 'calc(32px + var(--ion-safe-area-bottom, 0px))',
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="recipient-form-card">
|
|
<div className="rf-photo-picker">
|
|
<IonIcon icon={cameraOutline} style={{ display: 'none' }} />
|
|
<AvatarPicker
|
|
previewUrl={photoPreview}
|
|
onFileChange={handlePhotoChange}
|
|
initials={initials || undefined}
|
|
disabled={loading || saving}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="rf-input-shell rf-text-shell"
|
|
onClick={() => void firstNameInputRef.current?.setFocus()}
|
|
disabled={loading || saving}
|
|
aria-label="Enter first name"
|
|
>
|
|
<span className="rf-field-label">First name</span>
|
|
<IonInput
|
|
ref={firstNameInputRef}
|
|
className="rf-field"
|
|
type="text"
|
|
placeholder="Sarah"
|
|
value={firstName}
|
|
disabled={loading || saving}
|
|
onIonInput={(event) =>
|
|
setFirstName(formatNameInput(event.detail.value ?? ''))
|
|
}
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rf-input-shell rf-text-shell"
|
|
onClick={() => void lastNameInputRef.current?.setFocus()}
|
|
disabled={loading || saving}
|
|
aria-label="Enter last name"
|
|
>
|
|
<span className="rf-field-label">Last name</span>
|
|
<IonInput
|
|
ref={lastNameInputRef}
|
|
className="rf-field"
|
|
type="text"
|
|
placeholder="Moyo"
|
|
value={lastName}
|
|
disabled={loading || saving}
|
|
onIonInput={(event) =>
|
|
setLastName(formatNameInput(event.detail.value ?? ''))
|
|
}
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rf-input-shell rf-text-shell"
|
|
onClick={() => void relationshipInputRef.current?.setFocus()}
|
|
disabled={loading || saving}
|
|
aria-label="Enter relationship"
|
|
>
|
|
<span className="rf-field-label">Relationship</span>
|
|
<IonInput
|
|
ref={relationshipInputRef}
|
|
className="rf-field"
|
|
type="text"
|
|
placeholder="Mum, Brother, Aunt"
|
|
value={relationship}
|
|
disabled={loading || saving}
|
|
onIonInput={(event) =>
|
|
setRelationship(formatNameInput(event.detail.value ?? ''))
|
|
}
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rf-country-trigger"
|
|
onClick={() => setShowCountrySheet(true)}
|
|
disabled={loading || saving}
|
|
aria-label="Select country"
|
|
>
|
|
<span className="rf-country-label">Country</span>
|
|
<span className="rf-country-value-row">
|
|
<span className="rf-country-value">
|
|
{`${selectedCountry.flag} ${selectedCountry.name} (${selectedCountry.dialCode})`}
|
|
</span>
|
|
<IonIcon
|
|
icon={chevronBackOutline}
|
|
className="rf-country-chevron"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rf-country-trigger"
|
|
onClick={() => setShowLocationSheet(true)}
|
|
disabled={loading || saving}
|
|
aria-label="Select recipient location"
|
|
>
|
|
<span className="rf-country-label">Location</span>
|
|
<span className="rf-country-value-row">
|
|
<span
|
|
className={`rf-country-value${city ? '' : ' is-placeholder'}`}
|
|
>
|
|
{city || 'City, town, or rural area'}
|
|
</span>
|
|
<IonIcon
|
|
icon={chevronBackOutline}
|
|
className="rf-country-chevron"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="rf-input-shell rf-phone-shell"
|
|
onClick={() => void mobileInputRef.current?.setFocus()}
|
|
disabled={loading || saving}
|
|
aria-label="Enter mobile number"
|
|
>
|
|
<span className="rf-field-label">Mobile number</span>
|
|
<div className="rf-phone-row">
|
|
<div className="rf-phone-prefix" aria-hidden="true">
|
|
<span className="rf-phone-flag">{selectedCountry.flag}</span>
|
|
<span className="rf-phone-code">
|
|
{selectedCountry.dialCode}
|
|
</span>
|
|
</div>
|
|
<IonInput
|
|
ref={mobileInputRef}
|
|
className="rf-field rf-phone-field"
|
|
type="tel"
|
|
inputMode="tel"
|
|
placeholder="77 123 4567"
|
|
value={mobileNumber}
|
|
disabled={loading || saving}
|
|
onIonInput={(event) =>
|
|
setMobileNumber(
|
|
normalizePhoneInput(event.detail.value ?? '')
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
</button>
|
|
{error && (
|
|
<p style={{ margin: 0, color: '#dc2626', fontSize: '13px' }}>
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="rf-actions">
|
|
<IonButton
|
|
type="submit"
|
|
expand="block"
|
|
className="rf-submit-btn"
|
|
disabled={loading || saving}
|
|
>
|
|
{saving ? 'Saving...' : isEdit ? 'Save changes' : 'Add loved one'}
|
|
</IonButton>
|
|
</div>
|
|
</form>
|
|
|
|
<IonModal
|
|
isOpen={showCountrySheet}
|
|
onDidDismiss={() => setShowCountrySheet(false)}
|
|
initialBreakpoint={1}
|
|
breakpoints={[0, 1]}
|
|
handle={true}
|
|
className="country-sheet-modal"
|
|
>
|
|
<div className="country-sheet-shell">
|
|
<div className="country-sheet-header">
|
|
<h2>Select country</h2>
|
|
<IonButton
|
|
fill="clear"
|
|
className="country-sheet-close"
|
|
onClick={() => setShowCountrySheet(false)}
|
|
aria-label="Close country selector"
|
|
>
|
|
<IonIcon icon={closeOutline} slot="icon-only" />
|
|
</IonButton>
|
|
</div>
|
|
<div className="country-sheet-scroll">
|
|
<IonList lines="none" className="country-sheet-list">
|
|
{countryOptions.map((option) => {
|
|
const isSelected = option.name === country;
|
|
return (
|
|
<button
|
|
key={option.name}
|
|
type="button"
|
|
className={`country-sheet-option${isSelected ? ' is-selected' : ''}`}
|
|
onClick={() => {
|
|
setCountry(option.name);
|
|
setCity('');
|
|
setShowCountrySheet(false);
|
|
}}
|
|
>
|
|
<span className="country-sheet-option-text">
|
|
<span className="country-sheet-flag">
|
|
{option.flag}
|
|
</span>
|
|
<span className="country-sheet-name">
|
|
{option.name}
|
|
</span>
|
|
<span className="country-sheet-code">
|
|
({option.dialCode})
|
|
</span>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</IonList>
|
|
</div>
|
|
</div>
|
|
</IonModal>
|
|
|
|
<IonModal
|
|
isOpen={showLocationSheet}
|
|
onDidDismiss={() => setShowLocationSheet(false)}
|
|
initialBreakpoint={1}
|
|
breakpoints={[0, 1]}
|
|
handle={true}
|
|
className="country-sheet-modal"
|
|
>
|
|
<div className="country-sheet-shell">
|
|
<div className="country-sheet-header">
|
|
<h2>Select location</h2>
|
|
<IonButton
|
|
fill="clear"
|
|
className="country-sheet-close"
|
|
onClick={() => setShowLocationSheet(false)}
|
|
aria-label="Close location selector"
|
|
>
|
|
<IonIcon icon={closeOutline} slot="icon-only" />
|
|
</IonButton>
|
|
</div>
|
|
<div className="country-sheet-scroll">
|
|
<div className="location-sheet-helper">
|
|
Choose a city, town, or rural district where support collection
|
|
is available.
|
|
</div>
|
|
{locationGroups.map((group) => (
|
|
<div key={group.group} className="location-sheet-group">
|
|
<p className="location-sheet-group-title">{group.group}</p>
|
|
{group.options.map((option) => {
|
|
const isSelected = option.name === city;
|
|
return (
|
|
<button
|
|
key={option.name}
|
|
type="button"
|
|
className={`country-sheet-option location-sheet-option${isSelected ? ' is-selected' : ''}`}
|
|
onClick={() => {
|
|
setCity(option.name);
|
|
setShowLocationSheet(false);
|
|
}}
|
|
>
|
|
<span className="location-sheet-option-text">
|
|
<span className="location-sheet-name">
|
|
{option.name}
|
|
</span>
|
|
<span className="location-sheet-description">
|
|
{option.description}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</IonModal>
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default RecipientFormPage;
|