feat(create-spot): make address + access type required; move them out of the optional collapsible
This commit is contained in:
parent
c08592144a
commit
30a0aaa42b
5 changed files with 40 additions and 26 deletions
|
|
@ -1127,6 +1127,13 @@ session with **no other memory of this project**. So:
|
|||
stall signing (a miss falls back to the stats' read-time region derivation).
|
||||
⚠️ A geohash-prefix nearby query is still impossible — relays only exact-match
|
||||
`#g` — so the 2000-spot cache remains the mechanism.
|
||||
**Required set (2026-10-07):** pin + name + description + **address** +
|
||||
**access type** + photo. Address and access type were **moved OUT of the
|
||||
"More details (optional)" collapsible** into the required flow (the collapsible
|
||||
now holds only Shelter + Tags) — required fields must not be hidden under
|
||||
"optional". Access keeps its **Public default** (so it is always satisfied; the
|
||||
`*` marks membership in the required set), and Address has real inline
|
||||
validation (`createSpot.addressRequired`).
|
||||
- **Map pin rendering (perf, 2026-09-29):** the spot/tour/event/crew marker
|
||||
effect in `SpotMap.tsx` used to `clearLayers()` + rebuild **every** marker
|
||||
whenever the parent handed it fresh arrays, and added each marker with a
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ nav: {
|
|||
photoHint: 'a photo is required to publish',
|
||||
nameRequired: 'Please enter a name for this spot.',
|
||||
descriptionRequired: 'Please describe this spot.',
|
||||
addressRequired: 'Please add an address or area name.',
|
||||
photoRequired: 'Please add at least one photo.',
|
||||
draftTitle: 'You have an unsaved draft',
|
||||
draftBody: 'Pick up where you left off, or discard it.',
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ nav: {
|
|||
photoHint: 'se requiere una foto para publicar',
|
||||
nameRequired: 'Ponle un nombre a este spot.',
|
||||
descriptionRequired: 'Describe este spot.',
|
||||
addressRequired: 'Añade una dirección o nombre de zona.',
|
||||
photoRequired: 'Añade al menos una foto.',
|
||||
draftTitle: 'Tienes un borrador sin guardar',
|
||||
draftBody: 'Continúa donde lo dejaste o descártalo.',
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ const thBatch5 = {
|
|||
photoHint: 'ต้องมีรูปจึงจะเผยแพร่ได้',
|
||||
nameRequired: 'กรุณาตั้งชื่อสปอตนี้',
|
||||
descriptionRequired: 'กรุณาอธิบายสปอตนี้',
|
||||
addressRequired: 'กรุณาเพิ่มที่อยู่หรือชื่อพื้นที่',
|
||||
photoRequired: 'กรุณาเพิ่มรูปอย่างน้อยหนึ่งรูป',
|
||||
draftTitle: 'คุณมีฉบับร่างที่ยังไม่ได้บันทึก',
|
||||
draftBody: 'ทำต่อจากที่ค้างไว้ หรือลบทิ้ง',
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { Link } from 'react-router-dom';
|
|||
type FieldErrors = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
address?: string;
|
||||
location?: string;
|
||||
type?: string;
|
||||
photo?: string;
|
||||
|
|
@ -342,6 +343,7 @@ export default function CreateSpotPage() {
|
|||
const nextErrors: FieldErrors = {};
|
||||
if (!title.trim()) nextErrors.title = t('createSpot.nameRequired');
|
||||
if (!description.trim()) nextErrors.description = t('createSpot.descriptionRequired');
|
||||
if (!location.trim()) nextErrors.address = t('createSpot.addressRequired');
|
||||
if (!hasCoords) nextErrors.location = t('spotUi.locationRequiredDesc');
|
||||
if (!selectedCategory) nextErrors.type = t('spotUi.spotTypeRequiredDesc');
|
||||
if (images.length === 0) nextErrors.photo = t('createSpot.photoRequired');
|
||||
|
|
@ -613,6 +615,19 @@ export default function CreateSpotPage() {
|
|||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{/* Address (required) — the human-readable place name. */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="location">{t('spots.address')} *</Label>
|
||||
<Input
|
||||
id="location"
|
||||
placeholder={t('createSpot.addressPlaceholder')}
|
||||
value={location}
|
||||
onChange={(e) => { setLocation(e.target.value); clearError('address'); }}
|
||||
className={cn(errors.address && 'border-red-500/60')}
|
||||
/>
|
||||
{errors.address && <p className="text-xs font-medium text-red-400">{errors.address}</p>}
|
||||
</div>
|
||||
|
||||
{/* Duplicate warning — appears as soon as the pin lands, right
|
||||
below the map, instead of only on submit. */}
|
||||
{dupWarning && (
|
||||
|
|
@ -776,34 +791,9 @@ export default function CreateSpotPage() {
|
|||
isUploading={isUploading}
|
||||
/>
|
||||
|
||||
{/* ── More details (optional) ───────────────────────────── */}
|
||||
<Collapsible open={showMore} onOpenChange={setShowMore} className="rounded-xl border border-white/10">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between gap-2 p-4 text-left"
|
||||
>
|
||||
<span className="text-sm font-black uppercase tracking-wide text-white">
|
||||
{t('createSpot.moreDetails')}
|
||||
</span>
|
||||
<ChevronDown className={cn('h-4 w-4 text-gray-400 transition-transform', showMore && 'rotate-180')} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-6 px-4 pb-4">
|
||||
{/* Address */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="location">{t('spots.address')}</Label>
|
||||
<Input
|
||||
id="location"
|
||||
placeholder={t('createSpot.addressPlaceholder')}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Access Type */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-bold">{t('createSpot.accessType')}</Label>
|
||||
<Label className="text-base font-bold">{t('createSpot.accessType')} *</Label>
|
||||
<div className="space-y-2">
|
||||
{/* Public */}
|
||||
<Card
|
||||
|
|
@ -904,6 +894,20 @@ export default function CreateSpotPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* ── More details (optional) ───────────────────────────── */}
|
||||
<Collapsible open={showMore} onOpenChange={setShowMore} className="rounded-xl border border-white/10">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between gap-2 p-4 text-left"
|
||||
>
|
||||
<span className="text-sm font-black uppercase tracking-wide text-white">
|
||||
{t('createSpot.moreDetails')}
|
||||
</span>
|
||||
<ChevronDown className={cn('h-4 w-4 text-gray-400 transition-transform', showMore && 'rotate-180')} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-6 px-4 pb-4">
|
||||
{/* Shelter / weather protection */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-bold">{t('spotUi.shelter')}</Label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue