diff --git a/apps/citizen-pwa/src/App.routes.test.tsx b/apps/citizen-pwa/src/App.routes.test.tsx index 7453425d..70c79372 100644 --- a/apps/citizen-pwa/src/App.routes.test.tsx +++ b/apps/citizen-pwa/src/App.routes.test.tsx @@ -6,7 +6,7 @@ vi.mock('./components/MapTab/index.js', () => ({ MapTab: () =>
Map tab
, })) -vi.mock('./components/SubmitReportForm.js', () => ({ +vi.mock('./components/SubmitReportForm/index.js', () => ({ SubmitReportForm: () =>
Report form
, })) @@ -41,10 +41,11 @@ describe('App routes', () => { expect(screen.getByRole('button', { name: /map/i })).toHaveAttribute('aria-current', 'page') }) - it('shows the report shell at /report', async () => { + it('shows the report form at /report', async () => { await renderAppAt('/report') expect(screen.getByText('Report form')).toBeInTheDocument() - expect(screen.getByRole('button', { name: /report/i })).toHaveAttribute('aria-current', 'page') + expect(screen.queryByRole('banner')).not.toBeInTheDocument() + expect(screen.queryByRole('navigation', { name: /main navigation/i })).not.toBeInTheDocument() }) it('navigates between shell tabs', async () => { diff --git a/apps/citizen-pwa/src/components/CitizenShell.tsx b/apps/citizen-pwa/src/components/CitizenShell.tsx index da3798a6..739b739e 100644 --- a/apps/citizen-pwa/src/components/CitizenShell.tsx +++ b/apps/citizen-pwa/src/components/CitizenShell.tsx @@ -1,13 +1,14 @@ import type { ReactNode } from 'react' import { useLocation, useNavigate } from 'react-router-dom' +import { Map, Rss, AlertTriangle, Bell, User } from 'lucide-react' import '../styles/design-tokens.css' const TABS = [ - { label: 'Map', path: '/' }, - { label: 'Feed', path: '/feed' }, - { label: 'Report', path: '/report' }, - { label: 'Alerts', path: '/alerts' }, - { label: 'Profile', path: '/profile' }, + { label: 'Map', path: '/', Icon: Map }, + { label: 'Feed', path: '/feed', Icon: Rss }, + { label: 'Report', path: '/report', Icon: AlertTriangle }, + { label: 'Alerts', path: '/alerts', Icon: Bell }, + { label: 'Profile', path: '/profile', Icon: User }, ] as const export function CitizenShell({ children }: { children: ReactNode }) { @@ -80,17 +81,13 @@ export function CitizenShell({ children }: { children: ReactNode }) { cursor: 'pointer', }} > - - {tab.label === 'Report' - ? '🚨' - : tab.label === 'Alerts' - ? '⚠️' - : tab.label === 'Profile' - ? '👤' - : tab.label === 'Feed' - ? '📋' - : '🗺'} - + {tab.label} ))} diff --git a/apps/citizen-pwa/src/components/SubmitReportForm.tsx b/apps/citizen-pwa/src/components/SubmitReportForm.tsx deleted file mode 100644 index 59369910..00000000 --- a/apps/citizen-pwa/src/components/SubmitReportForm.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { addDoc, collection } from 'firebase/firestore' -import { httpsCallable } from 'firebase/functions' -import { - db, - fns, - ensureSignedIn, - hasFirebaseConfig, - FIREBASE_ENV_ERROR_MESSAGE, -} from '../services/firebase.js' -import { saveReport } from '../services/localForageReports.js' -import { submitReport, type SubmitReportDeps } from '../services/submit-report.js' -import { normalizeMsisdn } from '@bantayog/shared-validators' -import type { ReportType, Severity } from '@bantayog/shared-types' - -function randomPublicRef(): string { - const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' - const bytes = crypto.getRandomValues(new Uint8Array(8)) - return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join('') -} - -function randomSecret(): string { - return crypto.randomUUID() -} - -async function sha256Hex(input: string | Blob): Promise { - const buf = - typeof input === 'string' - ? new TextEncoder().encode(input) - : new Uint8Array(await input.arrayBuffer()) - const digest = await crypto.subtle.digest('SHA-256', buf) - return Array.from(new Uint8Array(digest)) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') -} - -async function putBlob(url: string, blob: Blob): Promise { - const res = await fetch(url, { - method: 'PUT', - body: blob, - headers: { 'content-type': blob.type }, - }) - if (!res.ok) throw new Error('upload failed: ' + String(res.status)) -} - -export function SubmitReportForm() { - const nav = useNavigate() - const [reportType, setReportType] = useState('flood') - const [severity, setSeverity] = useState('medium') - const [description, setDescription] = useState('') - const [photo, setPhoto] = useState(null) - const [lat, setLat] = useState(null) - const [lng, setLng] = useState(null) - const [phone, setPhone] = useState('') - const [smsConsent, setSmsConsent] = useState(false) - const [phoneError, setPhoneError] = useState(null) - const [busy, setBusy] = useState(false) - const [error, setError] = useState(null) - - async function getLocation(): Promise { - const pos = await new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, { - enableHighAccuracy: true, - timeout: 10000, - }) - }) - setLat(pos.coords.latitude) - setLng(pos.coords.longitude) - } - - async function onCaptureLocation(): Promise { - try { - await getLocation() - } catch (e: unknown) { - setError(e instanceof Error ? e.message : 'location failed') - } - } - - async function onSubmit(e: React.SubmitEvent): Promise { - e.preventDefault() - if (lat === null || lng === null) { - setError('Please capture your location.') - return - } - if (phone) { - try { - normalizeMsisdn(phone) - } catch { - setPhoneError('Enter a valid PH mobile number (e.g. 09171234567 or +639171234567)') - return - } - } - setBusy(true) - setError(null) - try { - if (!hasFirebaseConfig()) { - throw new Error(FIREBASE_ENV_ERROR_MESSAGE) - } - const deps: SubmitReportDeps = { - ensureSignedIn, - requestUploadUrl: async (args) => - (await httpsCallable(fns(), 'requestUploadUrl')(args)).data as { - uploadUrl: string - uploadId: string - storagePath: string - expiresAt: number - }, - putBlob, - writeInbox: async (doc) => { - const ref = await addDoc(collection(db(), 'report_inbox'), doc) - return ref.id - }, - randomUUID: () => crypto.randomUUID(), - randomPublicRef, - randomSecret, - sha256Hex, - now: () => Date.now(), - } - const result = await submitReport(deps, { - reportType, - severity, - description, - publicLocation: { lat, lng }, - ...(photo ? { photo } : {}), - ...(phone && smsConsent ? { contact: { phone, smsConsent: true as const } } : {}), - }) - void saveReport({ - publicRef: result.publicRef, - secret: result.secret, - reportType, - severity, - lat, - lng, - submittedAt: Date.now(), - }).catch((persistError: unknown) => { - console.error('Failed to cache report locally', persistError) - }) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - nav('/receipt', { state: result }) - } catch (e: unknown) { - setError(e instanceof Error ? e.message : 'submission failed') - } finally { - setBusy(false) - } - } - - return ( -
- - -