Skip to content
HelpingPeopleNowPublic

About

Frontend for HelpingPeopleNow

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Helping People Frontend

Preact + Vite SPA served behind nginx. Dark-themed home-services platform where clients find workers and workers find jobs. Connects to the Go backend via API proxied through Traefik.

URL: https://helpingpeople.cloud | Container: helpingpeoplenow-frontend


Stack

Layer Technology
UI Preact (React-compatible, 3kB)
Routing preact-router v4
Build Vite + TypeScript
Runtime nginx:alpine (multi-stage Docker)
CI/CD GitHub Actions β†’ ghcr.io

Pages & Routes

Route Component Auth Required Description
/ LandingPage.tsx No Landing page β€” redirects to /chat if logged in
/login LoginPage.tsx No Magic-link login + signup form
/terms TermsPage.tsx No Terms and conditions (bilingual EN/ES)
/privacy PrivacyPage.tsx No Privacy policy (bilingual EN/ES)
/cookies CookiesPage.tsx No Cookie policy (bilingual EN/ES)
/profile/:slug PublicProfilePage.tsx No Public worker profile with hero, stats, bio, links, CTA
/chat ChatPage.tsx Yes AI chat β€” modes via ?mode= param: worker_intake, client_intake, search
/find FindPage.tsx Yes Find professionals β€” worker card grid
/inbox InboxPage.tsx Yes DM inbox with status indicators
/inbox/:convId DirectMessagePage.tsx Yes DM thread with send, block, report, archive
/workers/:workerId WorkerContactPage.tsx Yes Create/resume DM with a worker, redirects to inbox
/admin AdminPage.tsx Yes Admin menu β€” links to LLM provider, prompts, and entity CRUD pages
/admin/feedback FeedbackAdminPage.tsx Yes Admin feedback dashboard β€” triage user-submitted feedback

Architecture

