diff --git a/apps/citizen-pwa/src/components/DeleteAccountFlow.tsx b/apps/citizen-pwa/src/components/DeleteAccountFlow.tsx
index b587fbbc..c8ffe806 100644
--- a/apps/citizen-pwa/src/components/DeleteAccountFlow.tsx
+++ b/apps/citizen-pwa/src/components/DeleteAccountFlow.tsx
@@ -1,4 +1,4 @@
-import { useState, useCallback } from 'react'
+import { useState, useCallback, useEffect, useRef } from 'react'
import { requestDataErasureAndSignOut } from '../services/erasure.js'
interface Props {
@@ -11,6 +11,7 @@ export function DeleteAccountFlow({ onGoodbye }: Props) {
const [step, setStep] = useState
('idle')
const [typed, setTyped] = useState('')
const [error, setError] = useState(null)
+ const confirmInputRef = useRef(null)
const handleConfirm = useCallback(() => {
void (async () => {
@@ -32,64 +33,148 @@ export function DeleteAccountFlow({ onGoodbye }: Props) {
setError(null)
}, [])
+ // Escape key closes dialog (but not while submitting)
+ useEffect(() => {
+ if (step === 'idle' || step === 'submitting') return
+ function onKey(e: KeyboardEvent) {
+ if (e.key === 'Escape') goIdle()
+ }
+ document.addEventListener('keydown', onKey)
+ return () => {
+ document.removeEventListener('keydown', onKey)
+ }
+ }, [step, goIdle])
+
+ // Focus the confirmation input when entering confirm step
+ useEffect(() => {
+ if (step === 'confirm') {
+ confirmInputRef.current?.focus()
+ }
+ }, [step])
+
if (step === 'idle') {
return (
)
}
- if (step === 'warn') {
- return (
-
-
Delete your account?
-
This will permanently:
-
- - Remove your name, contact info, and account
- - Anonymize your reports (they remain as public record)
- - Sign you out immediately
-
-
This cannot be undone. Your request will be reviewed before deletion is complete.
-
-
-
- )
- }
-
return (
-
-
Are you sure?
-
-
{
- setTyped(e.target.value)
- }}
- autoComplete="off"
- />
- {error &&
{error}
}
-
-
+
{
+ if (step !== 'submitting' && e.target === e.currentTarget) goIdle()
+ }}
+ >
+ {step === 'warn' ? (
+
+
+ Delete your account?
+
+
This will permanently:
+
+ - Remove your name, contact info, and account
+ - Anonymize your reports (they remain as public record)
+ - Sign you out immediately
+
+
+ This cannot be undone. Your request will be reviewed before deletion is complete.
+
+
+
+
+
+
+ ) : (
+
+
+ Are you sure?
+
+
+
{
+ setTyped(e.target.value)
+ }}
+ autoComplete="off"
+ className="w-full h-12 rounded-xl border-2 border-surface-200 px-4 text-sm font-mono focus:border-danger-500 focus:outline-none mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ )}
)
}
diff --git a/apps/citizen-pwa/src/components/FeedTab.tsx b/apps/citizen-pwa/src/components/FeedTab.tsx
index 56ba4430..be25ae41 100644
--- a/apps/citizen-pwa/src/components/FeedTab.tsx
+++ b/apps/citizen-pwa/src/components/FeedTab.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react'
-import { CheckCircle, MapPin } from 'lucide-react'
+import { MapPin, Info } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { usePublicIncidents } from '../hooks/usePublicIncidents.js'
import { incidentIcon, incidentLabel } from '../utils/incident-meta.js'
@@ -16,12 +16,12 @@ function timeAgo(timestamp: number): string {
function severityBadgeClass(severity: string): string {
if (severity === 'high')
- return 'px-2 py-0.5 rounded-full text-[10px] font-semibold bg-red-100 text-red-800'
+ return 'px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-800'
if (severity === 'medium')
- return 'px-2 py-0.5 rounded-full text-[10px] font-semibold bg-orange-50 text-orange-800'
+ return 'px-2 py-0.5 rounded-full text-xs font-semibold bg-orange-50 text-orange-800'
if (severity === 'low')
- return 'px-2 py-0.5 rounded-full text-[10px] font-semibold bg-surface-100 text-surface-700'
- return 'px-2 py-0.5 rounded-full text-[10px] font-semibold bg-blue-50 text-blue-900'
+ return 'px-2 py-0.5 rounded-full text-xs font-semibold bg-surface-100 text-surface-700'
+ return 'px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-50 text-blue-900'
}
function FeedCard({ incident, onTap }: { incident: PublicIncident; onTap: () => void }) {
@@ -45,7 +45,7 @@ function FeedCard({ incident, onTap }: { incident: PublicIncident; onTap: () =>
{label}
-
+
{timeAgo(incident.submittedAt)}
@@ -76,11 +76,11 @@ function SkeletonCard() {
return (
@@ -124,7 +124,7 @@ export function FeedTab() {
}}
className={
filters.severity === value
- ? 'bg-[#0f9488] text-white rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
+ ? 'bg-[#001e40] text-white rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
: 'bg-[#f0f4f4] text-[#5e6667] rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
}
>
@@ -145,7 +145,7 @@ export function FeedTab() {
}}
className={
filters.window === value
- ? 'bg-[#0f9488] text-white rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
+ ? 'bg-[#001e40] text-white rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
: 'bg-[#f0f4f4] text-[#5e6667] rounded-full px-3 py-1.5 text-xs font-medium flex-shrink-0 border-none cursor-pointer'
}
>
@@ -178,13 +178,13 @@ export function FeedTab() {
aria-atomic="true"
className="flex flex-col items-center justify-center min-h-[50vh] text-[#768081] px-4"
>
-
-
+
+
- All clear
+ No incidents
No incidents reported in the selected time window.
-
+
Walang naiulat na insidente sa panahong ito.
diff --git a/apps/citizen-pwa/src/components/LookupScreen.tsx b/apps/citizen-pwa/src/components/LookupScreen.tsx
index 8e827e42..36a1946a 100644
--- a/apps/citizen-pwa/src/components/LookupScreen.tsx
+++ b/apps/citizen-pwa/src/components/LookupScreen.tsx
@@ -1,7 +1,12 @@
import { useState } from 'react'
import { httpsCallable } from 'firebase/functions'
import { ArrowLeft } from 'lucide-react'
-import { fns, hasFirebaseConfig, FIREBASE_ENV_ERROR_MESSAGE } from '../services/firebase.js'
+import {
+ fns,
+ hasFirebaseConfig,
+ ensureSignedIn,
+ FIREBASE_ENV_ERROR_MESSAGE,
+} from '../services/firebase.js'
interface LookupResult {
status: string
@@ -50,6 +55,7 @@ export function LookupScreen() {
if (!hasFirebaseConfig()) {
throw new Error(FIREBASE_ENV_ERROR_MESSAGE)
}
+ await ensureSignedIn()
const res = await httpsCallable(
fns(),
'requestLookup',
@@ -170,6 +176,15 @@ export function LookupScreen() {
{`${result.municipalityLabel} MDRRMO`}
+
)}
diff --git a/apps/citizen-pwa/src/components/MapTab/FilterBar.tsx b/apps/citizen-pwa/src/components/MapTab/FilterBar.tsx
index 6c192f8b..73324543 100644
--- a/apps/citizen-pwa/src/components/MapTab/FilterBar.tsx
+++ b/apps/citizen-pwa/src/components/MapTab/FilterBar.tsx
@@ -17,7 +17,7 @@ interface Props {
const SEVERITIES: { value: SeverityFilter; label: string; dot?: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'high', label: 'High', dot: '#dc2626' },
- { value: 'medium', label: 'Medium', dot: '#a73400' },
+ { value: 'medium', label: 'Medium', dot: '#7c3500' },
{ value: 'low', label: 'Low', dot: '#414849' },
]
diff --git a/apps/citizen-pwa/src/components/MapTab/IncidentLayer.tsx b/apps/citizen-pwa/src/components/MapTab/IncidentLayer.tsx
index f47159bd..58d695ad 100644
--- a/apps/citizen-pwa/src/components/MapTab/IncidentLayer.tsx
+++ b/apps/citizen-pwa/src/components/MapTab/IncidentLayer.tsx
@@ -10,7 +10,7 @@ interface Props {
onPinTap: (incident: PublicIncident) => void
}
-const COLORS = { high: '#dc2626', medium: '#a73400', low: '#414849' } as const
+const COLORS = { high: '#dc2626', medium: '#7c3500', low: '#414849' } as const
function isValidCoordinate(lat: number, lng: number): boolean {
return (
@@ -29,7 +29,7 @@ function makeIcon(color: string, pulse: boolean): L.DivIcon {
iconSize: [44, 44],
iconAnchor: [22, 22],
html: `
-
+
${
pulse
? `
`
diff --git a/apps/citizen-pwa/src/components/MapTab/MyReportLayer.tsx b/apps/citizen-pwa/src/components/MapTab/MyReportLayer.tsx
index 567690fb..6c08c778 100644
--- a/apps/citizen-pwa/src/components/MapTab/MyReportLayer.tsx
+++ b/apps/citizen-pwa/src/components/MapTab/MyReportLayer.tsx
@@ -28,7 +28,7 @@ function makeIcon(color: string, queued: boolean): L.DivIcon {
iconSize: [44, 44],
iconAnchor: [22, 22],
html: `
-
+
${
queued
diff --git a/apps/citizen-pwa/src/components/MapTab/index.tsx b/apps/citizen-pwa/src/components/MapTab/index.tsx
index 02babd1a..8d8d1920 100644
--- a/apps/citizen-pwa/src/components/MapTab/index.tsx
+++ b/apps/citizen-pwa/src/components/MapTab/index.tsx
@@ -4,6 +4,7 @@ import { Crosshair } from 'lucide-react'
import L from 'leaflet'
import { PeekSheet } from './PeekSheet.js'
import { DetailSheet } from './DetailSheet.js'
+import { FilterBar } from './FilterBar.js'
import { IncidentLayer } from './IncidentLayer.js'
import { MyReportLayer } from './MyReportLayer.js'
import { usePublicIncidents } from '../../hooks/usePublicIncidents.js'
@@ -27,6 +28,12 @@ const INCIDENT_LABELS: Record
= {
other: 'Others',
}
+const WINDOW_LABELS: Record = {
+ '24h': '24 hours',
+ '7d': '7 days',
+ '30d': '30 days',
+}
+
interface SelectedPin {
id: string
type: 'incident' | 'myReport'
@@ -54,7 +61,7 @@ export function MapTab() {
const mapRef = useRef(null)
const [mapInstance, setMapInstance] = useState(null)
const [isOffline, setIsOffline] = useState(() => !navigator.onLine)
- const filters = { severity: 'all', window: '24h' } as const
+ const [filters, setFilters] = useState({ severity: 'all', window: '24h' })
const [selectedPin, setSelectedPin] = useState(null)
const [sheetPhase, setSheetPhase] = useState<'hidden' | 'peek' | 'expanded'>('hidden')
@@ -191,10 +198,22 @@ export function MapTab() {
visibleIncidents.length === 0 &&
myReports.length === 0
+ const showFilterHint =
+ !incidentsLoading &&
+ !incidentsError &&
+ visibleIncidents.length === 0 &&
+ (myReports.length > 0 || filters.severity !== 'all')
+
return (
+
+
{mapInstance ? (
<>
- {showEmpty ? (
+ {showEmpty || showFilterHint ? (
- No reported incidents in this area in the last {filters.window}.
+ {filters.severity !== 'all'
+ ? `No ${filters.severity} incidents reported in this area in the last ${WINDOW_LABELS[filters.window]}. Try clearing the severity filter.`
+ : showFilterHint
+ ? `No reported incidents in this area in the last ${WINDOW_LABELS[filters.window]}.`
+ : `No reported incidents in this area in the last ${WINDOW_LABELS[filters.window]}.`}
) : null}
diff --git a/apps/citizen-pwa/src/components/ProfileTab.tsx b/apps/citizen-pwa/src/components/ProfileTab.tsx
index 2fd448ea..e70c21f1 100644
--- a/apps/citizen-pwa/src/components/ProfileTab.tsx
+++ b/apps/citizen-pwa/src/components/ProfileTab.tsx
@@ -232,7 +232,7 @@ function ReportCard({ report, onTap }: { report: MyReport; onTap: () => void })
/* ── Skeleton card ── */
function SkeletonCard() {
return (
-
+
)
@@ -248,6 +248,25 @@ export function Step2WhoWhere({
) : null}
+ {locationMethod === 'gps' && !gpsLoading && !location ? (
+
+ {locationError &&
{locationError}
}
+
+
+ ) : null}
+
{locationMethod === 'gps' && location ? (
diff --git a/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx b/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx
index 2f4f3f4d..95e5fb21 100644
--- a/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx
+++ b/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx
@@ -84,7 +84,7 @@ export function Step3Review({
)
@@ -125,7 +125,7 @@ export function Step3Review({
{/* Type row */}
-
+
@@ -139,7 +139,7 @@ export function Step3Review({
{/* Patient count row */}
{reportData.patientCount > 0 && (
-
+
@@ -153,7 +153,7 @@ export function Step3Review({
)}
{/* Location row */}
-
+
@@ -174,7 +174,7 @@ export function Step3Review({
{/* Contact row */}
-
+
@@ -190,7 +190,7 @@ export function Step3Review({
{/* Consent */}
-
+