-
Notifications
You must be signed in to change notification settings - Fork 0
feat(citizen-pwa): all 6 screens redesign — feed, profile, alerts, map, report form, RevealSheet #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat(citizen-pwa): all 6 screens redesign — feed, profile, alerts, map, report form, RevealSheet #85
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
742a06e
docs(phase9): Phase 9 pilot-deployable milestone design spec
Exc1D b726189
docs(phase9): clarify PAGASA webhook deferral in Section 8
Exc1D afe1b72
docs(design): add PRODUCT.md, DESIGN.md, DESIGN.json for citizen PWA\…
Exc1D d457df5
feat(citizen-pwa): report form polish â fix border rule, add Tagalo…
Exc1D 58a73bb
feat(citizen-pwa): map tab redesign â chip filters, richer peek/det…
Exc1D 9e8de8e
feat(citizen-pwa): feed tab, incident detail page, useIncident hook
Exc1D e845887
feat(citizen-pwa): profile tab — my reports list + privacy section
Exc1D 46fc96d
feat(citizen-pwa): alerts tab + useAlerts hook
Exc1D 07dc24e
feat(citizen-pwa): RevealSheet polish — slide-up animation, scroll gu…
Exc1D 43da045
docs: update progress + learnings for citizen PWA redesign
Exc1D 21db95c
fix(shared-firebase): add optional onError callback to subscribeAlerts
Exc1D fb44e49
fix(citizen-pwa): add explicit error state to useAlerts hook
Exc1D ea7f714
fix(citizen-pwa): tighten isPublicIncidentData guard and add cancella…
Exc1D 7d4eab3
fix(citizen-pwa): accurate CTA copy in Step2WhoWhere
Exc1D 0c6e97c
test(citizen-pwa): add direct route test for /incidents/:id
Exc1D ff89323
fix(design): correct refersTo mappings in DESIGN.json
Exc1D aa863b6
fix(design): reconcile elevation rules and remove legacy stripe refer…
Exc1D bd2dcbf
docs(product): remove accidental Register stub
Exc1D 348a215
docs(spec): add language identifiers to fenced code blocks
Exc1D 5b16624
Merge branch 'main' into feat/citizen-pwa-redesign
Exc1D 5e5372f
docs(citizen-pwa): add redesign design spec — polish + spec gaps + ce…
Exc1D c352115
docs(responder-app): add redesign design spec â shell + ceremony la…
Exc1D d47a30f
feat(citizen-pwa): implement all 18 redesign tasks â design complia…
Exc1D 51f7539
fix(coderabbit): address PR #85 review comments
Exc1D 6804d7e
docs: fix DESIGN.json badge size + markdown lint in specs/plans
Exc1D File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Product | ||
|
|
||
| ## Register | ||
|
|
||
| product | ||
|
|
||
| ## Users | ||
|
|
||
| Citizens of Camarines Norte, Philippines. Reporting during active emergencies — flood water rising, a landslide just happened, they smell smoke. Time-pressured, scared, often in motion. Tech literacy ranges from first-time smartphone users to daily app users; the interface must work for both without condescending to either. Mobile-first: iOS Safari and Android Chrome on mid-range to low-end Android devices. Connectivity is unreliable during the exact moments the app matters most. Many will report without creating an account — pseudonymous use is supported and respected, not treated as a lesser experience. | ||
|
|
||
| ## Product Purpose | ||
|
|
||
| Bantayog Alert gives citizens the fastest path from "I see an emergency" to "I've reported it and I'm being heard." Secondary jobs: stay spatially aware of what's happening nearby, browse the public incident feed, receive official government alerts, and track the status of their own reports. Success looks like a report submitted under 60 seconds, a citizen who feels their report mattered, and a community better coordinated during a crisis. | ||
|
|
||
| ## Brand Personality | ||
|
|
||
| Caring, Assuring, Urgent. | ||
|
|
||
| The app speaks the way a calm, trusted neighbor would during a crisis — it does not panic, it does not waste words, it does not decorate. It moves with purpose. Every interaction should leave the user feeling: "I was heard, I am safe, I know what happens next." | ||
|
|
||
| ## References | ||
|
|
||
| - **Apple.com** — for cleanliness: generous whitespace, precise typography, nothing that doesn't earn its place on the screen. | ||
| - **Grab mobile** — for efficiency: task-focused flows, obvious primary actions, no detours between intent and completion. | ||
| - **Facebook feed** — for the Feed tab only: familiar infinite scroll, card rhythm, recognizable interaction patterns citizens already know. | ||
| - **Google Maps** — for the Map tab: spatial clarity, intuitive pin interactions, minimal chrome around the map surface itself. | ||
|
|
||
| ## Anti-references | ||
|
|
||
| Shopping apps (Shopee, Lazada aesthetic): cluttered grids, promotional banners, badge inflation, competing CTAs, anything that optimizes for attention over task completion. This app is used during emergencies. Visual noise costs lives. | ||
|
|
||
| ## Design Principles | ||
|
|
||
| 1. **Clarity under pressure.** Every screen must work at a glance for someone who is scared, moving, and has 4% battery. If a screen requires reading to understand, it has failed. | ||
| 2. **Speed is care.** The fastest path to a submitted report is the most caring UX. Fewer taps, fewer decisions, fewer chances to abandon. Urgency and care are the same thing here. | ||
| 3. **Trust through calm.** The UI must never feel chaotic, even when reporting chaos. Calm visual weight, unambiguous status, no jank. A panicked user needs an unshakeable interface. | ||
| 4. **Familiar over clever.** When a pattern is already trusted (Maps-style pins, Feed-style cards, Apple-style form fields), use it. Novel UX patterns are a liability in a crisis. | ||
| 5. **Inclusive by default.** WCAG 2.1 AAA on report submission and alerts. Colorblind-safe status indicators. Legible at arm's length in rain. Subtle Tagalog translations on any sentence that could confuse a first-time user. | ||
|
|
||
| ## Accessibility & Inclusion | ||
|
|
||
| - **WCAG 2.1 AAA** on critical paths: report submission flow, alerts tab, and any error or status message. | ||
| - **WCAG 2.1 AA** minimum on all other surfaces. | ||
| - Colorblind-safe status palette (never rely on red/green alone — pair with icons and labels). | ||
| - Touch targets minimum 44×44px; prefer 48px on interactive elements in the report flow. | ||
| - Reduced-motion: all animations must respect `prefers-reduced-motion`. | ||
| - Tagalog subtitles or inline translations on complex English sentences (legal disclosures, privacy notices, error explanations). Inline, subtle — not a separate language toggle. | ||
| - Tested on low-end Android (Chrome) at 110% system font scale. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| import { useAlerts } from '../hooks/useAlerts.js' | ||
| import type { AlertDoc } from '@bantayog/shared-types' | ||
|
|
||
| const SEVERITY_ORDER: Record<string, number> = { | ||
| critical: 0, | ||
| high: 1, | ||
| medium: 2, | ||
| low: 3, | ||
| info: 4, | ||
| } | ||
|
|
||
| function severityMeta(severity: string): { label: string; bg: string; color: string } { | ||
| switch (severity) { | ||
| case 'critical': | ||
| return { label: 'CRITICAL', bg: '#fecaca', color: '#7f1d1d' } | ||
| case 'high': | ||
| return { label: 'HIGH', bg: '#fee2e2', color: '#991b1b' } | ||
| case 'medium': | ||
| return { label: 'MEDIUM', bg: '#fff5ef', color: '#a73400' } | ||
| case 'low': | ||
| return { label: 'LOW', bg: '#e0e7f0', color: '#001e40' } | ||
| default: | ||
| return { label: 'INFO', bg: '#dbeafe', color: '#1e40af' } | ||
| } | ||
| } | ||
|
|
||
| function severityIcon(severity: string): string { | ||
| switch (severity) { | ||
| case 'critical': | ||
| case 'high': | ||
| return '🚨' | ||
| case 'medium': | ||
| return '⚠️' | ||
| default: | ||
| return 'ℹ️' | ||
| } | ||
| } | ||
|
|
||
| function timeAgo(ts: number): string { | ||
| const minutes = Math.floor((Date.now() - ts) / 60000) | ||
| if (minutes < 1) return 'just now' | ||
| if (minutes < 60) return `${String(minutes)}m ago` | ||
| const hours = Math.floor(minutes / 60) | ||
| if (hours < 24) return `${String(hours)}h ago` | ||
| return `${String(Math.floor(hours / 24))}d ago` | ||
| } | ||
|
|
||
| function AlertCard({ alert }: { alert: AlertDoc }) { | ||
| const { label, bg, color } = severityMeta(alert.severity) | ||
| const icon = severityIcon(alert.severity) | ||
| const isCritical = alert.severity === 'critical' || alert.severity === 'high' | ||
|
|
||
| return ( | ||
| <div | ||
| className="card" | ||
| style={{ | ||
| marginBottom: '0.5rem', | ||
| padding: '0.875rem', | ||
| ...(isCritical ? { background: '#fffbfb', border: '1px solid #fecaca' } : {}), | ||
| }} | ||
| > | ||
| <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}> | ||
| <span aria-hidden="true" style={{ fontSize: '1.25rem', flexShrink: 0, lineHeight: 1.3 }}> | ||
| {icon} | ||
| </span> | ||
| <div style={{ flex: 1, minWidth: 0 }}> | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'flex-start', | ||
| gap: 8, | ||
| }} | ||
| > | ||
| <p | ||
| style={{ | ||
| margin: 0, | ||
| fontWeight: 700, | ||
| fontSize: '0.9375rem', | ||
| color: '#001e40', | ||
| lineHeight: 1.3, | ||
| }} | ||
| > | ||
| {alert.title} | ||
| </p> | ||
| <span style={{ flexShrink: 0, fontSize: '0.6875rem', color: '#7b8794' }}> | ||
| {timeAgo(alert.publishedAt)} | ||
| </span> | ||
| </div> | ||
| <p | ||
| style={{ | ||
| margin: '4px 0 8px', | ||
| fontSize: '0.8125rem', | ||
| color: '#374151', | ||
| lineHeight: 1.5, | ||
| }} | ||
| > | ||
| {alert.body} | ||
| </p> | ||
| <span | ||
| style={{ | ||
| display: 'inline-block', | ||
| padding: '2px 8px', | ||
| borderRadius: 999, | ||
| fontSize: '0.625rem', | ||
| fontWeight: 700, | ||
| letterSpacing: '0.05em', | ||
| background: bg, | ||
| color, | ||
| }} | ||
| > | ||
| {label} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| function SkeletonCard() { | ||
| return ( | ||
| <div | ||
| className="card" | ||
| style={{ animation: 'pulse 1.6s ease-in-out infinite', marginBottom: '0.5rem' }} | ||
| > | ||
| <div style={{ display: 'flex', gap: 10 }}> | ||
| <div | ||
| style={{ | ||
| width: 24, | ||
| height: 24, | ||
| borderRadius: '50%', | ||
| background: '#e0e3e5', | ||
| flexShrink: 0, | ||
| }} | ||
| /> | ||
| <div style={{ flex: 1 }}> | ||
| <div | ||
| style={{ | ||
| height: 14, | ||
| width: '65%', | ||
| background: '#e0e3e5', | ||
| borderRadius: 4, | ||
| marginBottom: 8, | ||
| }} | ||
| /> | ||
| <div | ||
| style={{ | ||
| height: 12, | ||
| width: '90%', | ||
| background: '#e0e3e5', | ||
| borderRadius: 4, | ||
| marginBottom: 6, | ||
| }} | ||
| /> | ||
| <div | ||
| style={{ | ||
| height: 12, | ||
| width: '70%', | ||
| background: '#e0e3e5', | ||
| borderRadius: 4, | ||
| marginBottom: 10, | ||
| }} | ||
| /> | ||
| <div style={{ height: 18, width: 60, background: '#e0e3e5', borderRadius: 999 }} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export function AlertsTab() { | ||
| const { alerts, loading } = useAlerts() | ||
|
|
||
| const sorted = [...alerts].sort( | ||
| (a, b) => | ||
| (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99) || | ||
| b.publishedAt - a.publishedAt, | ||
| ) | ||
|
|
||
| return ( | ||
| <div style={{ height: '100%', overflowY: 'auto', WebkitOverflowScrolling: 'touch' }}> | ||
| {/* Sticky header */} | ||
| <div | ||
| style={{ | ||
| position: 'sticky', | ||
| top: 0, | ||
| zIndex: 10, | ||
| background: 'rgba(247,249,251,0.96)', | ||
| backdropFilter: 'blur(12px)', | ||
| WebkitBackdropFilter: 'blur(12px)', | ||
| padding: '12px 16px 10px', | ||
| borderBottom: '1px solid #e5e7eb', | ||
| }} | ||
| > | ||
| <h1 style={{ margin: 0, fontSize: '1.125rem', fontWeight: 800, color: '#001e40' }}> | ||
| Alerts | ||
| <span | ||
| style={{ | ||
| display: 'block', | ||
| fontSize: '0.6875rem', | ||
| fontWeight: 400, | ||
| color: '#7b8794', | ||
| marginTop: 2, | ||
| }} | ||
| > | ||
| Mga babala at abiso para sa inyong lugar | ||
| </span> | ||
| </h1> | ||
| </div> | ||
|
|
||
| {/* Content */} | ||
| <div style={{ padding: '12px 16px 24px' }}> | ||
| {loading ? ( | ||
| <> | ||
| <SkeletonCard /> | ||
| <SkeletonCard /> | ||
| </> | ||
| ) : sorted.length === 0 ? ( | ||
| <div role="status" style={{ padding: '48px 16px', textAlign: 'center' }}> | ||
| <p style={{ fontSize: '2.5rem', margin: '0 0 8px' }}>✅</p> | ||
| <p | ||
| style={{ | ||
| margin: '0 0 6px', | ||
| fontWeight: 700, | ||
| color: '#001e40', | ||
| fontSize: '0.9375rem', | ||
| }} | ||
| > | ||
| No active alerts | ||
| </p> | ||
| <p style={{ margin: 0, fontSize: '0.8125rem', color: '#52606d' }}> | ||
| You will be notified when new alerts are issued. | ||
| <span | ||
| style={{ | ||
| display: 'block', | ||
| fontSize: '0.6875rem', | ||
| color: '#7b8794', | ||
| marginTop: 4, | ||
| fontStyle: 'italic', | ||
| }} | ||
| > | ||
| Ipaaalam sa inyo kung may bagong babala. | ||
| </span> | ||
| </p> | ||
| </div> | ||
| ) : ( | ||
| sorted.map((alert) => <AlertCard key={alert.id} alert={alert} />) | ||
| )} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.