Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,16 @@ Post/live-event feedback for selected volunteers + nonprofit partners. Both rout
- **Auth/CAPTCHA**: not gated by `RequiredAuthProvider` — nonprofits/anonymous can submit. Logged-in `isSelected` volunteers are trusted (no CAPTCHA); everyone else gets an invisible reCAPTCHA v3 token (`useRecaptcha`). Mode `upcoming` shows a "not started yet" card; `live`→"how's it going", `post`→"how was your experience". Pages are `noindex`. Backend computes mode (timezone-aware) — the frontend does NOT recompute dates.
- **Discoverability**: `src/components/Survey/SurveyCTA.js` — a self-contained CTA (var-fallback inline styles so it works in or out of `RefinedRoot`; mount-gated to avoid SSR/ISR hydration mismatch) linking to `/hack/<id>/survey`, rendered only once the event has **started** (live or ended; hidden for upcoming). Wired into: the event page `/hack/[event_id]` (after the masthead — the only surface reaching anonymous nonprofits), `mentor-checkin`, `manageteam`, `team/[team_id]`, and `judge-application` (gated on `isSelected`). Pass `eventId` + `startDate`/`endDate`/`timezone`; the component owns the visibility gate.

## Volunteer Job Board (`/jobs`, `/jobs/[slug]`, `/admin/jobs` — Aug 2026)

Volunteer organizer roles (Social Media Manager, Hackathon Operations Lead — Phoenix, Mentor Program Lead) with an AI-resistant application: required intro **video** (reuses `IntroVideoField` — its bio-video upload mint works as-is because applying requires login), required **PDF resume** (`src/components/Jobs/ResumeUploadField.js` → `POST /api/jobs/apply/resume-upload-url`, clone of the bio-video signed-URL flow, lands at `job_applications/<db_id>/` on the public CDN — accepted obscured-URL risk), a role-specific **work sample** (min 200 chars, mirrored in backend `MIN_JOB_WORK_SAMPLE_LENGTH`), and a **reply-within-5-days responsiveness test** baked into the confirmation email. Load-bearing details:

- **Backend**: blueprint `api/jobs/` (`jobs_views.py` + `jobs_service.py`; registered in `api/__init__.py`). Collections: `job_listings` (doc id = slug, **slug immutable after create**) and `job_applications` (uuid4). Public `GET /api/jobs` returns published+closed (lean fields); `GET /api/jobs/<slug>` 404s drafts/hidden but **returns closed** so shared links render a calm closed panel. Apply/`me` routes are `@auth.require_user`; submit verifies recaptcha (volunteers_service `verify_recaptcha` + FLASK_ENV=development bypass), re-verifies resume/video URLs against the caller's own CDN prefix via `get_blob_metadata`, and 409s duplicates (listing_slug+user_id). Validators in `common/utils/validators.py` (`validate_job_listing[_partial]`, `validate_job_application`, `ALLOWED_JOB_*`). Emails (Resend, `_notifications_disabled()` gate): applicant confirmation (`reply_to` questions@ohack.org + the reply-to-confirm ask), FYI to questions@ohack.org, and warm accept/reject decision emails (`POST /api/jobs/admin/applications/<id>/decision`, optional `personal_note`). TTL caches (300s) cleared on every admin write. Seed: `scripts/seed_job_listings.py` (dry-run default, skips existing slugs, seeds drafts).
- **Frontend pages**: both ISR revalidate 300. `/jobs` index is a refined pillar page (FAQ_ITEMS module-scope → `<details>` + FAQPage JSON-LD); its getStaticProps treats a 404 from `/api/jobs` as empty (deploy-ordering: backend must ship first or the page renders the empty state) but **rethrows other errors** (ISR keeps last good). `/jobs/[slug]` SSRs the listing publicly (SEO/unfurls) with a **top-level `JobPosting` JSON-LD node** (`employmentType: "VOLUNTEER"` → Google for Jobs; TELECOMMUTE + applicantLocationRequirements for remote/hybrid, Tempe `jobLocation` for phoenix_in_person; `datePosted`/`validThrough` set conditionally — **Next rejects `undefined` in props**). Only the `#apply` section is auth-gated — via `useAuthInfo` + `redirectToLoginPage` (app-level AuthProvider), NOT a page-level RequiredAuthProvider which would hide content from crawlers.
- **`JobApplicationForm`** (`src/components/Jobs/`): 4 steps, mentor-form patterns (refinedStyles imports, `useFormPersistence` localStorage autosave with `formType:"job"`/`eventId:slug` — `loadPreviousSubmission` deliberately NOT called; already-applied comes from `GET /api/jobs/<slug>/applications/me` with a 6s `AbortSignal.timeout` so a slow backend can't pin the spinner). **The `<form>` MUST keep `noValidate`** — the required MUI Selects render hidden native inputs and browser constraint validation otherwise silently blocks submission (no submit event, no visible error; this bit us). Phoenix listing (`location_type === "phoenix_in_person"`) hard-blocks `inPersonOk !== "Yes"`; hours below `listing.min_hours_per_week` blocks with a kind redirect message. Never add `isSelected`/staff-owned fields to `initialFormData`.
- **Admin** `/admin/jobs?tab=listings|applications` (blog-admin auth pattern; registered in BOTH nav registries): `src/components/admin/jobs/ListingsTab.js` (table + edit Dialog — deliberately no blog-style editor pages; publish/hide quick toggle) and `ApplicationsTab.js` (filters, detail Dialog derived live from list state — stale-snapshot gotcha —, status/notes PATCH, one-click kind-rejection/accept decision emails).
- **SEO plumbing**: `/jobs/[slug]` in next-sitemap `exclude` + `jobs` substring in the 0.8-priority branch; jobs block in `server-sitemap.xml.js`. Cross-link card on `/volunteer` (`#roles` section). Footer deliberately untouched (CWV height contract).

## Admin Feedback review (`/admin/feedback`)

One `volunteer.admin`-gated page (`src/pages/admin/feedback/index.js`) with 3 MUI tabs over 3 distinct data sources (different scopes — don't merge them into one table). Plain MUI, standard `AdminPage` + `RequiredAuthProvider` shell. Registered in BOTH nav registries (`src/components/admin/AdminNavigation.js` + `src/pages/admin/index.js`). Only the active tab mounts (lazy fetch). Panels live in `src/components/admin/feedback/`:
Expand Down
4 changes: 3 additions & 1 deletion next-sitemap.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
"/hackathon/[hackathon_id]",
"/project/[project_id]",
"/hack/[event_id]",
"/jobs/[slug]",
// Dynamic routes covered by /server-sitemap.xml instead
"https://api.test.ohack.dev/",
"https://test.api.ohack.dev/",
Expand Down Expand Up @@ -60,7 +61,8 @@ module.exports = {
path.includes("recruit") ||
path.includes("hackathon") ||
path.includes("social-good") ||
path.includes("nonprofits")
path.includes("nonprofits") ||
path.includes("jobs")
) {
priority = 0.8;
changefreq = "weekly";
Expand Down
Loading