Professional Green Collectors, one trash at a time. Malinis na barangay, powered by neighbors.
A mobile-first platform for a professional green-collector workforce. Posters photograph and post trash-pickup requests with a peso bounty; verified, rated Green Collectors find nearby jobs, clean up, upload proof, and get paid; a shared Community feed surfaces events and news. Every collector carries a derived, tamper-proof credential — verified badge, rating, pickups completed, and a career ladder (Verified → Certified → Top-Rated → Team Lead → Barangay Coordinator). Everything syncs live across devices.
Built with React 19 + Vite, styled with Tailwind CSS (via CDN), with maps from Leaflet / react-leaflet and a full Supabase backend (Postgres, Auth, Storage, Realtime). Designed for a 430px max-width mobile viewport.
- Three modes, one app — switch between Post, Collect, and Community from the top bar.
- Real authentication — email/password sign-up and login via Supabase Auth, with email confirmation and password recovery (branded HTML emails).
- Request lifecycle —
open→accepted→collected→paid, with the right action buttons surfaced per status and role. - Photo uploads — before/after photos and post images upload to Supabase Storage (JPEG/PNG, ≤5 MB) and are served as public URLs.
- Maps & location — pick a request location on a map (reverse-geocoded label + geohash), and browse open jobs on a map.
- Live collector tracking — once a job is accepted, the collector's location streams in real time; the poster sees a proximity status ("on the way" → "nearby" → "arrived") and a live map.
- In-app chat — poster and collector message each other per request, in real time.
- Auto-priced by trash type — Biodegradable (₱20), Recyclable (₱30), Residual (₱50).
- Likes, ratings & leaderboard — like requests and community posts, rate completed jobs, and rank collectors by completed work.
- Realtime everywhere — requests, feed, chat, and tracking all update instantly via Supabase Realtime (Postgres change subscriptions). Open two devices and watch updates propagate.
- Node.js 18+ and npm
- A Supabase project (free tier is fine)
Copy .env.example to .env and fill in your project's values (Supabase Dashboard → Project Settings → API):
cp .env.example .envVITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-hereThese are read by src/lib/supabase.js to create the browser client. In Vercel/Netlify, set them as environment variables in the hosting dashboard — never commit real values.
npm install
npm run dev # start the Vite dev server with HMRThen open the printed local URL and sign up for an account on the auth screen (or log in if you already have one).
npm run build # production build to dist/
npm run preview # serve the production build locally
npm run lint # run ESLint over the repo
npm run db:backup # export all table data to a timestamped SQL file in backups/There is no test runner configured.
playwrightis installed as a devDependency, but no test suite exists yet. Standalone Node scripts inscripts/verify-*.mjssmoke-test realtime sync against a live project.
The app talks directly to Supabase from the browser using the anon key; Row-Level Security (RLS) is enabled on every table and is the real authorization boundary.
| Table | Purpose |
|---|---|
profiles |
One row per auth user — name, default_role (poster/collector), avatar. |
requests |
Trash-pickup requests: photo, location (lat/lng/label/geohash), tags, price, status, rating, after_photo_url, poster_id, collected_by. |
request_likes |
Join table (request_id, user_id) of who liked a request. |
posts |
Community feed entries — type is event, news, or post. |
post_likes |
Join table (post_id, user_id) of who liked a post. |
messages |
Per-request chat (request_id, sender_id, text, sent_at). |
collector_locations |
Live collector position per accepted job (request_id, lat, lng). |
trash-photos— request "before" photosafter-photos— proof-of-cleanup photospost-photos— community post images
Sign-up/login use Supabase Auth (email + password). Sign-up stores name and default_role in user metadata; a new profiles row is created on confirmation. Confirmation and password-recovery emails use the branded HTML templates in email-templates/, applied to the project with scripts/apply-email-templates.mjs. Template customization requires a custom SMTP provider (Resend) configured in the Supabase dashboard.
Deployed on Vercel (vercel.json): npm run build → dist/, with an SPA rewrite so all routes serve index.html.
App.jsx drives a three-state machine via appState:
loading → auth → app
(LoadingScreen) (AuthScreen) (PosterView | CollectorView | FeedView)
On load it checks for an existing Supabase session; if found, it fetches the user's profile and goes straight to the app. Once in the app, role ('poster' | 'collector' | 'community') chooses which view renders; TopBar switches the role.
App.jsx holds the high-level state (current user, profiles, role) and every mutator — addRequest, updateStatus, handleAfterPhoto, handleLike, handleRate, addPost, handlePostLike. Each mutator writes to Supabase; the UI updates when the corresponding realtime subscription echoes the change back. Read state comes from hooks, not props-drilled state.
- useRequests — fetches all requests (+ likes) once, then subscribes to
INSERT/UPDATEonrequestsandINSERT/DELETEonrequest_likes, keeping the list live. It also surfaces a connection status soAppcan show a "reconnecting" toast. - useFeed — same pattern for
postsandpost_likes. - ChatDrawer and CollectorTracker each open their own per-request channel for
messagesandcollector_locations.
Each hook maps DB rows (snake_case) to the app's camelCase shape in a dbToApp function.
The status field advances through:
| Status | Set when… |
|---|---|
open |
Poster creates the request |
accepted |
Collector takes the job (sets collected_by) |
collected |
Collector uploads an after-photo (Supabase Storage → public URL) |
paid |
Poster confirms payment |
StatusBadge.jsx maps both status values and trash categories to colors/labels; lib/tagColors.js is the canonical tag→color map. TrashCard.jsx renders the correct action buttons for each status + viewer-role combination.
While a collector has an active job, CollectorView polls navigator.geolocation and upserts into collector_locations. The poster's CollectorTracker subscribes to that row and uses utils/haversine.js to convert distance into a friendly proximity label and a live Leaflet map.
src/
├── App.jsx # session/role state + Supabase mutators + app-state machine
├── main.jsx # React entry point
├── index.css # global resets + 430px centered shell
├── lib/
│ ├── supabase.js # createClient from VITE_SUPABASE_* env vars
│ ├── tagColors.js # canonical tag → color/label map
│ └── validateImage.js # client-side upload guard (JPEG/PNG, ≤5 MB)
├── hooks/
│ ├── useRequests.js # requests + request_likes, fetch + realtime
│ └── useFeed.js # posts + post_likes, fetch + realtime
├── utils/
│ └── haversine.js # distance + proximity labels for tracking
└── components/
├── AuthScreen.jsx # Supabase email/password login + signup + confirm
├── LoadingScreen.jsx # splash; checks for existing session on finish
├── TopBar.jsx # header + Post/Collect/Community toggle + logout
├── PosterView.jsx # Poster home (create + manage own requests)
├── CollectorView.jsx # Collector home (browse/accept jobs, stream location)
├── FeedView.jsx # Community feed
├── PostForm.jsx # create a trash request
├── CreatePostForm.jsx # create a community post
├── PostCard.jsx # community feed card
├── TrashCard.jsx # request card + per-status action buttons
├── StatusBadge.jsx # status & category → color/label mapping
├── MapView.jsx # Leaflet map of requests / collector marker
├── LocationPicker.jsx # tap-to-pin map + geolocation for new requests
├── CollectorTracker.jsx # live collector position + proximity (poster side)
├── ChatDrawer.jsx # realtime per-request chat
├── Leaderboard.jsx # ranks collectors by completed jobs
├── ConfirmModal.jsx # confirmation dialog
├── SuccessModal.jsx # success feedback dialog
└── Toast.jsx # transient status/connection messages
scripts/
├── apply-email-templates.mjs # push branded auth emails via the Management API
├── db-backup.mjs # logical data backup (service-role key)
└── verify-*.mjs # realtime smoke tests (requests / feed / chat)
email-templates/ # confirmation.html, recovery.html
Tailwind is loaded via a CDN <script> in index.html (no PostCSS/Tailwind build step), with a custom DM Sans font config inlined there. Components mix Tailwind utility classes with inline style={{}} objects for the specific color palette (e.g. #f3f4f2 background, category colors). Global resets and the 430px centered shell live in src/index.css.
The browser holds the Supabase anon key, so RLS policies are the only thing protecting data — keep them tight. The db:backup script and apply-email-templates script use privileged keys (service-role / personal access token) and must only run locally with secrets kept out of the repo. backups/ and .env are gitignored.