import { useRef, useState } from 'react'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Selector } from '@astryxdesign/core/Selector'; import { Text } from '@astryxdesign/core/Text'; import { TextInput } from '@astryxdesign/core/TextInput'; import { TimeInput } from '@astryxdesign/core/TimeInput'; import { createISOTimeString, type ISOTimeString } from '@astryxdesign/core/utils'; import { VStack } from '@astryxdesign/core/VStack'; import { ImagePlus, Upload } from 'lucide-react'; import type { StoreDraft } from '../onboardingState'; import type { Errors } from '../validation'; /** * The one step that is a form, kept to three short groups. * * Grouped rather than listed because twelve fields in a column reads as a tax * return. Basics, then where the shop is, then how it trades — each of which a * shopkeeper can answer without looking anything up. * * Fields are marked required with an asterisk and validated on submit, not on * every keystroke: telling somebody their email is invalid while they are on * the third character is correcting them for not having finished typing. */ export interface StoreInfoStepProps { value: StoreDraft; errors: Errors; onChange: (next: StoreDraft) => void; } /** Matches `tenants.tenanttype`, which the platform already stores. */ const STORE_TYPES = [ { value: 'grocery', label: 'Grocery / kirana' }, { value: 'supermarket', label: 'Supermarket' }, { value: 'pharmacy', label: 'Pharmacy' }, { value: 'bakery', label: 'Bakery' }, { value: 'restaurant', label: 'Restaurant' }, { value: 'other', label: 'Something else' }, ]; const MAX_LOGO_BYTES = 2 * 1024 * 1024; /** * "08:00" as the picker wants it. * * The draft stores plain strings so it can be serialised into localStorage and * posted to Fiesta unchanged; TimeInput wants a branded ISOTimeString. Undefined * for anything unparseable, which shows an empty picker rather than throwing on * a value somebody's older draft is carrying. */ function asTime(value: string): ISOTimeString | undefined { return createISOTimeString(value) ?? undefined; } export function StoreInfoStep({ value, errors, onChange }: StoreInfoStepProps) { const set = (key: K) => (next: StoreDraft[K]) => onChange({ ...value, [key]: next }); return (
Store name as unknown as string} value={value.tenantname} onChange={set('tenantname')} placeholder="e.g. Suriya Stores" description="This is the name shoppers see." {...(errors['tenantname'] ? { error: errors['tenantname'] } : {})} /> Phone number as unknown as string} value={value.primarycontact} onChange={set('primarycontact')} placeholder="98765 43210" {...(errors['primarycontact'] ? { error: errors['primarycontact'] } : {})} /> Store type as unknown as string} value={value.tenanttype} onChange={set('tenanttype')} placeholder="Choose one" options={STORE_TYPES} {...(errors['tenanttype'] ? { error: errors['tenanttype'] } : {})} />
Address as unknown as string} value={value.address} onChange={set('address')} placeholder="Shop number, street, area" {...(errors['address'] ? { error: errors['address'] } : {})} />
City as unknown as string} value={value.city} onChange={set('city')} {...(errors['city'] ? { error: errors['city'] } : {})} /> Pincode as unknown as string} value={value.postcode} onChange={set('postcode')} placeholder="641001" {...(errors['postcode'] ? { error: errors['postcode'] } : {})} />
Opens at as unknown as string} value={asTime(value.opentime)} onChange={(next) => set('opentime')(String(next ?? value.opentime))} {...(errors['opentime'] ? { description: errors['opentime'] } : {})} /> Closes at as unknown as string} value={asTime(value.closetime)} onChange={(next) => set('closetime')(String(next ?? value.closetime))} {...(errors['closetime'] ? { description: errors['closetime'] } : {})} />
); } /* ── Pieces ───────────────────────────────────────────────────────────────── */ function Group({ title, note, children, }: { title: string; note?: string; children: React.ReactNode; }) { return ( {title} {note ? ( {note} ) : null} {children} ); } /** The asterisk carries a text alternative, so it is not colour-and-glyph only. */ function Required({ children }: { children: React.ReactNode }) { return ( {children}{' '} * (required) ); } /** * The logo. * * Read as a data URI rather than uploaded, deliberately: `tenants.tenantimage` * holds a string and there is no image endpoint for a merchant to post to — the * console's own S3 helper is disabled ("S3 not enabled, skipping image store"). * A URL box would be the honest alternative and is offered underneath, but a * shopkeeper photographing their shopfront has a file, not a link. * * 2 MB is enforced here because the field is a database column: a 6 MB photo * base64s to 8 MB of text and fails on write, long after the person has moved * on. */ function LogoField({ value, onChange }: { value: string; onChange: (next: string) => void }) { const input = useRef(null); const [problem, setProblem] = useState(null); const [isReading, setIsReading] = useState(false); function take(file: File | undefined) { if (!file) return; setProblem(null); if (!/^image\/(png|jpe?g|webp)$/.test(file.type)) { setProblem('That is not an image. Choose a JPG, PNG or WebP file.'); return; } if (file.size > MAX_LOGO_BYTES) { setProblem( `That file is ${(file.size / 1024 / 1024).toFixed(1)} MB. The limit is 2 MB — try a smaller photo.`, ); return; } setIsReading(true); const reader = new FileReader(); reader.onload = () => { onChange(String(reader.result ?? '')); setIsReading(false); }; reader.onerror = () => { setProblem('That file could not be read. Try another one.'); setIsReading(false); }; reader.readAsDataURL(file); } return ( Store logo {value ? ( Your store logo ) : ( )} JPG, PNG or WebP, up to 2 MB. Optional. take(event.target.files?.[0])} style={{ display: 'none' }} aria-hidden tabIndex={-1} /> {/* The honest alternative: the field is a string, so a hosted image works just as well and costs the database nothing. */} {problem ? ( {problem} ) : null} ); }