build: 444813b5-5daf-49b2-bf1a-7e4caeab4e17
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
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 AvatarPicker from '../components/AvatarPicker';
|
||||
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 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 showError = (message: string) => {
|
||||
setError(message);
|
||||
setTimeout(() => setError(null), 4000);
|
||||
};
|
||||
|
||||
const loadRecipient = async () => {
|
||||
if (!user || !id) return;
|
||||
setLoading(true);
|
||||
|
||||
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', user.id)
|
||||
.single();
|
||||
|
||||
if (loadError || !data) {
|
||||
showError(loadError?.message ?? 'Recipient not found');
|
||||
setLoading(false);
|
||||
history.replace('/recipients');
|
||||
return;
|
||||
}
|
||||
|
||||
const recipient = data as 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);
|
||||
|
||||
if (recipient.photo_path) {
|
||||
const { data: signed } = await supabase.storage
|
||||
.from('recipient-photos')
|
||||
.createSignedUrl(recipient.photo_path, 3600);
|
||||
setPhotoPreview(signed?.signedUrl ?? null);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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={() =>
|
||||
history.length > 1
|
||||
? history.goBack()
|
||||
: history.replace('/recipients')
|
||||
}
|
||||
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': '0px',
|
||||
'--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;
|
||||
Reference in New Issue
Block a user