add list of cities for car pick drop off locations
This commit is contained in:
@@ -9,6 +9,8 @@ import { apiFetch } from '@/lib/api'
|
|||||||
import VehicleCalendar from '@/components/VehicleCalendar'
|
import VehicleCalendar from '@/components/VehicleCalendar'
|
||||||
import VehiclePricingManager from '@/components/VehiclePricingManager'
|
import VehiclePricingManager from '@/components/VehiclePricingManager'
|
||||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||||
|
import LocationDropdownField from '@/components/LocationDropdownField'
|
||||||
|
import { getMoroccanCityOptions } from '@/lib/moroccanCities'
|
||||||
|
|
||||||
interface VehicleDetail {
|
interface VehicleDetail {
|
||||||
id: string
|
id: string
|
||||||
@@ -287,6 +289,7 @@ export default function FleetDetailPage() {
|
|||||||
const { dict, language } = useDashboardI18n()
|
const { dict, language } = useDashboardI18n()
|
||||||
const vd = dict.vehicleDetail
|
const vd = dict.vehicleDetail
|
||||||
const fl = dict.fleet
|
const fl = dict.fleet
|
||||||
|
const cityOptions = getMoroccanCityOptions(language)
|
||||||
|
|
||||||
const [vehicle, setVehicle] = useState<VehicleDetail | null>(null)
|
const [vehicle, setVehicle] = useState<VehicleDetail | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -754,17 +757,15 @@ export default function FleetDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
<div>
|
<LocationDropdownField
|
||||||
<label className="block text-xs font-medium text-slate-500 mb-1.5">{fl.pickupLocations}</label>
|
label={fl.pickupLocations}
|
||||||
<textarea
|
placeholder={fl.pickupLocationsPlaceholder}
|
||||||
className="input-field resize-none"
|
hint={fl.locationHint}
|
||||||
rows={2}
|
addCustomLabel={fl.addCustomLocation}
|
||||||
placeholder={fl.pickupLocationsPlaceholder}
|
options={cityOptions}
|
||||||
value={form.pickupLocations}
|
value={parseLocationInput(form.pickupLocations)}
|
||||||
onChange={(e) => setForm({ ...form, pickupLocations: e.target.value })}
|
onChange={(locations) => setForm({ ...form, pickupLocations: formatLocationInput(locations) })}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">{fl.locationHint}</p>
|
|
||||||
</div>
|
|
||||||
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
|
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -779,17 +780,15 @@ export default function FleetDetailPage() {
|
|||||||
<span>{fl.allowDifferentDropoff}</span>
|
<span>{fl.allowDifferentDropoff}</span>
|
||||||
</label>
|
</label>
|
||||||
{form.allowDifferentDropoff && (
|
{form.allowDifferentDropoff && (
|
||||||
<div>
|
<LocationDropdownField
|
||||||
<label className="block text-xs font-medium text-slate-500 mb-1.5">{fl.dropoffLocations}</label>
|
label={fl.dropoffLocations}
|
||||||
<textarea
|
placeholder={fl.dropoffLocationsPlaceholder}
|
||||||
className="input-field resize-none"
|
hint={fl.locationHint}
|
||||||
rows={2}
|
addCustomLabel={fl.addCustomLocation}
|
||||||
placeholder={fl.dropoffLocationsPlaceholder}
|
options={cityOptions}
|
||||||
value={form.dropoffLocations}
|
value={parseLocationInput(form.dropoffLocations)}
|
||||||
onChange={(e) => setForm({ ...form, dropoffLocations: e.target.value })}
|
onChange={(locations) => setForm({ ...form, dropoffLocations: formatLocationInput(locations) })}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">{fl.locationHint}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|||||||
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
|
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
|
||||||
import { formatCurrency } from '@rentaldrivego/types'
|
import { formatCurrency } from '@rentaldrivego/types'
|
||||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||||
|
import LocationDropdownField from '@/components/LocationDropdownField'
|
||||||
|
import { getMoroccanCityOptions } from '@/lib/moroccanCities'
|
||||||
|
|
||||||
interface Vehicle {
|
interface Vehicle {
|
||||||
id: string
|
id: string
|
||||||
@@ -214,9 +216,14 @@ function parseLocationInput(value: string) {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLocationInput(values: string[]) {
|
||||||
|
return values.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
|
function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
|
||||||
const { dict } = useDashboardI18n()
|
const { dict, language } = useDashboardI18n()
|
||||||
const f = dict.fleet
|
const f = dict.fleet
|
||||||
|
const cityOptions = getMoroccanCityOptions(language)
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
|
make: '', model: '', year: new Date().getFullYear(), category: 'ECONOMY',
|
||||||
licensePlate: '', vin: '', color: '', seats: 5, transmission: 'MANUAL',
|
licensePlate: '', vin: '', color: '', seats: 5, transmission: 'MANUAL',
|
||||||
@@ -451,17 +458,15 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
<div className="space-y-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||||
<div>
|
<LocationDropdownField
|
||||||
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.pickupLocations}</label>
|
label={f.pickupLocations}
|
||||||
<textarea
|
placeholder={f.pickupLocationsPlaceholder}
|
||||||
className="input-field resize-none"
|
hint={f.locationHint}
|
||||||
rows={2}
|
addCustomLabel={f.addCustomLocation}
|
||||||
placeholder={f.pickupLocationsPlaceholder}
|
options={cityOptions}
|
||||||
value={form.pickupLocations}
|
value={parseLocationInput(form.pickupLocations)}
|
||||||
onChange={(e) => setForm({ ...form, pickupLocations: e.target.value })}
|
onChange={(locations) => setForm({ ...form, pickupLocations: formatLocationInput(locations) })}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">{f.locationHint}</p>
|
|
||||||
</div>
|
|
||||||
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
|
<label className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white px-3 py-3 text-sm font-medium text-slate-700">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -476,17 +481,15 @@ function AddVehicleModal({ open, onClose, onSaved }: AddVehicleModalProps) {
|
|||||||
<span>{f.allowDifferentDropoff}</span>
|
<span>{f.allowDifferentDropoff}</span>
|
||||||
</label>
|
</label>
|
||||||
{form.allowDifferentDropoff && (
|
{form.allowDifferentDropoff && (
|
||||||
<div>
|
<LocationDropdownField
|
||||||
<label className="block text-xs font-medium text-slate-500 mb-1.5">{f.dropoffLocations}</label>
|
label={f.dropoffLocations}
|
||||||
<textarea
|
placeholder={f.dropoffLocationsPlaceholder}
|
||||||
className="input-field resize-none"
|
hint={f.locationHint}
|
||||||
rows={2}
|
addCustomLabel={f.addCustomLocation}
|
||||||
placeholder={f.dropoffLocationsPlaceholder}
|
options={cityOptions}
|
||||||
value={form.dropoffLocations}
|
value={parseLocationInput(form.dropoffLocations)}
|
||||||
onChange={(e) => setForm({ ...form, dropoffLocations: e.target.value })}
|
onChange={(locations) => setForm({ ...form, dropoffLocations: formatLocationInput(locations) })}
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">{f.locationHint}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type FleetDict = {
|
|||||||
dropoffLocations: string
|
dropoffLocations: string
|
||||||
dropoffLocationsPlaceholder: string
|
dropoffLocationsPlaceholder: string
|
||||||
locationHint: string
|
locationHint: string
|
||||||
|
addCustomLocation: (location: string) => string
|
||||||
allowDifferentDropoff: string
|
allowDifferentDropoff: string
|
||||||
dropoffLocationsRequired: string
|
dropoffLocationsRequired: string
|
||||||
sameDropoffOnly: string
|
sameDropoffOnly: string
|
||||||
@@ -406,10 +407,11 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
|||||||
automatic: 'Automatic',
|
automatic: 'Automatic',
|
||||||
fuelType: 'Fuel Type',
|
fuelType: 'Fuel Type',
|
||||||
pickupLocations: 'Pick-up locations',
|
pickupLocations: 'Pick-up locations',
|
||||||
pickupLocationsPlaceholder: 'e.g. Casablanca Airport, Casablanca Downtown',
|
pickupLocationsPlaceholder: 'Type a pick-up city',
|
||||||
dropoffLocations: 'Drop-off locations',
|
dropoffLocations: 'Drop-off locations',
|
||||||
dropoffLocationsPlaceholder: 'e.g. Rabat Downtown, Marrakech Center',
|
dropoffLocationsPlaceholder: 'Type a drop-off city',
|
||||||
locationHint: 'Separate multiple locations with commas.',
|
locationHint: 'Start typing and select one or more cities from the suggestions. Press Enter to add a new city.',
|
||||||
|
addCustomLocation: (location) => `Add "${location}"`,
|
||||||
allowDifferentDropoff: 'Allow different drop-off location',
|
allowDifferentDropoff: 'Allow different drop-off location',
|
||||||
dropoffLocationsRequired: 'Please add at least one drop-off location when different drop-off is enabled.',
|
dropoffLocationsRequired: 'Please add at least one drop-off location when different drop-off is enabled.',
|
||||||
sameDropoffOnly: 'Same drop-off only',
|
sameDropoffOnly: 'Same drop-off only',
|
||||||
@@ -763,10 +765,11 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
|||||||
automatic: 'Automatique',
|
automatic: 'Automatique',
|
||||||
fuelType: 'Carburant',
|
fuelType: 'Carburant',
|
||||||
pickupLocations: 'Lieux de départ',
|
pickupLocations: 'Lieux de départ',
|
||||||
pickupLocationsPlaceholder: 'ex. Aéroport de Casablanca, Centre-ville de Casablanca',
|
pickupLocationsPlaceholder: 'Tapez une ville de départ',
|
||||||
dropoffLocations: 'Lieux de retour',
|
dropoffLocations: 'Lieux de retour',
|
||||||
dropoffLocationsPlaceholder: 'ex. Centre-ville de Rabat, Centre de Marrakech',
|
dropoffLocationsPlaceholder: 'Tapez une ville de retour',
|
||||||
locationHint: 'Séparez plusieurs lieux par des virgules.',
|
locationHint: 'Commencez à taper puis sélectionnez une ou plusieurs villes suggérées. Appuyez sur Entrée pour ajouter une nouvelle ville.',
|
||||||
|
addCustomLocation: (location) => `Ajouter « ${location} »`,
|
||||||
allowDifferentDropoff: 'Autoriser un lieu de retour différent',
|
allowDifferentDropoff: 'Autoriser un lieu de retour différent',
|
||||||
dropoffLocationsRequired: 'Ajoutez au moins un lieu de retour lorsque le retour différent est activé.',
|
dropoffLocationsRequired: 'Ajoutez au moins un lieu de retour lorsque le retour différent est activé.',
|
||||||
sameDropoffOnly: 'Retour au même lieu uniquement',
|
sameDropoffOnly: 'Retour au même lieu uniquement',
|
||||||
@@ -1120,10 +1123,11 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
|||||||
automatic: 'أوتوماتيكي',
|
automatic: 'أوتوماتيكي',
|
||||||
fuelType: 'نوع الوقود',
|
fuelType: 'نوع الوقود',
|
||||||
pickupLocations: 'مواقع الاستلام',
|
pickupLocations: 'مواقع الاستلام',
|
||||||
pickupLocationsPlaceholder: 'مثال: مطار الدار البيضاء، وسط الدار البيضاء',
|
pickupLocationsPlaceholder: 'اكتب مدينة الاستلام',
|
||||||
dropoffLocations: 'مواقع الإرجاع',
|
dropoffLocations: 'مواقع الإرجاع',
|
||||||
dropoffLocationsPlaceholder: 'مثال: وسط الرباط، مركز مراكش',
|
dropoffLocationsPlaceholder: 'اكتب مدينة الإرجاع',
|
||||||
locationHint: 'افصل بين المواقع المتعددة بفواصل.',
|
locationHint: 'ابدأ بالكتابة ثم اختر مدينة واحدة أو أكثر من الاقتراحات. اضغط Enter لإضافة مدينة جديدة.',
|
||||||
|
addCustomLocation: (location) => `إضافة "${location}"`,
|
||||||
allowDifferentDropoff: 'السماح بموقع إرجاع مختلف',
|
allowDifferentDropoff: 'السماح بموقع إرجاع مختلف',
|
||||||
dropoffLocationsRequired: 'أضف موقع إرجاع واحداً على الأقل عند تفعيل الإرجاع المختلف.',
|
dropoffLocationsRequired: 'أضف موقع إرجاع واحداً على الأقل عند تفعيل الإرجاع المختلف.',
|
||||||
sameDropoffOnly: 'الإرجاع في نفس الموقع فقط',
|
sameDropoffOnly: 'الإرجاع في نفس الموقع فقط',
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
|
||||||
|
interface LocationDropdownFieldProps {
|
||||||
|
label: string
|
||||||
|
placeholder: string
|
||||||
|
hint: string
|
||||||
|
addCustomLabel: (location: string) => string
|
||||||
|
options: string[]
|
||||||
|
value: string[]
|
||||||
|
onChange: (locations: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SUGGESTIONS = 8
|
||||||
|
const CUSTOM_LOCATION_OPTIONS_KEY = 'dashboard-custom-vehicle-location-options'
|
||||||
|
const CUSTOM_LOCATION_OPTIONS_CHANGED = 'dashboard-custom-vehicle-location-options-changed'
|
||||||
|
|
||||||
|
function normalizeLocationLabel(value: string) {
|
||||||
|
return value.trim().replace(/\s+/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSearchValue(value: string) {
|
||||||
|
return normalizeLocationLabel(value)
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCustomLocationOptions() {
|
||||||
|
if (typeof window === 'undefined') return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(window.localStorage.getItem(CUSTOM_LOCATION_OPTIONS_KEY) ?? '[]')
|
||||||
|
if (!Array.isArray(parsed)) return []
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
.map((entry) => (typeof entry === 'string' ? normalizeLocationLabel(entry) : ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCustomLocationOptions(options: string[]) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.localStorage.setItem(CUSTOM_LOCATION_OPTIONS_KEY, JSON.stringify(options))
|
||||||
|
window.dispatchEvent(new Event(CUSTOM_LOCATION_OPTIONS_CHANGED))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocationDropdownField({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
hint,
|
||||||
|
addCustomLabel,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LocationDropdownFieldProps) {
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [focused, setFocused] = useState(false)
|
||||||
|
const [customOptions, setCustomOptions] = useState<string[]>([])
|
||||||
|
const selected = value.map((location) => location.trim()).filter(Boolean)
|
||||||
|
const selectedSet = new Set(selected.map(normalizeSearchValue))
|
||||||
|
const allOptions = useMemo(() => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return [...customOptions, ...options]
|
||||||
|
.map(normalizeLocationLabel)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((option) => {
|
||||||
|
const key = normalizeSearchValue(option)
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}, [customOptions, options])
|
||||||
|
const normalizedQuery = normalizeSearchValue(query)
|
||||||
|
const suggestions = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return []
|
||||||
|
|
||||||
|
return allOptions
|
||||||
|
.filter((option) => !selectedSet.has(normalizeSearchValue(option)))
|
||||||
|
.filter((option) => normalizeSearchValue(option).startsWith(normalizedQuery))
|
||||||
|
.slice(0, MAX_SUGGESTIONS)
|
||||||
|
}, [allOptions, normalizedQuery, selectedSet])
|
||||||
|
const normalizedOptionSet = useMemo(() => {
|
||||||
|
return new Set(allOptions.map(normalizeSearchValue))
|
||||||
|
}, [allOptions])
|
||||||
|
const typedLocation = normalizeLocationLabel(query)
|
||||||
|
const canAddTypedLocation = Boolean(
|
||||||
|
normalizedQuery &&
|
||||||
|
!selectedSet.has(normalizedQuery) &&
|
||||||
|
!normalizedOptionSet.has(normalizedQuery),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncCustomOptions = () => setCustomOptions(readCustomLocationOptions())
|
||||||
|
|
||||||
|
syncCustomOptions()
|
||||||
|
window.addEventListener('storage', syncCustomOptions)
|
||||||
|
window.addEventListener(CUSTOM_LOCATION_OPTIONS_CHANGED, syncCustomOptions)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', syncCustomOptions)
|
||||||
|
window.removeEventListener(CUSTOM_LOCATION_OPTIONS_CHANGED, syncCustomOptions)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const saveCustomLocation = (location: string) => {
|
||||||
|
const normalizedLocation = normalizeLocationLabel(location)
|
||||||
|
if (!normalizedLocation) return
|
||||||
|
|
||||||
|
const existingOptions = readCustomLocationOptions()
|
||||||
|
const existingKeys = new Set([...existingOptions, ...options].map(normalizeSearchValue))
|
||||||
|
if (existingKeys.has(normalizeSearchValue(normalizedLocation))) return
|
||||||
|
|
||||||
|
const nextOptions = [...existingOptions, normalizedLocation].sort((a, b) => a.localeCompare(b))
|
||||||
|
writeCustomLocationOptions(nextOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLocation = (location: string) => {
|
||||||
|
const normalizedLocation = normalizeLocationLabel(location)
|
||||||
|
if (!normalizedLocation || selectedSet.has(normalizeSearchValue(normalizedLocation))) return
|
||||||
|
if (!normalizedOptionSet.has(normalizeSearchValue(normalizedLocation))) {
|
||||||
|
saveCustomLocation(normalizedLocation)
|
||||||
|
}
|
||||||
|
onChange([...selected, normalizedLocation])
|
||||||
|
setQuery('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLocation = (location: string) => {
|
||||||
|
onChange(selected.filter((selectedLocation) => selectedLocation !== location))
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSuggestions = focused && (suggestions.length > 0 || canAddTypedLocation)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-slate-500 mb-1.5">{label}</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input-field"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
onFocus={() => setFocused(true)}
|
||||||
|
onBlur={() => setFocused(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== 'Enter') return
|
||||||
|
if (suggestions[0]) {
|
||||||
|
e.preventDefault()
|
||||||
|
addLocation(suggestions[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (canAddTypedLocation) {
|
||||||
|
e.preventDefault()
|
||||||
|
addLocation(typedLocation)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={label}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{showSuggestions && (
|
||||||
|
<div className="absolute z-20 mt-1 max-h-56 w-full overflow-y-auto rounded-lg border border-slate-200 bg-white py-1 shadow-lg">
|
||||||
|
{suggestions.map((location) => (
|
||||||
|
<button
|
||||||
|
key={location}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
addLocation(location)
|
||||||
|
}}
|
||||||
|
className="block w-full px-3 py-2 text-left text-sm text-slate-700 transition hover:bg-slate-50 focus:bg-slate-50 focus:outline-none"
|
||||||
|
>
|
||||||
|
{location}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{canAddTypedLocation && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
addLocation(typedLocation)
|
||||||
|
}}
|
||||||
|
className="block w-full border-t border-slate-100 px-3 py-2 text-left text-sm font-medium text-blue-700 transition hover:bg-blue-50 focus:bg-blue-50 focus:outline-none"
|
||||||
|
>
|
||||||
|
{addCustomLabel(typedLocation)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selected.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{selected.map((location) => (
|
||||||
|
<span key={location} className="inline-flex max-w-full items-center gap-1 rounded-full bg-white px-2.5 py-1 text-xs font-medium text-slate-700 ring-1 ring-slate-200">
|
||||||
|
<span className="truncate">{location}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeLocation(location)}
|
||||||
|
className="rounded-full p-0.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
|
||||||
|
aria-label={`Remove ${location}`}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-xs text-slate-400">{hint}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
import citiesDataset from '@/data/morocco-cities-rgph-2024.json'
|
||||||
|
|
||||||
|
export type SupportedCityLocale = 'en' | 'fr' | 'ar'
|
||||||
|
|
||||||
|
interface LocalizedName {
|
||||||
|
en: string
|
||||||
|
fr: string
|
||||||
|
ar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MoroccanCity {
|
||||||
|
id: string
|
||||||
|
name: LocalizedName
|
||||||
|
province: LocalizedName
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MoroccanCitiesDataset {
|
||||||
|
cities: MoroccanCity[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMoroccanCityOptions(locale: string) {
|
||||||
|
const resolvedLocale: SupportedCityLocale = locale === 'fr' || locale === 'ar' ? locale : 'en'
|
||||||
|
const cityNames = (citiesDataset as MoroccanCitiesDataset).cities.map((city) => city.name[resolvedLocale])
|
||||||
|
|
||||||
|
return Array.from(new Set(cityNames)).sort((a, b) => a.localeCompare(b, resolvedLocale))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeLocationOptions(options: string[], selectedLocations: string[]) {
|
||||||
|
const selected = selectedLocations.map((location) => location.trim()).filter(Boolean)
|
||||||
|
return Array.from(new Set([...selected, ...options]))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user