Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions apps/citizen-pwa/public/sw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const CACHE_NAME = 'bantayog_shell_v1'
const CACHE_NAME = 'bantayog_shell_v2'
// WARNING: This must match the localforage instance name in
// apps/citizen-pwa/src/services/draft-store.ts ('bantayog-drafts').
// The SW uses raw IndexedDB; localforage wraps IndexedDB with its own
Expand All @@ -8,8 +8,24 @@ const CACHE_NAME = 'bantayog_shell_v1'
const DB_NAME = 'bantayog-drafts'
const DB_STORE = 'drafts'

// Precache the app shell so a cold offline navigation falls back to
// index.html instead of the browser's default offline page. Vite's hashed
// JS/CSS chunks are still cached opportunistically by the fetch handler.
const PRECACHE_URLS = [
'/',
'/index.html',
'/manifest.webmanifest',
'/icons/icon-192.png',
'/icons/icon-512.png',
]

self.addEventListener('install', (event) => {
self.skipWaiting()
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting()),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

self.addEventListener('activate', (event) => {
Expand Down Expand Up @@ -41,9 +57,17 @@ self.addEventListener('fetch', (event) => {
}
return response
})
.catch(() => {
.catch(async () => {
// Navigation requests (SPA routes) fall back to the cached shell so
// React Router can render the right view instead of the browser
// showing its default offline page.
if (event.request.mode === 'navigate') {
const shell = await caches.match('/index.html')
if (shell) return shell
}
if (event.request.method === 'GET') {
return caches.match(event.request)
const cached = await caches.match(event.request)
if (cached) return cached
}
throw new Error('not found')
}),
Expand Down
25 changes: 23 additions & 2 deletions apps/citizen-pwa/src/components/SubmitReportForm/Step1Evidence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Step1EvidenceProps {
onNext: (data: { reportType: string; photoFile: File | null }) => void
onBack: () => void
isSubmitting?: boolean
initialReportType?: string
}

const INCIDENT_TYPES = [
Expand Down Expand Up @@ -98,8 +99,17 @@ const INCIDENT_TYPES = [
},
] as const

export function Step1Evidence({ onNext, onBack, isSubmitting = false }: Step1EvidenceProps) {
const [reportType, setReportType] = useState('flood')
export function Step1Evidence({
onNext,
onBack,
isSubmitting = false,
initialReportType = '',
}: Step1EvidenceProps) {
// Default to empty so the user must actively pick a type. This is what
// gates the "Skip photo for now" path — without it, Skip silently
// submitted whatever the seeded default was.
const [reportType, setReportType] = useState(initialReportType)
const [reportTypeError, setReportTypeError] = useState<string | null>(null)
const [photoFile, setPhotoFile] = useState<File | null>(null)
const [photoError, setPhotoError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
Expand Down Expand Up @@ -170,6 +180,11 @@ export function Step1Evidence({ onNext, onBack, isSubmitting = false }: Step1Evi
}

const handleNext = () => {
if (!reportType) {
setReportTypeError('Please select an incident type to continue.')
return
}
setReportTypeError(null)
onNext({ reportType, photoFile })
}

Expand Down Expand Up @@ -293,6 +308,7 @@ export function Step1Evidence({ onNext, onBack, isSubmitting = false }: Step1Evi
type="button"
onClick={() => {
setReportType(value)
setReportTypeError(null)
}}
className={`flex flex-col items-center justify-center gap-2 min-h-[80px] rounded-xl border-2 transition-all active:scale-95 ${
isSelected
Expand All @@ -310,6 +326,11 @@ export function Step1Evidence({ onNext, onBack, isSubmitting = false }: Step1Evi
)
})}
</div>
{reportTypeError && (
<p role="alert" className="mt-2 text-xs text-danger-500 font-medium">
{reportTypeError}
</p>
)}
</div>
</div>

Expand Down
20 changes: 15 additions & 5 deletions apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,19 @@ export function Step2WhoWhere({ onNext, onBack, isSubmitting = false }: Step2Who
)}
</div>

{/* Bottom action */}
{locationMethod !== null && (
<div className="sticky bottom-0 z-float bg-surface-100/90 backdrop-blur-md border-t border-surface-200 px-5 py-4">
{/* Bottom action — always rendered so users always see what to do next.
Before a location method is picked, show a polite hint instead of
hiding the entire region (which previously felt like silent failure). */}
<div className="sticky bottom-0 z-float bg-surface-100/90 backdrop-blur-md border-t border-surface-200 px-5 py-4">
{locationMethod === null ? (
<p
role="status"
aria-live="polite"
className="text-center text-sm font-medium text-surface-700"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
>
Pick a location method above (GPS or Manual) to continue.
</p>
) : (
<Button
variant="primary"
fullWidth
Expand All @@ -318,8 +328,8 @@ export function Step2WhoWhere({ onNext, onBack, isSubmitting = false }: Step2Who
>
{isSubmitting ? 'Please wait...' : 'Review Report'}
</Button>
</div>
)}
)}
</div>
</div>
)
}
43 changes: 43 additions & 0 deletions apps/citizen-pwa/src/components/SubmitReportForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { normalizeMsisdn } from '@bantayog/shared-validators'
import type { ReportType } from '@bantayog/shared-types'
import { createDraft } from '../../services/submit-report.js'
import type { Draft } from '../../services/draft-store.js'
import { wizardSnapshot } from '../../services/wizard-snapshot.js'
import { useSubmissionMachine } from '../../hooks/useSubmissionMachine.js'
import { Step1Evidence } from './Step1Evidence.js'
import { Step2WhoWhere } from './Step2WhoWhere.js'
Expand Down Expand Up @@ -41,13 +42,46 @@ export function SubmitReportForm() {

function WizardContainer() {
const nav = useNavigate()
const [hasLoadedSnapshot, setHasLoadedSnapshot] = useState(false)
const [step, setStep] = useState<1 | 2 | 3>(1)
const [formData, setFormData] = useState<FormData>({ step1: null, step2: null })
const [draft, setDraft] = useState<Draft | null>(null)
const [secret, setSecret] = useState<string | null>(null)
const [isCreatingDraft, setIsCreatingDraft] = useState(false)
const [draftError, setDraftError] = useState<string | null>(null)

// Resume an in-progress wizard from a prior session (refresh, accidental close).
// photoFile is intentionally not persisted — File can't be reliably serialized;
// the user re-attaches if needed by going back to Step 1.
useEffect(() => {
let cancelled = false
void wizardSnapshot.load().then((snap) => {
if (cancelled) return
if (snap) {
setStep(snap.step)
setFormData({
step1: snap.step1 ? { reportType: snap.step1.reportType, photoFile: null } : null,
step2: snap.step2 ?? null,
})
}
setHasLoadedSnapshot(true)
})
return () => {
cancelled = true
}
}, [])
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Persist after load + on every step/formData change. The hasLoadedSnapshot
// gate prevents the initial empty state from clobbering a fresh resume.
useEffect(() => {
if (!hasLoadedSnapshot) return
void wizardSnapshot.save({
step,
step1: formData.step1 ? { reportType: formData.step1.reportType } : null,
step2: formData.step2,
})
}, [hasLoadedSnapshot, step, formData])

const handleStep1Next = (data: Step1Data) => {
setFormData((prev) => ({ ...prev, step1: data }))
setStep(2)
Expand Down Expand Up @@ -116,15 +150,23 @@ function WizardContainer() {
}

const handleStep1Back = () => {
// User abandoned the wizard from Step 1 — drop the snapshot so a fresh
// /report visit starts clean rather than resuming the old draft.
void wizardSnapshot.clear()
void nav('/')
}

if (!hasLoadedSnapshot) {
return <div className="min-h-[100dvh] bg-surface-100" aria-hidden="true" />
}

if (draft) {
return (
<SubmissionPanel
draft={draft}
secret={secret}
onSuccess={(publicRef) => {
void wizardSnapshot.clear()
void nav(`/reports/${publicRef}`)
}}
/>
Expand All @@ -137,6 +179,7 @@ function WizardContainer() {
onNext={handleStep1Next}
onBack={handleStep1Back}
isSubmitting={isCreatingDraft}
initialReportType={formData.step1?.reportType ?? ''}
/>
)
}
Expand Down
18 changes: 16 additions & 2 deletions apps/citizen-pwa/src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ export function LoginPage() {
const navigate = useNavigate()
const { show, message, type, toast } = useToast()
const [step, setStep] = useState<'phone' | 'otp'>('phone')
const [phone, setPhone] = useState('+63')
// Seed from sessionStorage so users who bounce between /login and /register
// (e.g. wrong-account → register flow) keep the phone they already typed.
const [phone, setPhone] = useState(() => {
try {
return sessionStorage.getItem('bantayog.last-phone') ?? '+63'
} catch {
return '+63'
}
})
const [otp, setOtp] = useState('')
const [loading, setLoading] = useState(false)
const [verificationId, setVerificationId] = useState<string | null>(null)
Expand Down Expand Up @@ -151,7 +159,13 @@ export function LoginPage() {
type="tel"
value={phone}
onChange={(e) => {
setPhone(e.target.value)
const next = e.target.value
setPhone(next)
try {
sessionStorage.setItem('bantayog.last-phone', next)
} catch {
// Private mode / quota / security errors — best effort persistence.
}
}}
placeholder="+63 XXX XXX XXXX"
className="w-full pl-10 pr-4 py-3 border border-surface-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none"
Expand Down
30 changes: 26 additions & 4 deletions apps/citizen-pwa/src/pages/RegisterPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ export function RegisterPage() {
const isResume = searchParams.get('resume') === 'registration'

const [step, setStep] = useState<Step>(isResume ? 'consent' : 'phone')
const [phone, setPhone] = useState('+63')
// Seed from sessionStorage so users who bounce here from /login (or back)
// do not have to retype the +63 prefix. The resume effect below still
// overrides this with `currentUser.phoneNumber` when applicable.
const [phone, setPhone] = useState(() => {
if (isResume) return '+63'
try {
return sessionStorage.getItem('bantayog.last-phone') ?? '+63'
} catch {
return '+63'
}
})
const [otp, setOtp] = useState('')
const [displayName, setDisplayName] = useState('')
const [consentGiven, setConsentGiven] = useState(false)
Expand Down Expand Up @@ -165,14 +175,26 @@ export function RegisterPage() {

{step === 'phone' && (
<div>
<p className="mb-4 text-[0.9375rem] font-semibold text-[#001e40]">
<label
htmlFor="register-phone"
className="mb-4 text-[0.9375rem] font-semibold text-[#001e40] block"
>
Enter your phone number
</p>
</label>
<input
id="register-phone"
name="phone"
autoComplete="tel"
type="tel"
value={phone}
onChange={(e) => {
setPhone(e.target.value)
const next = e.target.value
setPhone(next)
try {
sessionStorage.setItem('bantayog.last-phone', next)
} catch {
// Private mode / quota / security errors — best effort persistence.
}
}}
placeholder="+63XXXXXXXXXX"
className="w-full h-14 rounded-xl border border-[#d5dedd] px-4 text-base focus:border-[#0f9488] focus:outline-none mb-4"
Expand Down
18 changes: 9 additions & 9 deletions apps/citizen-pwa/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function SettingsPage() {
}

return (
<div className="min-h-[100dvh] bg-[#f0f4f4]">
<main id="main-content" className="min-h-[100dvh] bg-[#f0f4f4]">
{/* Back header */}
<div className="flex items-center gap-3 px-4 py-4 border-b bg-white border-[#d5dedd]">
<button
Expand All @@ -132,7 +132,7 @@ export function SettingsPage() {
</div>

{/* Notifications section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Notifications
</p>
<div className="bg-white divide-y divide-[#f0f4f4]">
Expand Down Expand Up @@ -165,7 +165,7 @@ export function SettingsPage() {
</div>

{/* Location section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Location
</p>
<div className="bg-white">
Expand All @@ -180,7 +180,7 @@ export function SettingsPage() {
</div>

{/* Offline Mode section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Offline Mode
</p>
<div className="bg-white">
Expand All @@ -195,7 +195,7 @@ export function SettingsPage() {
</div>

{/* Storage section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Storage
</p>
<div className="bg-white">
Expand All @@ -205,7 +205,7 @@ export function SettingsPage() {
</div>

{/* Account section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Account
</p>
<div className="bg-white divide-y divide-[#f0f4f4]">
Expand All @@ -218,7 +218,7 @@ export function SettingsPage() {
>
Download my data
</button>
<span className="text-xs text-[#a3adae]">
<span className="text-xs text-surface-500">
Receive a JSON export of your profile, reports, and media.
</span>
</div>
Expand All @@ -236,7 +236,7 @@ export function SettingsPage() {
</div>

{/* Danger Zone section */}
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-[#768081]">
<p className="text-xs font-semibold uppercase tracking-wider px-4 pt-6 pb-2 text-surface-600">
Danger Zone
</p>
<div className="bg-white divide-y divide-[#f0f4f4]">
Expand All @@ -249,6 +249,6 @@ export function SettingsPage() {
<div className="pb-8" />

<Toast show={show} message={message} type={type} />
</div>
</main>
)
}
Loading
Loading