Browser ──► Traefik (:80)
              β”‚
              β”œβ”€β”€ /api/v1/*           ──► Backend (:8081)
              β”‚                          (unified POST /api/v1/chat with mode + optional GPS coordinates in body,
              β”‚                           GET/DELETE /api/v1/{worker,client}/profile,
              β”‚                           GET/PUT /api/v1/system-prompts/,
              β”‚                           GET /api/v1/conversations,
              β”‚                           GET/POST/PATCH /api/v1/direct-messages/,
              β”‚                           GET /api/v1/workers/:id/contact,
              β”‚                           GET/PUT/DELETE /api/v1/admin/{entity}/)
              β”‚
              β”œβ”€β”€ /api/auth/*         ──► Auth Service (:8083) β€” magic-link + sessions
              β”‚
              β”œβ”€β”€ adminer.helpingpeople.cloud, grafana.helpingpeople.cloud, prometheus.helpingpeople.cloud β€” observability / DB UI
              β”‚
              └── /*                  ──► Frontend nginx β†’ SPA (index.html)
                 /health              ──► Frontend nginx β†’ 200 "ok"

The SPA uses client-side routing via preact-router. Public routes (/, /login, /terms, /privacy, /cookies, /profile/:slug) require no auth. All other routes are wrapped in ProtectedRoute which redirects to /login if no valid session.

API calls use relative URLs (/api/v1/..., /api/auth/...) which Traefik proxies to the appropriate backend service.

Health Check

The nginx config has a /health location block that returns 200 OK with body "ok" and has access_log off. It is used for Docker container healthchecks.

Endpoint Response Notes
GET /health 200 OK with body ok access_log off; no auth required

Auth Flow

  1. User visits /login β†’ enters email (or name + email for new accounts) β†’ auth service sends a magic link
  2. Clicking the magic link creates a session cookie (better-auth.session_token)
  3. AuthProvider checks the session on every route change
  4. ProtectedRoute wrapper redirects to /login if no valid session
  5. After login, user lands on LandingPage β†’ ModeChooser (or navigates to /chat?mode=...)

Cap CAPTCHA

  • Provider: Cap β€” open-source CAPTCHA service
  • Widget: <cap-widget> custom element embedded on LoginPage and PublicProfilePage (for the contact-CTA getContact flow)
  • Token delivery: capToken is obtained from the widget and sent:
    • In metadata.capToken when calling sendMagicLink (magic-link login/signup)
    • As a ?capToken= query parameter when calling GET /api/v1/workers/:id/contact (public profile CTA)
  • CSP requirements: The Content-Security-Policy in nginx.conf must allow:
    • script-src / worker-src: cap.helpingpeople.cloud and wasm-unsafe-eval
    • connect-src: cap.helpingpeople.cloud

Internationalization (i18n)

  • Spanish is the default language (configurable via LangToggle in sidebar)
  • Single i18n.ts file with translations, language context, useLanguage() hook, and toggle
  • All UI text uses t() from useLanguage() for translated strings
  • Chat requests include a lang parameter so the AI responds in the matching language

Analytics

  • A self-hosted Umami analytics script is injected in index.html (https://helpingpeople.cloud/umami/script.js). It tracks page views without cookies and does not collect personal data. The CookiesPage states that the platform does not use third-party, advertising, social-media, or analytics/tracking cookies.

GPS Geolocation

  • useGeolocation() hook wraps navigator.geolocation with 5-min cache (maximumAge: 300000)
  • Chat and Find pages pass latitude/longitude in the POST /api/v1/chat body when available
  • Backend computes distance; WorkerCard renders Β· πŸ“ X.X km inline (values <1 km show as <1 km)
  • When location permission is denied, a blue banner (.location-banner) shows with t('chat.location.denied') guiding users to enable it in browser settings

GPS source precedence (highest β†’ lowest):

  1. Request latitude/longitude from browser geolocation (most current)
  2. Stored client_profiles.latitude/longitude from intake
  3. Absent β†’ no distance sorting, no proximity filter

City source precedence (highest β†’ lowest):

  1. City extracted from the user's message by the Pass-1 LLM
  2. Stored client_profiles.city from intake
  3. Absent β†’ no city filter

Search response shape

When mode: "search", the backend returns a different response envelope than intake modes:

{
  "answer": "Here are the plumbers I found near you...",
  "workers": [
    { "id": "...", "full_name": "...", "profession": "Plumber", "city": "Madrid",
      "distance_km": 2.3, "slug": "carlos-plumbing", ... }
  ],
  "conversation_id": "42",
  "branch": "vector",
  "top_score": 0.82
}
  • workers[] β€” array of matched worker cards (rendered as WorkerCard grid by FindPage)
  • branch β€” which search path was taken: vector (pgvector cosine), ilike (text match), ilike_low_top_score (vector score below threshold), ilike_embed_failed (embedding error)
  • top_score β€” highest cosine similarity score from the vector search (absent for ILIKE-only results)
  • distance_km β€” Haversine distance in km from the client's GPS coordinates (absent when coordinates unavailable)

Key Files

File Purpose
src/main.tsx App entry point
src/App.tsx Root component β€” router config + AuthProvider + ProtectedRoute wrapper
src/AuthProvider.tsx Session context β€” loads user from /api/auth/get-session, exposes useAuth() hook
src/auth.ts Barrel re-export of services/auth β€” getSession, sendMagicLink, logout
src/ChatPage.tsx Chat UI β€” message list, input, mode-based routing (worker_intake/client_intake), SSE streaming
src/AdminPage.tsx Admin menu β€” links to /admin/llm, /admin/prompts, and entity CRUD pages
src/AdminLLMPage.tsx LLM provider dropdown β€” calls PUT /api/v1/system-prompts/provider
src/AdminPromptsPage.tsx 4-prompt textarea editor β€” calls PUT /api/v1/system-prompts/{column}
src/FeedbackAdminPage.tsx Admin feedback dashboard β€” lists/subcategories feedback with status filters
src/LoginPage.tsx Magic-link login + signup β€” email input, send link (embeds Cap CAPTCHA widget)
src/PublicProfilePage.tsx Public worker profile with hero, stats, bio, links, CTA (embeds Cap CAPTCHA for contact)
src/TermsPage.tsx Bilingual EN/ES terms and conditions
src/PrivacyPage.tsx Bilingual EN/ES privacy policy
src/CookiesPage.tsx Bilingual EN/ES cookie policy
src/i18n.ts Internationalization β€” translations, language toggle
src/lib/logger.ts Centralized logger β€” createLogger(prefix) for scoped console output
src/lib/feedbackApi.ts Feedback API client β€” submitFeedback(), listFeedback(), updateFeedbackStatus()
src/lib/publicProfileApi.ts Public profile API β€” fetchPublicProfile(slug), fetchLatestProfiles(limit)
src/lib/validate.ts Anti-corruption validators β€” assertString, assertNumber, assertArray, etc.
src/components/feedback/FeedbackWidget.tsx Feedback FAB button (πŸ’¬) β€” fixed-position, opens FeedbackPopover
src/components/feedback/FeedbackPopover.tsx Feedback form β€” category buttons, textarea, submit with success toast
src/components/CookieConsent.tsx Cookie consent banner β€” shown on first visit
src/hooks/useGeolocation.ts GPS geolocation β€” wraps navigator.geolocation, returns { latitude, longitude, loading, permissionDenied, error }
nginx.conf Static file serving + SPA fallback (try_files $uri /index.html) + security headers (CSP/HSTS/X-Frame-Options/etc.), gzip, asset caching (audit P1-2)
Dockerfile Multi-stage: node:22-alpine build β†’ nginx:alpine runtime. Runs as non-root nginx user on port 8080 with HEALTHCHECK (audit P2-1)

Worker Profile Intake Chat

The Worker intake mode is reached via /chat?mode=worker_intake. The chat is the only UI β€” no separate /worker page exists.

Chat Panel

  • Users type naturally: "I'm a plumber in Madrid with 12 years experience"
  • The LLM (with the worker_profile_prompt) asks follow-up questions to gather all 22 fields
  • The LLM emits a [FIELDS]{json}[/FIELDS] block; the backend parses it and merges into the worker profile via map-based upsert
  • Previous conversations are loaded on mount from GET /api/v1/conversations?type=worker&limit=1

Profile Field Coverage

Field JSON key Type
Profession profession string
Business Name business_name string
Bio bio string
Phone phone string
City city string
Address address string
Service Radius service_radius_km number
Hourly Rate hourly_rate number
Minimum Charge minimum_charge number
Free Estimate free_estimate boolean
Years Exp years_experience number
Certifications certifications string[]
Has Insurance has_insurance boolean
Languages languages string[]
Emergency emergency_service boolean
Website website string
Social Links social_links {platform,url}[]
  • "Reset Profile" calls DELETE /api/v1/worker/profile (no PUT endpoint β€” chat handlers do the saving)
  • Chat request body includes mode: "worker_intake" and lang

Client Profile Intake Chat

The Client intake mode is reached via /chat?mode=client_intake. The chat is the only UI β€” no separate /client page exists.

Chat Panel

  • Users describe what they need: "I need help fixing my bathroom"
  • The LLM (with the client_profile_prompt) asks follow-up questions to gather profile fields
  • The LLM emits a [FIELDS]{json}[/FIELDS] block; the backend parses it and merges into the client profile
  • Previous conversations are loaded on mount from GET /api/v1/conversations?type=client&limit=1

Client Profile Fields

Field JSON key Type
Full Name full_name string
Phone phone string
City city string
Address address string
Bio bio string
Property Type property_type string
Notes notes string
  • "Reset Profile" calls DELETE /api/v1/client/profile
  • Chat request body includes mode: "client_intake" and lang

Admin Page Features

LLM Provider Selector

Dropdown at the top of the admin page:

Option Value Behaviour
Default (auto) "" Falls back to helper's auto fallback chain (OpenCode 0 β†’ OpenCode 1 β†’ OpenCode 2 β†’ Mistral β†’ Ollama, cheap-first R5)
OpenCode 0 (Big Pickle) "opencode0" Forces big-pickle model
OpenCode 1 "opencode1" Forces first OpenCode endpoint (deepseek-v4-flash-free)
OpenCode 2 "opencode2" Forces second OpenCode endpoint (mimo-v2.5-free)
Ollama (local) "ollama" Forces local Ollama
Mistral (cloud) "mistral" Forces Mistral API (requires MISTRAL_API_KEY)

Changes take effect immediately β€” no container restart needed.

Prompt Editor

Textareas to edit (one per column in the system_prompts table):

Column i18n key Purpose
worker_profile_prompt admin.prompt.worker System prompt for worker profile intake chat
client_profile_prompt admin.prompt.client System prompt for client profile intake chat
find_trader_search_prompt admin.prompt.find_trader_search First-pass search-params extraction
find_trader_presentation_prompt admin.prompt.find_trader_presentation Second-pass results-presentation

Each textarea has a per-column Save button that calls PUT /api/v1/system-prompts/{column}.


Development

npm install          # Install dependencies
npm run dev          # Vite dev server with HMR on :5173
npm run build        # Production build β†’ dist/
npm run preview      # vite preview (serves built dist/)
npm run typecheck    # tsc --noEmit
npm run lint         # eslint src/

Tests

npm run test:coverage       # vitest run --coverage (unit + integration)
npm run test:e2e            # playwright test (uses built dist/)

Coverage thresholds (enforced in vitest.config.ts): lines 75%, branches 75%, functions 78%, statements 75%. The CI pipeline (.github/workflows/ci.yml) runs: lint β†’ typecheck β†’ vitest (unit + integration) β†’ Playwright e2e β†’ Docker build/push to ghcr.io/helpingpeoplenow/frontend.

Note: the package.json only exposes test:coverage and test:e2e β€” there is no plain npm test (vitest watch) script. Use npx vitest for watch mode locally.

Docker Build

docker build -t ghcr.io/helpingpeoplenow/frontend:latest .
docker push ghcr.io/helpingpeoplenow/frontend:latest

CI builds and pushes automatically on push to main.


Logging

Browser console logging with component prefixes:

Prefix Component Events
[Chat] ChatPage Request timing, answer length, mode, errors
[Auth] AuthProvider Session check, login/logout, redirect
[Admin] AdminLLMPage / AdminPromptsPage Prompt load/save, provider switch, timing
[DM] DirectMessagePage / WorkerContactPage DM send, block, report, archive
[Nav] App router Route changes, auth redirects
[ModeChooser] ModeChooser Card click navigation
[API] services/api.ts Generic fetch calls, error handling
[SSE] DirectMessageSSE Connect, disconnect, reconnect attempts, polling fallback
[Inbox] InboxPage Thread list load, unread badge
[Thread] DirectMessagePage Message load, scroll, read receipt
[Speech] useSpeechRecognition Voice input start/stop/error
[App] App.tsx Mount, route init
[Profile] PublicProfilePage Profile load, CTA click
[Landing] LandingPage Hero load, latest professionals
[Feedback] FeedbackWidget / FeedbackPopover Submit, success, error
[Geo] useGeolocation GPS permission, coords, error

Open browser DevTools (F12) β†’ Console for debugging.


Project Structure

frontend/
β”œβ”€β”€ index.html                    # HTML shell (imports /src/main.tsx)
β”œβ”€β”€ nginx.conf                    # nginx static file serving + SPA fallback + /health
β”œβ”€β”€ Dockerfile                    # Multi-stage: node:22-alpine β†’ nginx:alpine
β”œβ”€β”€ package.json                  # Dependencies
β”œβ”€β”€ vite.config.js                # Vite config (Preact preset)
β”œβ”€β”€ tsconfig.json                 # TypeScript config
β”œβ”€β”€ playwright.config.js          # Playwright e2e config
β”œβ”€β”€ e2e/
β”‚   └── deploy.spec.js            # Playwright deploy-smoke tests
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main.tsx                  # Entry point β€” renders App with LanguageProvider
β”‚   β”œβ”€β”€ App.tsx                   # Router + AuthProvider + ProtectedRoute + ErrorBoundary
β”‚   β”œβ”€β”€ AppShell.tsx              # Sidebar + header layout
β”‚   β”œβ”€β”€ AuthProvider.tsx          # Session context + useAuth hook
β”‚   β”œβ”€β”€ auth.ts                   # Barrel re-export of services/auth
β”‚   β”œβ”€β”€ i18n.ts                   # EN/ES translations + LanguageProvider + LangToggle
β”‚   β”œβ”€β”€ style.css                 # Shared design system
β”‚   β”œβ”€β”€ LoginPage.tsx             # Magic link login + signup (embeds Cap CAPTCHA widget)
β”‚   β”œβ”€β”€ LandingPage.tsx           # Marketing landing for visitors / ModeChooser for authed
β”‚   β”œβ”€β”€ ChatPage.tsx              # Worker/client intake chat (mode in query string)
β”‚   β”œβ”€β”€ FindPage.tsx              # Search/find professional chat
β”‚   β”œβ”€β”€ PublicProfilePage.tsx     # Public worker profile with hero, stats, bio, links, CTA
β”‚   β”œβ”€β”€ ModeChooser.tsx           # 3-card mode selector
β”‚   β”œβ”€β”€ AdminPage.tsx             # Admin menu
β”‚   β”œβ”€β”€ AdminLLMPage.tsx          # LLM provider dropdown
β”‚   β”œβ”€β”€ AdminPromptsPage.tsx      # 4-prompt textarea editor
β”‚   β”œβ”€β”€ FeedbackAdminPage.tsx     # Admin feedback dashboard β€” status filters, category, message
β”‚   β”œβ”€β”€ TermsPage.tsx             # Bilingual EN/ES terms and conditions
β”‚   β”œβ”€β”€ PrivacyPage.tsx           # Bilingual EN/ES privacy policy
β”‚   β”œβ”€β”€ CookiesPage.tsx           # Bilingual EN/ES cookie policy
β”‚   β”œβ”€β”€ UsersPage / UserDetailPage.tsx
β”‚   β”œβ”€β”€ WorkersPage / WorkerDetailPage.tsx
β”‚   β”œβ”€β”€ ClientsPage / ClientDetailPage.tsx
β”‚   β”œβ”€β”€ ConversationsPage / ConversationDetailPage.tsx
β”‚   β”œβ”€β”€ MessagesPage / MessageDetailPage.tsx
β”‚   β”œβ”€β”€ EntityListPage.tsx        # Generic admin CRUD list
β”‚   β”œβ”€β”€ EntityDetailPage.tsx      # Generic admin CRUD detail
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ ErrorBoundary.tsx     # Catches render errors
β”‚   β”‚   β”œβ”€β”€ CookieConsent.tsx     # Cookie consent banner β€” shown on first visit
β”‚   β”‚   β”œβ”€β”€ feedback/
β”‚   β”‚   β”‚   β”œβ”€β”€ FeedbackWidget.tsx   # Fixed-position FAB (πŸ’¬) β€” opens FeedbackPopover
β”‚   β”‚   β”‚   └── FeedbackPopover.tsx  # Feedback form β€” category, textarea, submit
β”‚   β”‚   └── chat/
β”‚   β”‚       β”œβ”€β”€ ChatInput.tsx     # Input bar with optional mic button
β”‚   β”‚       β”œβ”€β”€ ChatMessages.tsx  # Bubble list with worker card grid
β”‚   β”‚       β”œβ”€β”€ ChatWelcome.tsx   # First-message welcome card
β”‚   β”‚       └── WorkerCard.tsx    # Clickable worker summary card (shows distance_km when available)
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useChat.ts            # Chat state + SSE streaming
β”‚   β”‚   β”œβ”€β”€ useChatInit.ts        # Load most recent conversation
β”‚   β”‚   β”œβ”€β”€ useGeolocation.ts     # GPS geolocation (latitude, longitude, permissionDenied)
β”‚   β”‚   └── useSpeechRecognition.ts  # Voice input via Web Speech API
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”œβ”€β”€ logger.ts              # Centralized logger β€” createLogger(prefix)
β”‚   β”‚   β”œβ”€β”€ feedbackApi.ts         # Feedback API β€” submitFeedback, listFeedback, updateFeedbackStatus
β”‚   β”‚   β”œβ”€β”€ publicProfileApi.ts    # Public profile API β€” fetchPublicProfile, fetchLatestProfiles
β”‚   β”‚   β”œβ”€β”€ validate.ts            # Anti-corruption validators β€” assertString, assertArray, etc.
β”‚   β”‚   β”œβ”€β”€ directMessageApi.ts   # DM API client (contact, inbox, messages, etc.)
β”‚   β”‚   └── sse.ts                # DirectMessageSSE class with reconnect + polling fallback
β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”œβ”€β”€ InboxPage.tsx         # DM inbox
β”‚   β”‚   β”œβ”€β”€ DirectMessagePage.tsx # DM thread + actions
β”‚   β”‚   └── WorkerContactPage.tsx # Intermediate redirect after clicking WorkerCard
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ api.ts                # request() β€” generic fetch with credentials
β”‚   β”‚   β”œβ”€β”€ auth.ts               # getSession, sendMagicLink, logout
β”‚   β”‚   β”œβ”€β”€ chat.ts               # sendChat (raw fetch, SSE-aware)
β”‚   β”‚   β”œβ”€β”€ conversations.ts      # list/get conversations
β”‚   β”‚   β”œβ”€β”€ systemPrompts.ts      # GET/PUT system prompts
β”‚   β”‚   β”œβ”€β”€ admin.ts              # list/get/update/delete admin entities
β”‚   β”‚   └── profiles.ts           # (dead β€” see GOTCHA)
β”‚   └── store/
β”‚       └── directMessages.ts     # Zustand store
└── dist/                         # Production build output

About

Frontend for HelpingPeopleNow

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages