Skip to content

Feat(app): navigating guide view page to objective / subobjective - #529

Open
Aditya-Dawadikar wants to merge 4 commits into
bluelearn-org:mainfrom
Aditya-Dawadikar:feat/guide_view_page_to_objective
Open

Aditya-Dawadikar wants to merge 4 commits into
bluelearn-org:mainfrom
Aditya-Dawadikar:feat/guide_view_page_to_objective

Conversation

@Aditya-Dawadikar

@Aditya-Dawadikar Aditya-Dawadikar commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #515.

Going back from a guide opened in an objective sent the learner to the graph view and the first sub-objective, at the top of the page. They had to pick the sub-objective again and scroll back down. Now the objective page remembers where they were:

  • Browser back returns to the same view and sub-objective, scrolled to where they were.
  • The breadcrumb on the guide page links back to the exact sub-objective and scrolls to the guide they were reading. If the guide was opened from the graph view, the graph opens with that guide's panel showing.
  • Objective page: the view and open sub-objective are kept in the URL (?view=linear&target=<id>), and defaults are left out so a plain visit keeps a bare URL. Changes replace the current history entry, and shouldReload keeps those URL updates from refetching the objective.
  • Guide page: its URL carries ?objective=&target=, which drives both Prev/Next and the breadcrumb. The breadcrumb links to /objectives/<slug>?view=linear&target=<id>#guide-<slug>.
  • Scrolling: each guide card has id="guide-<slug>", so the router's built-in hash scrolling brings it into view. There's no custom scroll code.
  • Graph view: returning reads the #guide-<slug> hash and opens that node. This happens after the page loads in the browser, since the server never sees the hash.
  • Also added one new test data record for objective with multiple sub objectives

Builds on top of the PR #517

Type of change

  • Bug fix
  • Feature
  • Refactor (no behavior change)
  • Documentation
  • Build / tooling / CI
  • Other: _________

Verification

  • pnpm -r typecheck passes
  • pnpm -r build passes
  • Manually verified in a browser (for UI / behavior changes)
  • Followed the app layout conventions
  • No new dependencies, or new dependencies are justified and AGPL-compatible
  • Change does not violate any non-negotiable principle
  • Documentation only, no changes to the codebase, so no tests required

…ading

Going back from a guide opened in an objective reset the page to the graph and the default sub-objective at the top, so the learner had to find their place again. The objective page now remembers where they
were, both for browser back and for the breadcrumb.

Paths to trace:

1. Objective page state lives in the URL
   routes/objectives/$slug/index.tsx
   - validateSearch: parseObjectivePageSearch -> { view?: "linear", target?: <target node id> };
   the graph view and the default target are left out, so a plain visit keeps a bare URL.
   - The view and selection are seeded from the URL on mount
     (useObjectiveSelection(snapshot, { targetId: search.target })), then
     a useEffect writes them back with navigate({ replace: true,
     resetScroll: false }). A target already named in the URL is kept as
     written, so arriving never triggers a navigate of its own.
   - shouldReload: cause !== "stay" stops these URL writes from
     refetching the objective; entering the page still loads it.
   -> Browser back restores the view and sub-objective, and the router's
      scrollRestoration restores the scroll position.

2. Breadcrumb from the guide reader lands on the exact step
   routes/guides/$slug/index.tsx -> lib/breadcrumbs.ts
   - The guide URL already carries ?objective=&target= (from
     SubObjectiveStep / WalkthroughPanel). With a sequence, the objective
     crumb gets:
       search: { view: "linear", target: sequence.targetId } when
               search.target is set (opened from the linear view),
               otherwise nothing (opened from the graph)
       hash:   guideAnchor(slug) -> "guide-<slug>"
   - BreadcrumbOrigin / Breadcrumb now take optional search and hash;
     buildBreadcrumbs passes them through, and the reader's crumb <Link>
     uses them.

3. Scrolling to the guide (linear view)
   components/objective/SubObjectiveStep.tsx
   - Each guide step that can be opened gets id={guideAnchor(slug)}, plus
     scroll-mt-24 so the sticky navbar does not cover it.
   - Uses the router's defaultHashScrollIntoView (router.tsx): no custom
     scroll code.

4. Reopening the guide (graph view)
   routes/objectives/$slug/index.tsx
   - guideSlugFromAnchor(location.hash) finds the guide; a mount-only
     useEffect selects its node, which opens the panel on it. Browsers
     never send the hash to the server, so this waits until after
     hydration; seeding it into the first render caused a hydration
     mismatch.

5. Selection hook accepts a starting point
   components/objective/ObjectiveGraph.tsx
   - useObjectiveSelection(snapshot, initial?) seeds selectedId and
     targetId; an id that is no longer a target falls back to
     defaultTargetId as before. Existing callers (preview, revisions) are
     unchanged.

6. Fix: target listed twice in a sub-objective
   lib/objectiveSnapshot.ts
   - buildSubObjectives skips the target's own row when a stored sequence
     already ends with the target, since it appends the target once. This
     stops the duplicate step, React duplicate-key errors and a duplicate
     anchor id in the linear view, the contribution preview and revisions.
     The dedupe that worked around this in findGuideNeighbours is
     removed.
   - New helpers: ObjectivePageSearch, parseObjectivePageSearch,
     guideAnchor, guideSlugFromAnchor.

7. Seed data for manual verification
   supabase/seed.sql
   - "Make a 2D Platformer" (make-a-2d-platformer) reuses the Game
     Development guides across three sub-objectives (first is featured);
     "Sprites and Assets" sits only in the 2nd and 3rd, which reproduces
     the case in the issue.
lib/__tests__/objectiveSnapshot.test.ts
- buildSubObjectives: lists the target once when the stored sequence
  already ends with it (the duplicate-target fix).
- parseObjectivePageSearch: keeps view=linear and the target; leaves out
  the default graph view; drops values that are missing or malformed.
- guideAnchor / guideSlugFromAnchor: round-trip a slug; a hash that
  names no guide gives undefined.

lib/__tests__/breadcrumbs.test.ts
- buildBreadcrumbs passes an origin's search and hash through to the
  crumb, which the guide reader's link to the objective relies on.

components/objective/__tests__/SubObjectiveStep.test.tsx
- Only openable guides get the guide-<slug> anchor id that the router
  scrolls to; requests get none.

components/objective/__tests__/ObjectiveGraph.test.tsx
- useObjectiveSelection's starting point, through the two-target
  Reader harness (now takes initialView and initial):
  - reopens the linear view on the sub-objective it left
  - reopens the graph with the returned guide selected
  - falls back to the default for a target no longer in the objective
@Aditya-Dawadikar Aditya-Dawadikar changed the title Feat(app): guide view page to objective Feat(app): navigating guide view page to objective / subobjective Oct 18, 2026
@Aditya-Dawadikar
Aditya-Dawadikar marked this pull request as ready for review October 21, 2026 18:02
@jpotter08 jpotter08 added area:frontend Tasks related to frontend (app) area:database Tasks related to the database schema, migrations, or data management labels Oct 22, 2026
@kevin8999

Copy link
Copy Markdown
Member

LGTM!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:database Tasks related to the database schema, migrations, or data management area:frontend Tasks related to frontend (app)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Guide view page to Objective resets to the first sub-objective

4 participants