Skip to content

feat(web,cms): video hero, experience blocks rename, easter seed#301

Merged
Kneesal merged 12 commits intoJesusFilm:mainfrom
Kneesal:feat/video-hero-and-experience-blocks
Mar 9, 2026
Merged

feat(web,cms): video hero, experience blocks rename, easter seed#301
Kneesal merged 12 commits intoJesusFilm:mainfrom
Kneesal:feat/video-hero-and-experience-blocks

Conversation

@Kneesal
Copy link
Copy Markdown
Member

@Kneesal Kneesal commented Mar 9, 2026

Summary

Adds Video Hero block to Strapi and web, renames Experience sections to blocks, and adds a runnable seed script for the Easter watch experience. The /watch/easter page is driven by CMS content (single block renderer). Web uses STRAPI_API_TOKEN from .env.local; 401 handling and error copy clarified.

Resolves #152

Contracts Changed

  • yes (GraphQL: Experience.blocks, ComponentSectionsVideoHero with streamingUrl)

Regeneration Required

  • yes
  • no

Validation

  • Contracts validated
  • Generated code verified (no manual edits)
  • Tests and build passed
  • Terraform plan reviewed (if infra change) — N/A

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Video Hero section with streaming playback, mute controls, and scroll-aware autoplay.
    • Experience metadata utility for improved SEO and per-locale metadata.
  • Bug Fixes

    • Clearer error messaging for authentication failures.
  • Chores

    • CMS seeding script and top-level "seed" npm script to initialize Easter content.
  • Refactor

    • Renamed Experience "sections" to "blocks" across web, mobile, and GraphQL.

Kneesal and others added 2 commits February 19, 2026 10:29
Co-authored-by: Cursor <cursoragent@cursor.com>
- CMS: rename Experience sections to blocks; add streamingUrl to video-hero
- CMS: seed script (pnpm seed) for Easter experience + Video Hero block
- Web: VideoHero component (video.js), GET_WATCH_EXPERIENCE with blocks
- Web: ExperienceSectionRenderer for blocks; typedRoutes in next.config
- Web: 401 error copy and ExperienceError known messages
- GraphQL + mobile: WatchExperience blocks type and usage
- Docs: DEV-COMMANDS.md, VIDEO-HERO-MANUAL-SETUP.md, package seed script

Resolves JesusFilm#164

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Renames Experience dynamic zone from sections → blocks across CMS, web, mobile, and GraphQL types; adds streamingUrl to VideoHero schema; introduces a VideoHero React component (Video.js with scroll/mute controls); adds a CMS seed script to create the Easter video/experience; updates fragments, exports, and metadata helpers.

Changes

Cohort / File(s) Summary
CMS schema & content
apps/cms/schema.graphql, apps/cms/src/api/experience/content-types/experience/schema.json
Rename Experience.sections → blocks; add ExperienceBlocksDynamicZone/input; update ExperienceInput and description to reference blocks.
CMS component schema
apps/cms/src/components/sections/video-hero.json
Add required streamingUrl string attribute to VideoHero component definition.
CMS scripts & package
apps/cms/scripts/seed-easter.cjs, apps/cms/package.json
Add seed script scripts/seed-easter.cjs and npm run seed in CMS package.json to create Easter Video and Experience (VideoHero block with MUX streamingUrl).
Web pages & routing
apps/web/src/app/page.tsx, apps/web/src/app/[slug]/page.tsx, apps/web/src/app/[slug]/[locale]/page.tsx, apps/web/src/app/watch/easter/page.tsx
Switch page logic to use experience.blocks; update rendering keys/props; remove /watch/easter page implementation.
Web components & fragments
apps/web/src/components/sections/VideoHero.tsx, apps/web/src/components/sections/videoHeroFragment.ts, apps/web/src/components/sections/index.tsx
Add VideoHero component (Video.js player, HLS support, scroll pause/resume, mute UI), fragment, and wire into section renderer (new ExperienceSectionRenderer) with deprecated wrapper.
Web data layer & metadata
apps/web/src/lib/content.ts, apps/web/src/lib/experience-metadata.ts, apps/web/next-env.d.ts, apps/web/package.json
Query blocks not sections, register videoHeroFragment, aggregate GraphQL errors earlier, add getExperienceMetadata helper, switch next-env to triple-slash reference, add video.js and @types/video.js deps.
Web Easter metadata removed
apps/web/src/app/watch/easter/metadata.ts
Deleted dedicated Easter metadata module.
Error handling
apps/web/src/components/ExperienceError.tsx
Add mappings for 401/credential errors and trim messages in sanitizeMessage.
Mobile queries & types
mobile/android/.../ExperienceBySlug.graphql, mobile/ios/.../GetWatchExperience.graphql, mobile/expo/src/lib/graphql/queries.ts, mobile/expo/src/lib/experienceService.ts, mobile/expo/src/lib/experienceService.test.ts
Update GraphQL queries to use experiences.blocks; rename exported/imported WatchExperienceSectionWatchExperienceBlock; adjust tests and mappings.
GraphQL package types & exports
packages/graphql/src/graphql-env.d.ts, packages/graphql/src/watchExperience.ts, packages/graphql/src/index.ts
Add Experience and ExperienceBlocksDynamicZone types/inputs; remove ExperienceSectionsDynamicZone; add streamingUrl to ComponentSectionsVideoHero; rename WatchExperienceSectionWatchExperienceBlock and update exports.
Root scripts & tooling
package.json, turbo.json
Add root seed script and new generate turbo task (outputs src/graphql-env.d.ts).
Misc web typing
apps/web/next-env.d.ts
Replace import with triple-slash reference for Next.js route types.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Web Client
  participant Web as Next.js App
  participant CMS as Strapi GraphQL
  participant CDN as Video CDN (HLS)

  Client->>Web: GET /watch/:slug
  Web->>CMS: GraphQL query for Experience (blocks) + fragments (VideoHero)
  CMS-->>Web: Experience.blocks (includes VideoHero with streamingUrl)
  Web-->>Client: Render page HTML + VideoHero shell
  Client->>CDN: Video.js requests streamingUrl (HLS)
  CDN-->>Client: HLS manifest/segments
  Client->>Client: Video.js playback, scroll/mute controls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding VideoHero component, renaming Experience.sections to blocks, and providing an Easter seed script—all core objectives of the PR.
Linked Issues check ✅ Passed The PR implements the VideoHero component requirement (#152): new VideoHero.tsx component with GraphQL integration, dynamic zone rendering support, video.js player with mute toggle, accessibility features, and responsive design.
Out of Scope Changes check ✅ Passed All changes are scoped to the PR objectives: VideoHero implementation, Experience.sections→blocks migration across all packages, error handling improvements, and Easter seed infrastructure. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread DEV-COMMANDS.md Outdated
Comment thread mobile/ios/package.json
Comment thread apps/web/next.config.mjs
Comment thread apps/web/next-env.d.ts Outdated
Comment thread apps/web/src/env.ts Outdated
Comment thread apps/web/src/lib/client.ts Outdated
Comment thread apps/cms/VIDEO-HERO-MANUAL-SETUP.md Outdated
- Remove DEV-COMMANDS.md and VIDEO-HERO-MANUAL-SETUP.md
- Revert mobile/ios lint script to swiftlint lint
- Revert next.config to experimental.typedRoutes; keep images
- Revert next-env.d.ts to reference path for routes
- Revert env.ts STRAPI_API_TOKEN to z.string()
- Revert client.ts to uri/headers variables style

Made-with: Cursor
@Kneesal
Copy link
Copy Markdown
Member Author

Kneesal commented Mar 9, 2026

Review feedback addressed (9388849)

Fixed:

  • DEV-COMMANDS.md: Removed.
  • apps/cms/VIDEO-HERO-MANUAL-SETUP.md: Removed.
  • mobile/ios/package.json: Reverted lint script to swiftlint lint.
  • apps/web/next.config.mjs: Reverted typedRoutes to experimental.typedRoutes; kept images.remotePatterns.
  • apps/web/next-env.d.ts: Reverted to /// <reference path="./.next/types/routes.d.ts" />.
  • apps/web/src/env.ts: Reverted STRAPI_API_TOKEN to z.string() (no .min(1)).
  • apps/web/src/lib/client.ts: Reverted to uri / headers variables then HttpLink({ uri, headers }).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/graphql/src/watchExperience.ts (1)

15-69: ⚠️ Potential issue | 🔴 Critical

Fetch ComponentSectionsVideoHero in this blocks query.

The renderer in apps/web/src/components/sections/index.tsx now switches on ComponentSectionsVideoHero, but this query never selects that union member. A seeded Easter experience with a video hero will therefore come back with at most __typename, so /watch/easter cannot render the hero content or streamingUrl.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/graphql/src/watchExperience.ts` around lines 15 - 69, The blocks
selection in watchExperience.ts is missing the union member
ComponentSectionsVideoHero, so queries for experiences with a video hero return
only __typename; add a new inline fragment for ComponentSectionsVideoHero under
blocks (matching the same pattern as other fragments) and select the
renderer-used fields such as id, title/heading fields, subtitle/body, image
(url), streamingUrl (and any video/document fields the renderer expects) so the
renderer in apps/web/src/components/sections/index.tsx receives full hero
content.
apps/cms/src/api/experience/content-types/experience/schema.json (1)

94-116: ⚠️ Potential issue | 🔴 Critical

Fix mobile client field access: experience.sectionsexperience.blocks.

The CMS schema renamed the dynamic zone from sections to blocks (line 94), and GraphQL queries were correctly updated to fetch blocks. However, mobile client implementations still try to access experience.sections, which no longer exists:

  • mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift: accesses experience.sections
  • mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt: accesses experience.sections

Both iOS and Android apps will fail at runtime when trying to access the renamed field. Update these files to use blocks instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cms/src/api/experience/content-types/experience/schema.json` around
lines 94 - 116, The mobile clients still access the renamed dynamic zone field
experience.sections which was changed to experience.blocks in the CMS schema;
update both GraphQLContentClient.swift and GraphQLContentClient.kt to read the
blocks field instead of sections, including any JSON/GQL parsing keys, property
names, decoder/mapper logic and any model classes or accessors (e.g., change
occurrences of experience.sections, Sections or related mapping methods to
experience.blocks, Blocks or corresponding accessor names) so the GraphQL
response mapping and runtime access use "blocks" consistently on iOS
(GraphQLContentClient.swift) and Android (GraphQLContentClient.kt).
🧹 Nitpick comments (5)
apps/web/src/components/ExperienceError.tsx (1)

8-11: Consider using user-friendly messages without internal setup instructions.

The error messages expose implementation details (.env.local path, Strapi Admin navigation) that are developer-facing. In production, end users seeing these would be confused and it leaks internal infrastructure information.

Consider either:

  1. Showing a generic user-facing message like "Configuration error. Please contact support."
  2. Conditionally showing detailed messages only in development mode.
💡 Example: Generic user-facing message
   "Response not successful: Received status code 401":
-    "Invalid or missing API token. Set STRAPI_API_TOKEN in apps/web/.env.local (create token in Strapi Admin → Settings → API Tokens).",
+    "Unable to authenticate with the content service. Please contact support if this persists.",
   "Missing or invalid credentials":
-    "Invalid or missing API token. Set STRAPI_API_TOKEN in apps/web/.env.local (create token in Strapi Admin → Settings → API Tokens).",
+    "Unable to authenticate with the content service. Please contact support if this persists.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ExperienceError.tsx` around lines 8 - 11, Update the
user-facing error strings in ExperienceError (the mapping entries for "Response
not successful: Received status code 401" and "Missing or invalid credentials")
to avoid exposing internal setup details; replace the current ".env.local /
Strapi Admin" instructions with a generic message like "Configuration error.
Please contact support." and, if you want dev details, conditionally append the
original detailed guidance only when running in development (check NODE_ENV or a
runtime isDevelopment flag) so production users never see infrastructure paths.
mobile/android/app/src/main/graphql/ExperienceBySlug.graphql (2)

2-2: Stale comment references "section variants".

The comment still says "all section variants" but the field was renamed to blocks. Update for consistency.

📝 Suggested fix
 # Android-owned operation — mirrors web query intent, NOT shared with iOS.
-# Fetches a single Experience by slug with all section variants.
+# Fetches a single Experience by slug with all block variants.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/android/app/src/main/graphql/ExperienceBySlug.graphql` at line 2,
Update the top-line comment to reflect the renamed field: replace "section
variants" with "blocks" so the comment reads that it fetches a single Experience
by slug with all blocks; edit the comment near the ExperienceBySlug GraphQL
definition (the header comment above the query) to reference "blocks" instead of
"section variants" to keep documentation consistent with the schema change.

11-73: VideoHero fragment missing in Android query.

The web query includes ... on ComponentSectionsVideoHero (lines 53–54 in apps/web/src/lib/content.ts), but the Android ExperienceBySlug.graphql does not. If Android should support VideoHero content, add the fragment; otherwise, this can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/android/app/src/main/graphql/ExperienceBySlug.graphql` around lines 11
- 73, The Android GraphQL query ExperienceBySlug.graphql is missing the
ComponentSectionsVideoHero fragment so VideoHero blocks won't be fetched; add a
branch inside the blocks selection matching "... on ComponentSectionsVideoHero"
and include the same fields used by web (e.g., __typename, id, heading, intro,
video { documentId, title, slug, image { url, alternativeText, width, height } }
or whichever fields your app needs) to mirror the web implementation and ensure
Android can consume VideoHero content.
apps/web/src/components/sections/VideoHero.tsx (1)

39-48: Consider a scroll threshold instead of exact scrollY === 0 for resuming playback.

Currently, the video only resumes when the user scrolls to the absolute top (scrollY === 0). Users who scroll partially back up won't see the video resume, which may feel unresponsive.

♻️ Suggested threshold-based resume
   const pauseOnScrollAway = useCallback(() => {
     const scrollY = window.scrollY
     if (playerRef.current) {
       if (scrollY > 100) {
         playerRef.current.pause()
-      } else if (scrollY === 0) {
+      } else if (scrollY < 50) {
         void playerRef.current.play()
       }
     }
   }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/sections/VideoHero.tsx` around lines 39 - 48, The
pauseOnScrollAway handler currently only resumes playback when scrollY === 0;
change it to use a resume threshold (e.g., resume when scrollY <= 100) so
partial upward scrolling restarts the video. Update the logic in
pauseOnScrollAway (referenced function) to compare against a numeric threshold
(or a named constant like resumeThreshold) instead of strict zero, keeping the
existing pause condition (scrollY > 100) and ensuring you reference
playerRef.current.pause() and playerRef.current.play() accordingly.
apps/web/src/lib/content.ts (1)

145-150: The type cast for accessing GraphQL errors is pragmatic but could be typed more explicitly.

The cast to { errors?: Array<{ message?: string }> } works but loses type safety. If Apollo Client's types already include errors on the result, the cast is unnecessary; if not, consider augmenting the type.

That said, the error handling logic itself is correct and properly aggregates multiple error messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/content.ts` around lines 145 - 150, The current cast (result
as { errors?: Array<{ message?: string }> }) is imprecise; instead import and
use the appropriate Apollo type (e.g., FetchResult or ApolloQueryResult from
`@apollo/client` or `@apollo/client/core`) or add a narrow type guard so you can
access errors safely; update the code around the graphqlErrors assignment (the
graphqlErrors variable and the result value returned from the GraphQL call) to
use the correct imported type or a small type-guard function that checks for an
errors array, then keep the existing mapping logic that builds msg and returns {
data: null, error: new Error(msg) } unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/cms/src/components/sections/video-hero.json`:
- Around line 20-23: The schema currently defines "streamingUrl" as optional
which allows publishing a Video Hero without any playable source; update the
video hero schema so "streamingUrl" is required (set "required": true for the
"streamingUrl" property) unless you add and validate a real fallback playback
source (e.g., a new "fallbackUrl" or "fallbackAsset" property and logic to
ensure at least one of "streamingUrl" or the fallback is present) so editors
cannot save a hero with no playable source.

In `@apps/cms/VIDEO-HERO-MANUAL-SETUP.md`:
- Around line 36-38: Replace the hard-coded year in the example subheading to
avoid stale content: update the "Subheading" example (the line labeled
**Subheading** in the diff) to remove the year (e.g., change "Easter 2025 —
videos & resources about Lent, Holy Week, Resurrection" to a yearless example
such as "Easter — videos & resources about Lent, Holy Week, Resurrection") so
editors won't publish an outdated year on /watch/easter.

In `@apps/web/next-env.d.ts`:
- Line 3: Remove the manual import statement `import
"./.next/dev/types/routes.d.ts"` from the auto-generated next-env.d.ts file;
next-env.d.ts is regenerated by Next.js and should not be edited manually, and
route types are already provided via tsconfig.json includes
(`.next/types/**/*.ts` and `.next/dev/types/**/*.ts`), so delete that import to
avoid committing generated changes.

In `@apps/web/package.json`:
- Around line 21-28: The repository's package.json now adds dependencies
("video.js" and "zod") but the pnpm lockfile wasn't updated, causing CI to fail
with pnpm install --frozen-lockfile; regenerate the pnpm lockfile locally (run
pnpm install) so pnpm-lock.yaml includes the new entries, verify the changes,
and commit the updated pnpm-lock.yaml alongside the package.json changes so CI
can install reproducibly.

In `@apps/web/src/app/`[slug]/page.tsx:
- Around line 23-30: The early emptiness check uses experience.blocks which may
include null/Error entries; first filter invalid blocks with the existing filter
(the const blocks = experience.blocks.filter(...) that narrows to Section) and
then check if blocks.length is zero to return <ExperienceEmpty />; update the
flow in page.tsx so the filtered "blocks" (not experience.blocks) are used for
the emptiness guard and subsequent rendering (keep the Section type guard and
ExperienceEmpty return).

In `@apps/web/src/components/sections/index.tsx`:
- Around line 22-24: The switch default case in the sections renderer currently
does "return null" for unhandled CMS block types which silently drops content;
change the default branch in the switch inside components/sections/index.tsx to
surface an explicit signal: log a warning (including the block type/identifier)
via the existing logger or console.warn and render a visible
placeholder/fallback React element that indicates an unsupported block (so
missing content is detectable), rather than returning null. Update the default
case handling where "return null" appears and include the block's type/id in the
warning to aid debugging.

In `@apps/web/src/env.ts`:
- Line 6: The new env schema requires STRAPI_API_TOKEN to be non-empty; update
the example environment template and setup docs so they no longer include an
empty assignment for STRAPI_API_TOKEN and instead either provide a placeholder
value (e.g., STRAPI_API_TOKEN=your_token_here) or document that users must set
this variable before startup; change the example env file and any README/setup
docs that reference STRAPI_API_TOKEN to reflect the new requirement and include
a short note on how to obtain or configure the token.

In `@DEV-COMMANDS.md`:
- Line 3: Update the sentence "From repo root (`qyi`). Install first: `pnpm
install`." in DEV-COMMANDS.md to remove the stray directory name or replace
`qyi` with the actual repository directory name; specifically edit that line so
it reads either "From repo root. Install first: `pnpm install`." or "From the
repo root (replace-with-repo-name). Install first: `pnpm install`." to avoid
confusion.

In `@mobile/android/app/src/main/graphql/ExperienceBySlug.graphql`:
- Line 11: Update GraphQLContentClient.kt to use the regenerated GraphQL
property name `blocks` instead of `sections`: replace both occurrences of
`experience.sections?.mapNotNull` with `experience.blocks?.mapNotNull` (these
appear in the methods that build/transform experience content in
GraphQLContentClient.kt), ensuring any downstream variables/names expecting
`sections` are updated to `blocks` as well so the code compiles with the new
Apollo-generated types.

In `@mobile/ios/GraphQL/Operations/GetWatchExperience.graphql`:
- Line 9: The GraphQL operation was changed to use the field `blocks` but the
generated Swift query and mapper still reference `sections`, causing a
selection-set mismatch; regenerate the Apollo iOS artifacts for the
GetWatchExperience operation (re-run the Apollo codegen/cli or the build phase
that generates GraphQL files) so the generated query class/struct and the mapper
use `blocks` instead of `sections`, and then verify/update any references to the
generated types (e.g., GetWatchExperienceQuery, its selectionSet/mapping code,
and any mapper helper) to match the new `blocks` property.

In `@mobile/ios/package.json`:
- Line 5: The current "lint" npm script in package.json is masking a missing
SwiftLint binary by returning success; change the "lint" script (the "lint"
entry) to require SwiftLint and exit non-zero when it's not present (so CI/local
runs fail if SwiftLint is missing), and add a new optional script (e.g.,
"lint:optional" or "lint:local") that performs the conditional check and echoes
the skip message for convenience. Update only the "lint" and add the optional
script entries so linting behavior matches other packages and CI remains strict.

---

Outside diff comments:
In `@apps/cms/src/api/experience/content-types/experience/schema.json`:
- Around line 94-116: The mobile clients still access the renamed dynamic zone
field experience.sections which was changed to experience.blocks in the CMS
schema; update both GraphQLContentClient.swift and GraphQLContentClient.kt to
read the blocks field instead of sections, including any JSON/GQL parsing keys,
property names, decoder/mapper logic and any model classes or accessors (e.g.,
change occurrences of experience.sections, Sections or related mapping methods
to experience.blocks, Blocks or corresponding accessor names) so the GraphQL
response mapping and runtime access use "blocks" consistently on iOS
(GraphQLContentClient.swift) and Android (GraphQLContentClient.kt).

In `@packages/graphql/src/watchExperience.ts`:
- Around line 15-69: The blocks selection in watchExperience.ts is missing the
union member ComponentSectionsVideoHero, so queries for experiences with a video
hero return only __typename; add a new inline fragment for
ComponentSectionsVideoHero under blocks (matching the same pattern as other
fragments) and select the renderer-used fields such as id, title/heading fields,
subtitle/body, image (url), streamingUrl (and any video/document fields the
renderer expects) so the renderer in apps/web/src/components/sections/index.tsx
receives full hero content.

---

Nitpick comments:
In `@apps/web/src/components/ExperienceError.tsx`:
- Around line 8-11: Update the user-facing error strings in ExperienceError (the
mapping entries for "Response not successful: Received status code 401" and
"Missing or invalid credentials") to avoid exposing internal setup details;
replace the current ".env.local / Strapi Admin" instructions with a generic
message like "Configuration error. Please contact support." and, if you want dev
details, conditionally append the original detailed guidance only when running
in development (check NODE_ENV or a runtime isDevelopment flag) so production
users never see infrastructure paths.

In `@apps/web/src/components/sections/VideoHero.tsx`:
- Around line 39-48: The pauseOnScrollAway handler currently only resumes
playback when scrollY === 0; change it to use a resume threshold (e.g., resume
when scrollY <= 100) so partial upward scrolling restarts the video. Update the
logic in pauseOnScrollAway (referenced function) to compare against a numeric
threshold (or a named constant like resumeThreshold) instead of strict zero,
keeping the existing pause condition (scrollY > 100) and ensuring you reference
playerRef.current.pause() and playerRef.current.play() accordingly.

In `@apps/web/src/lib/content.ts`:
- Around line 145-150: The current cast (result as { errors?: Array<{ message?:
string }> }) is imprecise; instead import and use the appropriate Apollo type
(e.g., FetchResult or ApolloQueryResult from `@apollo/client` or
`@apollo/client/core`) or add a narrow type guard so you can access errors safely;
update the code around the graphqlErrors assignment (the graphqlErrors variable
and the result value returned from the GraphQL call) to use the correct imported
type or a small type-guard function that checks for an errors array, then keep
the existing mapping logic that builds msg and returns { data: null, error: new
Error(msg) } unchanged.

In `@mobile/android/app/src/main/graphql/ExperienceBySlug.graphql`:
- Line 2: Update the top-line comment to reflect the renamed field: replace
"section variants" with "blocks" so the comment reads that it fetches a single
Experience by slug with all blocks; edit the comment near the ExperienceBySlug
GraphQL definition (the header comment above the query) to reference "blocks"
instead of "section variants" to keep documentation consistent with the schema
change.
- Around line 11-73: The Android GraphQL query ExperienceBySlug.graphql is
missing the ComponentSectionsVideoHero fragment so VideoHero blocks won't be
fetched; add a branch inside the blocks selection matching "... on
ComponentSectionsVideoHero" and include the same fields used by web (e.g.,
__typename, id, heading, intro, video { documentId, title, slug, image { url,
alternativeText, width, height } } or whichever fields your app needs) to mirror
the web implementation and ensure Android can consume VideoHero content.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02852911-a3cd-47a9-b2df-cb2e333749ce

📥 Commits

Reviewing files that changed from the base of the PR and between 604f01d and 654b1c6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • DEV-COMMANDS.md
  • apps/cms/VIDEO-HERO-MANUAL-SETUP.md
  • apps/cms/package.json
  • apps/cms/schema.graphql
  • apps/cms/scripts/seed-easter.cjs
  • apps/cms/src/api/experience/content-types/experience/schema.json
  • apps/cms/src/components/sections/video-hero.json
  • apps/web/next-env.d.ts
  • apps/web/next.config.mjs
  • apps/web/package.json
  • apps/web/src/app/[slug]/[locale]/page.tsx
  • apps/web/src/app/[slug]/page.tsx
  • apps/web/src/app/page.tsx
  • apps/web/src/app/watch/easter/page.tsx
  • apps/web/src/components/ExperienceError.tsx
  • apps/web/src/components/sections/VideoHero.tsx
  • apps/web/src/components/sections/index.tsx
  • apps/web/src/components/sections/videoHeroFragment.ts
  • apps/web/src/env.ts
  • apps/web/src/lib/client.ts
  • apps/web/src/lib/content.ts
  • mobile/android/app/src/main/graphql/ExperienceBySlug.graphql
  • mobile/expo/src/lib/experienceService.test.ts
  • mobile/expo/src/lib/experienceService.ts
  • mobile/expo/src/lib/graphql/queries.ts
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
  • mobile/ios/package.json
  • package.json
  • packages/graphql/src/graphql-env.d.ts
  • packages/graphql/src/index.ts
  • packages/graphql/src/watchExperience.ts

Comment thread apps/cms/src/components/sections/video-hero.json
Comment thread apps/cms/VIDEO-HERO-MANUAL-SETUP.md Outdated
Comment thread apps/web/next-env.d.ts Outdated
Comment thread apps/web/package.json
Comment thread apps/web/src/app/[slug]/page.tsx
Comment thread apps/web/src/env.ts Outdated
Comment thread DEV-COMMANDS.md Outdated
Comment thread mobile/android/app/src/main/graphql/ExperienceBySlug.graphql
Comment thread mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
Comment thread mobile/ios/package.json Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
apps/web/src/lib/experience-metadata.ts (1)

24-30: Consider using a self-hosted default OG image.

The default OG image points to an external Unsplash URL. If Unsplash becomes unavailable or rate-limits requests, social previews will break. Consider hosting a default image on jesusfilm.org or a CDN you control for reliability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/experience-metadata.ts` around lines 24 - 30,
DEFAULT_OG_IMAGE currently references an external Unsplash URL which can be
rate-limited or unavailable; update the DEFAULT_OG_IMAGE constant to point to a
self-hosted or controlled CDN URL (e.g., an image served from jesusfilm.org or
our CDN), keeping the same shape (url, width, height, alt, type) and preserving
the "image/jpeg" as const typing so clients consuming DEFAULT_OG_IMAGE (the
constant symbol) continue to work.
apps/web/src/components/sections/VideoHero.tsx (2)

55-74: Callback dependencies may cause unintended player recreation.

The useEffect depends on onMutedChange and onPlayerReady. If the parent doesn't memoize these callbacks (which it does via useCallback on lines 155-161), changing parent state would dispose and recreate the player. The current implementation is correct, but this is fragile — consider documenting this requirement or using refs for callbacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/sections/VideoHero.tsx` around lines 55 - 74, The
effect is fragile because it re-runs when parent-provided callbacks change;
change the component to store onMutedChange and onPlayerReady in refs and use
the refs inside the effect so the effect only depends on src (and
videoRef/playerRef), or alternatively document that those callbacks must be
memoized; specifically create refs like onMutedChangeRef and onPlayerReadyRef,
update them on each render, call onPlayerReadyRef.current(player) after creating
the player, and inside player.on("volumechange") call
onMutedChangeRef.current(player.muted() ?? true) so the player is not
disposed/recreated when parent callbacks change.

39-48: Throttle scroll handler to avoid performance issues.

The pauseOnScrollAway callback fires on every scroll event without throttling. On pages with heavy scroll activity, this can cause performance degradation.

♻️ Proposed throttle implementation
+import { useCallback, useEffect, useRef, useState, useMemo } from "react"
+
+function throttle<T extends (...args: unknown[]) => void>(fn: T, ms: number): T {
+  let lastCall = 0
+  return ((...args) => {
+    const now = Date.now()
+    if (now - lastCall >= ms) {
+      lastCall = now
+      fn(...args)
+    }
+  }) as T
+}

// Inside VideoHeroPlayer:
-  const pauseOnScrollAway = useCallback(() => {
+  const pauseOnScrollAway = useMemo(() => throttle(() => {
     const scrollY = window.scrollY
     if (playerRef.current) {
       if (scrollY > 100) {
         playerRef.current.pause()
-      } else if (scrollY === 0) {
+      } else if (scrollY <= 10) {
         void playerRef.current.play()
       }
     }
-  }, [])
+  }, 100), [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/sections/VideoHero.tsx` around lines 39 - 48, The
scroll handler pauseOnScrollAway currently runs on every scroll event; wrap it
with a throttle (or use requestAnimationFrame batching) and wire the throttled
function as the window 'scroll' listener with proper cleanup to avoid
performance issues. Concretely, create a stable throttled handler (e.g., using
lodash.throttle or an rAF-based ref/ticking pattern) that calls
pauseOnScrollAway logic, ensure the throttled function is memoized or stored in
a ref so addEventListener/removeEventListener use the same reference, and
add/remove the listener inside a useEffect; reference pauseOnScrollAway,
playerRef, and the throttled handler when implementing these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/components/sections/VideoHero.tsx`:
- Around line 55-74: The effect is fragile because it re-runs when
parent-provided callbacks change; change the component to store onMutedChange
and onPlayerReady in refs and use the refs inside the effect so the effect only
depends on src (and videoRef/playerRef), or alternatively document that those
callbacks must be memoized; specifically create refs like onMutedChangeRef and
onPlayerReadyRef, update them on each render, call
onPlayerReadyRef.current(player) after creating the player, and inside
player.on("volumechange") call onMutedChangeRef.current(player.muted() ?? true)
so the player is not disposed/recreated when parent callbacks change.
- Around line 39-48: The scroll handler pauseOnScrollAway currently runs on
every scroll event; wrap it with a throttle (or use requestAnimationFrame
batching) and wire the throttled function as the window 'scroll' listener with
proper cleanup to avoid performance issues. Concretely, create a stable
throttled handler (e.g., using lodash.throttle or an rAF-based ref/ticking
pattern) that calls pauseOnScrollAway logic, ensure the throttled function is
memoized or stored in a ref so addEventListener/removeEventListener use the same
reference, and add/remove the listener inside a useEffect; reference
pauseOnScrollAway, playerRef, and the throttled handler when implementing these
changes.

In `@apps/web/src/lib/experience-metadata.ts`:
- Around line 24-30: DEFAULT_OG_IMAGE currently references an external Unsplash
URL which can be rate-limited or unavailable; update the DEFAULT_OG_IMAGE
constant to point to a self-hosted or controlled CDN URL (e.g., an image served
from jesusfilm.org or our CDN), keeping the same shape (url, width, height, alt,
type) and preserving the "image/jpeg" as const typing so clients consuming
DEFAULT_OG_IMAGE (the constant symbol) continue to work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c121d1fb-e7fe-40ed-8e88-5ba4c760413e

📥 Commits

Reviewing files that changed from the base of the PR and between 9096e45 and 4dea70d.

📒 Files selected for processing (6)
  • apps/web/src/app/[slug]/[locale]/page.tsx
  • apps/web/src/app/[slug]/page.tsx
  • apps/web/src/app/watch/easter/metadata.ts
  • apps/web/src/app/watch/easter/page.tsx
  • apps/web/src/components/sections/VideoHero.tsx
  • apps/web/src/lib/experience-metadata.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/app/watch/easter/page.tsx
  • apps/web/src/app/watch/easter/metadata.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/[slug]/page.tsx

@Kneesal
Copy link
Copy Markdown
Member Author

Kneesal commented Mar 9, 2026

Review feedback addressed (b3768ec, b496a60)

Fixed:

  • CMS (video-hero.json): streamingUrl is now required so editors cannot publish a Video Hero without a playable source.
  • Android: GraphQLContentClient.kt now uses experience.blocks instead of experience.sections (both call sites). ExperienceBySlug.graphql comment updated to "block variants".
  • Web (ExperienceError): 401 and "Missing or invalid credentials" now show a user-friendly message: "Unable to authenticate with the content service. Please contact support if this persists."
  • Web (VideoHero): Resume playback when scrollY < 50 instead of only scrollY === 0 for more responsive UX.

Not changed:

  • iOS: Regenerating Apollo artifacts for blocks and updating Swift mappers would require running iOS codegen in this repo; left for a follow-up or local run.
  • SwiftLint script (mobile/ios/package.json): Review suggested failing when SwiftLint is missing; not changed in this PR.
  • Default OG image (experience-metadata.ts): Still uses external URL; self-hosted fallback can be done in a separate change.
  • VideoHero throttle / callback refs: Nitpicks deferred; parent already memoizes callbacks.

@Kneesal Kneesal force-pushed the feat/video-hero-and-experience-blocks branch from b496a60 to 0c75c74 Compare March 9, 2026 22:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt (1)

47-63: ⚠️ Potential issue | 🟠 Major

Handle ComponentSectionsVideoHero in the Android block mapper.

experience.blocks now includes ComponentSectionsVideoHero, but this summary logic still only reads CTA/PromoBanner/InfoBlocks. For a VideoHero-only experience—like the Easter content added in this PR—Android will fall back to slug and an empty body because mobile/android/app/src/main/graphql/ExperienceBySlug.graphql also omits that inline fragment. Please add the VideoHero selection to the query and map its text fields here so the new block type produces usable content on Android.

Based on learnings: When GraphQL schema changes, update Kotlin call sites to match regenerated types in app/build/generated/source/apollo/forge/com/forge/mobile/graphql/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt`
around lines 47 - 63, The block mapper is missing handling for the new
ComponentSectionsVideoHero so VideoHero-only experiences get empty title/body;
update mobile/android/app/src/main/graphql/ExperienceBySlug.graphql to include
the inline fragment for ComponentSectionsVideoHero (select its text fields such
as heading, body, description as appropriate), regenerate the Apollo Kotlin
models, then in GraphQLContentClient.kt add mapping for
experience.blocks?.mapNotNull to check section?.onComponentSectionsVideoHero and
return its heading for title and its body/description for body alongside the
existing
onComponentSectionsCta/onComponentSectionsPromoBanner/onComponentSectionsInfoBlocks
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt`:
- Around line 47-63: The block mapper is missing handling for the new
ComponentSectionsVideoHero so VideoHero-only experiences get empty title/body;
update mobile/android/app/src/main/graphql/ExperienceBySlug.graphql to include
the inline fragment for ComponentSectionsVideoHero (select its text fields such
as heading, body, description as appropriate), regenerate the Apollo Kotlin
models, then in GraphQLContentClient.kt add mapping for
experience.blocks?.mapNotNull to check section?.onComponentSectionsVideoHero and
return its heading for title and its body/description for body alongside the
existing
onComponentSectionsCta/onComponentSectionsPromoBanner/onComponentSectionsInfoBlocks
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e99e2574-ee7e-4b36-a00a-41358613a0f2

📥 Commits

Reviewing files that changed from the base of the PR and between 4dea70d and b496a60.

📒 Files selected for processing (5)
  • apps/cms/src/components/sections/video-hero.json
  • apps/web/src/components/ExperienceError.tsx
  • apps/web/src/components/sections/VideoHero.tsx
  • mobile/android/app/src/main/graphql/ExperienceBySlug.graphql
  • mobile/android/app/src/main/kotlin/com/forge/mobile/GraphQLContentClient.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • mobile/android/app/src/main/graphql/ExperienceBySlug.graphql
  • apps/web/src/components/ExperienceError.tsx
  • apps/web/src/components/sections/VideoHero.tsx

@Kneesal Kneesal force-pushed the feat/video-hero-and-experience-blocks branch from 0c75c74 to 6c9e770 Compare March 9, 2026 22:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/web/src/app/[slug]/page.tsx (1)

38-44: ⚠️ Potential issue | 🟠 Major

Use the filtered block list for the empty-state guard.

This still treats null and Error entries as renderable content, so an experience with no valid blocks falls through to a blank page. The empty-state check should run on the narrowed blocks array instead.

Suggested fix
-  const experience = result.data
-  if (!experience?.blocks?.length) {
-    return <ExperienceEmpty />
-  }
-
-  const blocks = experience.blocks.filter(
-    (b): b is Section => b !== null && b.__typename !== "Error",
-  )
+  const blocks =
+    result.data?.blocks?.filter(
+      (b): b is Section => b !== null && b.__typename !== "Error",
+    ) ?? []
+
+  if (blocks.length === 0) {
+    return <ExperienceEmpty />
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[slug]/page.tsx around lines 38 - 44, The empty-state check
uses experience?.blocks length before filtering out null/Error items, so pages
with only invalid blocks render blank; update the guard to compute the filtered
array (const blocks = experience.blocks.filter((b): b is Section => b !== null
&& b.__typename !== "Error") ) first and then return <ExperienceEmpty /> when
blocks.length === 0, and thereafter render using the filtered blocks variable
(blocks) instead of experience.blocks.
apps/web/src/app/[slug]/[locale]/page.tsx (1)

35-40: ⚠️ Potential issue | 🟠 Major

Check emptiness after filtering invalid blocks.

experience.blocks.length still counts null and Error entries, so a response made entirely of invalid blocks skips ExperienceEmpty and renders a blank page instead. Filter first, then guard on blocks.length.

Suggested fix
-  const experience = result.data
-  if (!experience?.blocks?.length) {
-    return <ExperienceEmpty />
-  }
-
-  const blocks = experience.blocks.filter(
-    (b): b is Section => b !== null && b.__typename !== "Error",
-  )
+  const blocks =
+    result.data?.blocks?.filter(
+      (b): b is Section => b !== null && b.__typename !== "Error",
+    ) ?? []
+
+  if (blocks.length === 0) {
+    return <ExperienceEmpty />
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[slug]/[locale]/page.tsx around lines 35 - 40, The
emptiness check currently runs before filtering invalid entries, so null/Error
items can make the page skip ExperienceEmpty; change the order: first create the
filtered const blocks = experience.blocks.filter((b): b is Section => b !== null
&& b.__typename !== "Error"), then check if (!blocks.length) return
<ExperienceEmpty />; update any references that used the pre-filtered value to
use this filtered blocks variable (symbols: experience, blocks, Section,
ExperienceEmpty).
apps/web/src/components/sections/index.tsx (1)

25-27: ⚠️ Potential issue | 🟠 Major

Don’t silently drop unsupported block types.

return null still turns schema/query drift into missing page content with no signal. Please make this branch exhaustive or emit a warning/fallback so unsupported CMS blocks are detectable during rollout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/sections/index.tsx` around lines 25 - 27, The default
branch in the switch that handles sections currently does "return null", which
hides schema/query drift; update the default case in
apps/web/src/components/sections/index.tsx to surface unsupported blocks by
logging a clear warning (include the block identifier like block._type or
block._key / block.id) and rendering a visible, testable fallback (e.g., a
placeholder element with a data attribute indicating the unsupported block type)
so missing/unknown CMS blocks are detectable in QA/rollout; alternatively, make
the switch exhaustive by adding explicit handlers for the missing block types if
known and throw a dev-only error to fail fast during development.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/expo/src/lib/graphql/queries.ts`:
- Line 13: The import of the type WatchExperienceBlock is currently placed after
the export block which violates module-order rules; move the line importing
WatchExperienceBlock from "@forge/graphql" to the top of the module before any
export statements (ensure the import sits above the export of whatever is being
exported in this file) so the type import is hoisted and the file remains
lint-clean; update any references to WatchExperienceBlock accordingly if their
relative position changed.

In `@turbo.json`:
- Around line 29-30: The generate task currently only declares the output
"src/graphql-env.d.ts" causing stale cache when the CMS schema changes; update
the "generate" task in turbo.json (the "generate" entry that lists outputs) to
include the CMS schema file as an input/global dependency (e.g., add
"../../apps/cms/schema.graphql" to the task's inputs/globals) so Turbo will
invalidate and rerun codegen when the schema changes.

---

Duplicate comments:
In `@apps/web/src/app/`[slug]/[locale]/page.tsx:
- Around line 35-40: The emptiness check currently runs before filtering invalid
entries, so null/Error items can make the page skip ExperienceEmpty; change the
order: first create the filtered const blocks = experience.blocks.filter((b): b
is Section => b !== null && b.__typename !== "Error"), then check if
(!blocks.length) return <ExperienceEmpty />; update any references that used the
pre-filtered value to use this filtered blocks variable (symbols: experience,
blocks, Section, ExperienceEmpty).

In `@apps/web/src/app/`[slug]/page.tsx:
- Around line 38-44: The empty-state check uses experience?.blocks length before
filtering out null/Error items, so pages with only invalid blocks render blank;
update the guard to compute the filtered array (const blocks =
experience.blocks.filter((b): b is Section => b !== null && b.__typename !==
"Error") ) first and then return <ExperienceEmpty /> when blocks.length === 0,
and thereafter render using the filtered blocks variable (blocks) instead of
experience.blocks.

In `@apps/web/src/components/sections/index.tsx`:
- Around line 25-27: The default branch in the switch that handles sections
currently does "return null", which hides schema/query drift; update the default
case in apps/web/src/components/sections/index.tsx to surface unsupported blocks
by logging a clear warning (include the block identifier like block._type or
block._key / block.id) and rendering a visible, testable fallback (e.g., a
placeholder element with a data attribute indicating the unsupported block type)
so missing/unknown CMS blocks are detectable in QA/rollout; alternatively, make
the switch exhaustive by adding explicit handlers for the missing block types if
known and throw a dev-only error to fail fast during development.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1ff80d3-7cc3-4972-8289-157e866eadb1

📥 Commits

Reviewing files that changed from the base of the PR and between b496a60 and 6c9e770.

📒 Files selected for processing (10)
  • apps/web/src/app/[slug]/[locale]/page.tsx
  • apps/web/src/app/[slug]/page.tsx
  • apps/web/src/app/page.tsx
  • apps/web/src/components/sections/index.tsx
  • apps/web/src/lib/content.ts
  • mobile/expo/src/lib/experienceService.test.ts
  • mobile/expo/src/lib/experienceService.ts
  • mobile/expo/src/lib/graphql/queries.ts
  • package.json
  • turbo.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • mobile/expo/src/lib/experienceService.test.ts
  • mobile/expo/src/lib/experienceService.ts
  • apps/web/src/app/page.tsx
  • package.json

Comment thread mobile/expo/src/lib/graphql/queries.ts Outdated
Comment thread turbo.json
@Kneesal
Copy link
Copy Markdown
Member Author

Kneesal commented Mar 9, 2026

Review feedback (5f0225b)

Code changes:

  • Emptyness check: Slug and locale pages now filter blocks first, then check blocks.length, so all-invalid responses show empty state.
  • Unhandled block types: sections/index.tsx logs a dev warning for unhandled __typename so schema drift is visible.

Replied & resolved:

  • VIDEO-HERO-MANUAL-SETUP / stale year → file removed per review.
  • package.json lockfile → no new deps; lockfile in scope.
  • env.ts / .env.example → env changes dropped per review.
  • Android ExperienceBySlug → already using experience.blocks in GraphQLContentClient.
  • iOS package.json SwiftLint → dropped per review.

All outstanding review threads have been replied to and marked resolved.

@Kneesal Kneesal merged commit 19c0da1 into JesusFilm:main Mar 9, 2026
11 of 19 checks passed
Ur-imazing added a commit that referenced this pull request Mar 10, 2026
Update iOS data layer for CMS schema changes from #301 and #224:
- sections → blocks field rename (ExperienceBlocksDynamicZone)
- Add streamingUrl to VideoHero fragment, model, and mapper
- Regenerate Apollo codegen against updated schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ur-imazing added a commit that referenced this pull request Mar 10, 2026
* feat(ios): add GraphQL fragments and expand query for 10 section types

- 8 named fragments for leaf types (MediaCollection, CTA, VideoHero, Text,
  RelatedQuestions, BibleQuotesCarousel, Card, Video) reused across 3
  nesting levels
- Expand GetWatchExperience query with all 12 inline fragments including
  nested Container slots and Section wrapper content
- PromoBanner and InfoBlocks get id-only fragments (graceful skip)
- Broaden codegen operationSearchPaths to GraphQL/**/*.graphql

Made-with: Cursor

* chore(ios): regenerate Apollo codegen for expanded query

Generated types now include all 12 Experience section types, Container
slots, Section content zones, and 8 named fragments.

Made-with: Cursor

* feat(ios): add section model structs for leaf and structural types

- SectionModels.swift: shared types (UploadFileModel, VideoModel), enums,
  two-enum model (SectionContent + ExperienceSection)
- SectionLeafModels.swift: 8 leaf model structs with Codable + Sendable
- SectionStructuralModels.swift: ContainerSlot, ContainerSection,
  SectionWrapperSection for 3-level nesting
- ExperienceContent gains Codable for debug JSON encoding
- PromoBanner/InfoBlocks models removed (not applicable for mobile)

Made-with: Cursor

* feat(ios): expand GraphQLContentClient mappers for all section types

- SectionMappers.swift: fragment-based leaf mappers (8 types) as extension
  on GraphQLContentClient with shared helpers
- GraphQLContentClient rewritten: mapSection dispatches to fragment mappers
  + structural mappers for Container and Section wrapper
- Recursive nesting: Experience → Section → Container → leaf (3 levels)
- firstSectionTitle checks VideoHero, Text, Card, MediaCollection, CTA
- PromoBanner/InfoBlocks return nil (graceful skip)

Made-with: Cursor

* feat(ios): add debug view fetching sample Experience as JSON

- WatchHomeViewModel: add loadExperience(slug:locale:) exposing
  ExperienceContent with parsed sections
- ForgeRootView: ExperienceDebugView fetches experience and displays
  scrollable JSON dump of all parsed section types
- SwiftLint config: allow h1-h6 and id identifier names

Made-with: Cursor

* fix(ios): use correct slug 'easter' in debug view experience fetch

The debug view was fetching slug "experience" which doesn't exist;
the sample Experience has slug "easter".

Made-with: Cursor

* fix(ios): complete firstSectionTitle scan for all 10 section types

- card.cardTitle was returned unconditionally, stopping the scan even
  for empty titles; now guarded with !isEmpty
- Add missing checks for RelatedQuestions, BibleQuotesCarousel, Video
- Move firstSectionTitle to SectionMappers.swift to stay under class
  body length limit

Addresses CodeRabbit review on PR #298.

Made-with: Cursor

* fix(ios): add @mainactor to WatchHomeViewModel for UI thread safety

@observable state mutations (isLoading, homeError, experienceContent)
around awaits must happen on the main actor. SwiftUI expects UI-bound
state to be synchronized on @mainactor.

Addresses CodeRabbit review on PR #298.

Made-with: Cursor

* fix(ios): align data layer with CMS blocks rename and streamingUrl

Update iOS data layer for CMS schema changes from #301 and #224:
- sections → blocks field rename (ExperienceBlocksDynamicZone)
- Add streamingUrl to VideoHero fragment, model, and mapper
- Regenerate Apollo codegen against updated schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ios): revert CMS schema change, make VideoHero streamingUrl nullable

Revert accidental schema.graphql modification (bounded context violation).
Update VideoHero model and generated code to treat streamingUrl as
optional (String?) matching the current CMS schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ios): remove streamingUrl from VideoHero fragment until CMS data populated

The live CMS declares streamingUrl as non-null on VideoHero but existing
content has no value set, causing a GraphQL error at runtime. Remove the
field from the query for now; the model retains the optional property so
it can be re-added once CMS content is seeded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ios): use partial GraphQL data when sections have null required fields

Strapi returns errors alongside partial data when non-null fields are
null for individual sections (e.g. VideoHero without video or
streamingUrl). Instead of throwing on any GraphQL error, use partial
data when available — null sections are already skipped by compactMap
in the mapper. Only throw if no data is returned at all.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ios): propagate task cancellation into Apollo fetch

Replace withCheckedContinuation with withTaskCancellationHandler +
withCheckedThrowingContinuation so that Swift Task cancellation
cancels the in-flight Apollo network request via a thread-safe
ApolloRequestBox wrapper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web): add VideoHero component

1 participant