Merge pull request #320 from trails-cool/add-explore-proposal
Propose /explore page: local user discovery directory
This commit is contained in:
commit
65f8313f2b
6 changed files with 311 additions and 0 deletions
2
openspec/changes/add-explore-page/.openspec.yaml
Normal file
2
openspec/changes/add-explore-page/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-26
|
||||
108
openspec/changes/add-explore-page/design.md
Normal file
108
openspec/changes/add-explore-page/design.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
## Context
|
||||
|
||||
The Journal has no in-app discovery surface today. A signed-in user who wants to follow someone has to come in with a username from outside the app or notice a profile in the public activity feed on logged-out `/`. The four current prod users are all known to the operator, so this hasn't pinched yet — but the gap will become real as the user count grows, and worse once federation lands and remote actor discovery is also on the table.
|
||||
|
||||
The IA review (`docs/information-architecture.md`, Stream F) called this out as the highest-value missing surface. This design is the local-only v1: a paginated directory of public local users, ordered by recency of activity, plus a small "Active recently" sub-section to give the page some texture when the directory is small.
|
||||
|
||||
Existing infrastructure this builds on:
|
||||
|
||||
- `users` table has `profile_visibility` (`public` | `private`) — the directory's primary opt-out.
|
||||
- `activities` table has `created_at` and `visibility` — used for ordering by recency and for the "Active recently" filter.
|
||||
- `apps/journal/app/lib/follow.server.ts` already provides `countFollowers` and `getFollowState` per user — directly reusable for the per-row data.
|
||||
- `apps/journal/app/components/FollowButton.tsx` already renders the right Follow / Requested / Unfollow state — directly reusable.
|
||||
- The locked-account access rule (`social-follows`) already excludes private profiles from public listings; we follow the same pattern.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A signed-in user who lands on `/explore` can browse local public users and tap Follow / Request to follow without leaving the page.
|
||||
- An anonymous visitor can browse the same directory (the data is public anyway), so first-visit discovery doesn't require sign-up.
|
||||
- The page is paginated and sortable by a stable rule (recency of activity, descending) so it works at 4 users today and at 4,000 users later without a rewrite.
|
||||
- Private profiles, banned/suspended users (when those statuses exist), and the demo persona are excluded.
|
||||
- The capability lives in its own spec file; the directory query lives in its own server lib file. No bleed into `social-follows` or `activity-feed`.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Search. Deferred to v2. With four prod users, a paginated list is enough; users with known handles can already reach `/users/<username>` directly.
|
||||
- Algorithmic curation. No "people you might know" recommendations, no graph traversal, no engagement-based ranking. Recency-of-activity is the only signal.
|
||||
- Remote actor discovery. Federation is `social-federation`'s problem when that lands; v1 is local-only.
|
||||
- A dedicated user-detail card on `/explore`. Each row is a compact summary; deep dive is `/users/:username`.
|
||||
- Activity content on `/explore`. The directory is *people*, not *posts*. The Public view of `/feed` (Stream A) is the content discovery surface.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Anonymous access is allowed
|
||||
|
||||
`/explore` is reachable to logged-out visitors. Why:
|
||||
|
||||
- The data shown — public profiles only — is already reachable without auth (`/users/<username>` is anonymously reachable for public profiles), so there's no privacy concern.
|
||||
- It's a stronger first-visit experience than the current logged-out home, which is hero + marketing + public *activity* feed. A visitor can see who's on the instance and decide to sign up to follow them.
|
||||
- It mirrors how `/users/<username>` and the public activity feed treat anonymous visitors.
|
||||
|
||||
The Follow button on each row only renders for signed-in viewers; anonymous viewers see the row content but no action button.
|
||||
|
||||
**Alternative considered:** require auth, redirect anonymous to `/auth/login`. Rejected because it makes the discovery surface less valuable for the population it most needs to convert (first-time visitors).
|
||||
|
||||
### Directory is ordered by `MAX(activities.created_at)` descending
|
||||
|
||||
The most-recent-activity ordering keeps the directory feeling alive without algorithmic complexity. Users with no activities yet sort to the end (by `users.created_at` descending as a tiebreaker, so newer users appear ahead of older inactive ones).
|
||||
|
||||
**Alternatives considered:**
|
||||
|
||||
- **Alphabetical by username.** Predictable but boring; surfaces inactive accounts to the top half.
|
||||
- **Random shuffle.** Fair to small users but not bookmarkable, breaks pagination.
|
||||
- **Follower count.** Reasonable but has a Matthew-effect tilt; one or two heavy users would dominate page 1.
|
||||
|
||||
Recency-by-activity is the simplest signal that meaningfully reflects "who's active here right now."
|
||||
|
||||
### "Active recently" sub-section is the same query, sliced
|
||||
|
||||
The page renders an "Active recently" strip at the top (top 5 users with at least one public activity in the last 30 days). It's the same join as the main directory; the strip is just a different `WHERE` and `LIMIT`. We don't denormalize anything, don't precompute anything, don't cache anything. Below it is the full paginated directory.
|
||||
|
||||
**Why:** at 4 users this strip is the *whole* directory. At 4,000 users it's a useful signal-of-life. We can revisit when there's data to revisit on.
|
||||
|
||||
### Paginate via offset, not cursor (yet)
|
||||
|
||||
The directory uses standard `?page=N&perPage=20` offset pagination, capped at 100 per page. Why:
|
||||
|
||||
- The data is small (single-digit users today, low hundreds soon).
|
||||
- Recency-of-activity is unstable enough as a sort key that cursor pagination would need to break ties on `users.id` anyway, and the win over offset is marginal at this scale.
|
||||
- Offset pagination is the simplest thing that works.
|
||||
|
||||
When the directory hits ~10K users we'll revisit. Cursor pagination's the right answer there; not now.
|
||||
|
||||
**Alternative considered:** keyset cursor pagination like `/notifications`. Rejected as premature.
|
||||
|
||||
### Navbar entry for signed-in users only
|
||||
|
||||
Adding "Explore" to the navbar for signed-in users keeps the discovery surface visible from any page. For anonymous visitors, the navbar stays minimal (Login / Register CTAs) and `/explore` is reached via a link on the visitor home (`/`). The navbar redesign (Stream C) will work out the visual treatment; this proposal just specifies that the entry exists for signed-in users.
|
||||
|
||||
**Alternative considered:** put "Explore" in the navbar for everyone. Rejected because the anonymous navbar is intentionally minimal — anything other than auth CTAs dilutes the conversion goal of the visitor home.
|
||||
|
||||
### Privacy filter is `profile_visibility = 'public'` AND not in an exclusion set
|
||||
|
||||
The directory query filters:
|
||||
|
||||
1. `profile_visibility = 'public'` — Mastodon-style locked accounts opt out by their visibility setting, same as `/users/:username/followers` already enforces.
|
||||
2. **Banned/suspended users** — when those statuses exist (they don't yet; this is a forward-compat scaffold). The query should be written so adding a `status` column or similar is a one-line filter change, not a query rewrite.
|
||||
3. **Demo persona** — `apps/journal/app/lib/demo-bot.server.ts` exposes the persona username; exclude it from the directory so the demo bot doesn't appear in real discovery.
|
||||
|
||||
The query lives in `apps/journal/app/lib/explore.server.ts`. Tests cover each exclusion as a separate scenario.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Cold-start UX at 4 users.** The directory will be tiny. → Acceptable; the "Active recently" strip handles the "looks empty" feeling, and the public activity feed on logged-out `/` already does the same job from the content angle.
|
||||
- **Recency ordering tilts toward power users.** A user who posts daily will perpetually outrank a quieter user. → Acceptable for v1; if it becomes a fairness issue we add a tiebreaker (e.g., bucket by week, randomize within bucket). Don't optimize speculatively.
|
||||
- **No search.** A user looking for a specific handle can't search. → Mitigated by `/users/<username>` working directly. Search is a v2 follow-up.
|
||||
- **Federation collision.** Once federation lands, the directory will need a "local" / "remote" distinction. → The capability spec says "local users" explicitly; remote-actor discovery is a `social-federation` add-on that can introduce its own surface or extend `/explore` later.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
No data migration. The directory is a JOIN of existing `users` and `activities` rows. Deploy is a normal app rollout. Rollback is reverting the route registration in `routes.ts`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Bio in the directory row?** The user table has a `bio` column. Showing the first ~120 chars per row gives the directory more character but pads the layout. Default plan: include a truncated bio. Easy to drop if it's noisy.
|
||||
- **Follower count visible in the directory?** Default plan: yes (mirrors how `/users/:username` shows it). A dissenter could argue this creates social-pressure metric-anxiety; counter-argument is that Mastodon, the comparable, shows it.
|
||||
- **Does `/explore` deserve its own SSE event for follow-state changes?** The Follow button on a row should reflect "you just followed this person" without a hard reload. Today the FollowButton component handles this via local state on `/users/<username>`; the same pattern works on `/explore` with the same component. No new SSE channel needed.
|
||||
34
openspec/changes/add-explore-page/proposal.md
Normal file
34
openspec/changes/add-explore-page/proposal.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
## Why
|
||||
|
||||
A user who wants to follow someone on this instance has no in-app path to discover them — they need a username from outside the app, type it into `/users/<username>` directly, or stumble onto a profile via the public activity feed on logged-out `/`. With four prod users today this is just a friction; with federation in Phase 2 it becomes a real gap. Add `/explore` as the in-app discovery surface so finding people to follow is a first-class action, not a workaround.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new `/explore` route to the Journal app, reachable to both signed-in users and anonymous visitors.
|
||||
- Render a paginated directory of local users with `profile_visibility = 'public'`, ordered by most-recent activity (`MAX(activities.created_at)` per user, descending; users with no activities sort to the end by `users.created_at`).
|
||||
- Within the directory page, surface a small "Active recently" section at the top — the top N (5 by default) public users who posted a public activity in the last 30 days. Same data set, just sliced and surfaced.
|
||||
- Exclude private (locked) profiles, banned/suspended users (when those statuses exist), and the demo persona from the directory — they have explicit opt-outs from public discovery.
|
||||
- Each directory row shows: display name, handle (`@username`), short bio (truncated), follower count (accepted only), and a Follow / Requested / Unfollow button (locked-account semantics from `social-follows` apply, so a public-target row shows Follow, etc.). The button only renders for signed-in viewers.
|
||||
- Add an "Explore" entry to the navbar for signed-in users (visible alongside Feed / Routes / Activities). Anonymous visitors reach `/explore` via a link on the visitor home page, not via the navbar.
|
||||
- No search in v1. With four prod users a paginated list is enough; search is deferred — typing `/users/<username>` already partially solves it for known handles.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `explore`: discovery directory of local users for follow recommendations. Owns the `/explore` route, the directory query rules, the "Active recently" sub-list, the access model (anonymous + signed-in), and the navbar entry.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `social-follows`: the directory needs a count of accepted followers per public user, which is already available via `countFollowers`. No new requirement; cross-reference only.
|
||||
- `journal-landing`: the navbar entries grow by one ("Explore" for signed-in users). The visitor home gains a link to `/explore` so anonymous visitors have an in-app path to the directory.
|
||||
|
||||
## Impact
|
||||
|
||||
- **New code:** `apps/journal/app/routes/explore.tsx`, `apps/journal/app/lib/explore.server.ts` (the directory query and "Active recently" slice), navbar entry in `apps/journal/app/root.tsx`, link from the visitor home in `apps/journal/app/routes/home.tsx`, i18n keys in `packages/i18n/src/locales/{en,de}.ts`.
|
||||
- **Modified code:** none — the existing helpers (`countFollowers`, `getFollowState`) cover the per-row data; the new query is additive in `explore.server.ts`.
|
||||
- **No DB migrations:** the directory query is a JOIN of existing tables (`users`, `activities`); no new columns or tables.
|
||||
- **No new dependencies.**
|
||||
- **Privacy:** the directory is public-by-default and respects the profile-visibility opt-out. No private profile leaks.
|
||||
- **Federation:** out of scope for v1. Remote-actor discovery is `social-federation`'s problem when that lands.
|
||||
- **OpenSpec index:** `openspec/CAPABILITIES.md` gains an entry under the "Social" section.
|
||||
101
openspec/changes/add-explore-page/specs/explore/spec.md
Normal file
101
openspec/changes/add-explore-page/specs/explore/spec.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Local user directory at `/explore`
|
||||
The Journal SHALL expose a route at `/explore` that renders a paginated directory of local users with `profile_visibility = 'public'`. The directory SHALL be reachable to both signed-in users and anonymous visitors. Each row SHALL display the user's display name, handle (`@username`), follower count (accepted only — see `social-follows` spec), and a short truncated bio when present. For signed-in viewers each row SHALL also render a Follow / Requested / Unfollow button reflecting the viewer's current relationship to that user (using the same locked-account semantics as the profile page); for anonymous viewers the button SHALL NOT render.
|
||||
|
||||
#### Scenario: Signed-in user loads /explore
|
||||
- **WHEN** a signed-in user requests `/explore`
|
||||
- **THEN** the page renders a list of public local users with display name, handle, follower count, truncated bio, and a Follow / Requested / Unfollow button per row
|
||||
|
||||
#### Scenario: Anonymous visitor loads /explore
|
||||
- **WHEN** an unauthenticated visitor requests `/explore`
|
||||
- **THEN** the page renders the same list of public local users (display name, handle, follower count, truncated bio), but no Follow / Requested / Unfollow button is rendered on any row
|
||||
|
||||
#### Scenario: Truncated bio
|
||||
- **WHEN** a user with a bio longer than 120 characters appears in the directory
|
||||
- **THEN** the row shows the first 120 characters followed by an ellipsis
|
||||
|
||||
#### Scenario: User with no bio
|
||||
- **WHEN** a user with no bio (`bio IS NULL` or empty string) appears in the directory
|
||||
- **THEN** the row simply omits the bio area; the row remains valid
|
||||
|
||||
### Requirement: Directory is ordered by most-recent public activity
|
||||
The directory SHALL be ordered by `MAX(activities.created_at) DESC` per user, considering only activities with `visibility = 'public'`. Users with no public activities SHALL sort to the end of the list, ordered among themselves by `users.created_at DESC` (newer registrations first). The ordering SHALL be deterministic — ties on the primary sort key SHALL be broken by `users.id` (descending) so pagination doesn't omit or duplicate rows.
|
||||
|
||||
#### Scenario: Active user appears before inactive user
|
||||
- **WHEN** user A has a public activity from yesterday and user B has no public activities
|
||||
- **THEN** A appears before B in the directory listing
|
||||
|
||||
#### Scenario: More-recently-active user appears first
|
||||
- **WHEN** user A's most recent public activity is from today and user B's is from last week
|
||||
- **THEN** A appears before B in the directory listing
|
||||
|
||||
#### Scenario: Tied recency falls back to user id
|
||||
- **WHEN** users A and B share the same `MAX(activities.created_at)` (or both have no public activities)
|
||||
- **THEN** the ordering between them is determined by `users.id` descending (deterministic), so neither is omitted nor duplicated when paginating
|
||||
|
||||
### Requirement: Excluded users
|
||||
The directory SHALL exclude:
|
||||
|
||||
1. Users with `profile_visibility = 'private'` — they have explicitly opted out of public discovery (Mastodon-style locked accounts).
|
||||
2. The instance's demo persona, identified by the username returned by `loadPersona()` — the demo bot is not a real user and should not appear in real discovery.
|
||||
3. (Forward-compat) Users in any future banned/suspended state — when such a status column exists, it SHALL be added to the exclusion filter.
|
||||
|
||||
Excluded users SHALL NOT appear on `/explore` even if they have public activities and would otherwise sort to the top of the directory.
|
||||
|
||||
#### Scenario: Private profile is excluded from the directory
|
||||
- **WHEN** user A has `profile_visibility = 'private'` and any activity history
|
||||
- **THEN** A does not appear in the `/explore` directory regardless of which page is requested
|
||||
|
||||
#### Scenario: Demo persona is excluded
|
||||
- **WHEN** the demo persona username (per `loadPersona()`) matches a row that would otherwise appear
|
||||
- **THEN** that row is filtered out of the directory
|
||||
|
||||
#### Scenario: Public user with no activities is included
|
||||
- **WHEN** user A has `profile_visibility = 'public'` but has never created an activity
|
||||
- **THEN** A still appears in the directory (sorted toward the end by the recency rule)
|
||||
|
||||
### Requirement: "Active recently" sub-section
|
||||
The `/explore` page SHALL render an "Active recently" sub-section at the top of the directory, listing up to N (default 5) public users who have created at least one public activity in the last 30 days, ordered by `MAX(activities.created_at) DESC`. The sub-section SHALL apply the same exclusion rules as the main directory (private profiles, demo persona, future banned/suspended users). When fewer than 1 user qualifies, the sub-section SHALL be omitted entirely (no empty header).
|
||||
|
||||
#### Scenario: Active-recently strip rendered with qualifying users
|
||||
- **WHEN** at least one public local user (excluding private/demo) has a public activity within the last 30 days
|
||||
- **THEN** the page renders the "Active recently" sub-section above the main directory, with up to 5 such users
|
||||
|
||||
#### Scenario: No qualifying users hides the strip
|
||||
- **WHEN** no public local user (excluding private/demo) has any public activity within the last 30 days
|
||||
- **THEN** the "Active recently" sub-section is not rendered at all; the main directory is the only listing on the page
|
||||
|
||||
#### Scenario: Strip respects exclusion rules
|
||||
- **WHEN** a private profile or the demo persona has a recent public activity
|
||||
- **THEN** they are not included in the "Active recently" strip — the same exclusion rules apply as for the main directory
|
||||
|
||||
### Requirement: Pagination
|
||||
The directory SHALL paginate via `?page=N` (1-indexed) and `?perPage=K` query parameters. Page size SHALL default to 20 per page and SHALL be capped at 100; `perPage` values outside `[1, 100]` SHALL be clamped to that range without raising an error. The response SHALL surface a "Next page" link when more rows exist past the current page, and a "Previous page" link when `page > 1`.
|
||||
|
||||
#### Scenario: Default pagination
|
||||
- **WHEN** a visitor loads `/explore` with no query params
|
||||
- **THEN** the page renders the first 20 users in the directory ordering
|
||||
|
||||
#### Scenario: Subsequent page
|
||||
- **WHEN** a visitor loads `/explore?page=2`
|
||||
- **THEN** the page renders rows 21–40 in the directory ordering, with a "Previous page" link to `?page=1` and (if more rows exist) a "Next page" link to `?page=3`
|
||||
|
||||
#### Scenario: Out-of-range perPage is clamped
|
||||
- **WHEN** a visitor loads `/explore?perPage=10000` or `/explore?perPage=0`
|
||||
- **THEN** the loader clamps `perPage` to `[1, 100]` and renders normally — no 400 response
|
||||
|
||||
#### Scenario: Page beyond the last
|
||||
- **WHEN** a visitor loads `/explore?page=N` where N is past the last populated page
|
||||
- **THEN** the response renders the empty list and a "Previous page" link; no 404
|
||||
|
||||
### Requirement: Navbar entry for signed-in users
|
||||
The Journal navbar SHALL include an "Explore" entry for signed-in users, linking to `/explore`. Anonymous visitors SHALL NOT see this entry in the navbar — they reach `/explore` via the visitor home (see `journal-landing` spec). The visual treatment of the entry is governed by the navbar redesign (Stream C in `docs/information-architecture.md`); this requirement only mandates that the entry exists.
|
||||
|
||||
#### Scenario: Signed-in user sees the Explore entry
|
||||
- **WHEN** a signed-in user loads any page
|
||||
- **THEN** the navbar includes an "Explore" link to `/explore`
|
||||
|
||||
#### Scenario: Anonymous user does not see the Explore entry in the navbar
|
||||
- **WHEN** an unauthenticated visitor loads any page
|
||||
- **THEN** the navbar does not include an Explore entry; the visitor home provides the in-app path to `/explore` instead
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Visitor home links to /explore
|
||||
For signed-out visitors on the flagship instance and self-hosted instances alike, the visitor home (`/`) SHALL include a link to `/explore` so anonymous visitors have an in-app path to browse the local user directory before signing up. The link SHALL be visually secondary to the auth CTAs (it does not compete with Register / Sign In as the conversion goal) but reachable on the same first-fold page section.
|
||||
|
||||
#### Scenario: Visitor home renders the Explore link
|
||||
- **WHEN** a signed-out visitor loads `/`
|
||||
- **THEN** the page includes a link to `/explore` somewhere within the hero / CTAs section, visually secondary to Register and Sign In
|
||||
|
||||
#### Scenario: Signed-in user does not see the visitor-home Explore link
|
||||
- **WHEN** a signed-in user loads `/`
|
||||
- **THEN** the visitor-home layout is not rendered (the personal dashboard takes its place); the navbar's Explore entry is the signed-in user's path to `/explore`
|
||||
54
openspec/changes/add-explore-page/tasks.md
Normal file
54
openspec/changes/add-explore-page/tasks.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
## 1. Server-side directory query
|
||||
|
||||
- [ ] 1.1 Create `apps/journal/app/lib/explore.server.ts` with `listDirectory({ page, perPage })` returning `{ rows, totalCount }`. Rows include `id`, `username`, `displayName`, `bio`, `latestActivityAt` (nullable), and the user's `profileVisibility`. Filters: `profile_visibility = 'public'`, exclude demo persona, exclude future banned/suspended (placeholder filter — leave a TODO for when the column exists).
|
||||
- [ ] 1.2 Order: `LEFT JOIN activities ON activities.owner_id = users.id AND activities.visibility = 'public'`, then `MAX(activities.created_at) DESC NULLS LAST`, tiebreaker `users.id DESC`.
|
||||
- [ ] 1.3 Add `listActiveRecently(limit = 5)` returning the same row shape, filtered to users with at least one public activity in the last 30 days, capped at `limit`.
|
||||
- [ ] 1.4 Add `countDirectory()` returning the total number of users that match the directory's filter, for pagination math.
|
||||
- [ ] 1.5 Add a follower-count batch helper (`countFollowersBatch(userIds)`) so the page doesn't fan out N+1 queries — one query per page load.
|
||||
- [ ] 1.6 Add `apps/journal/app/lib/explore.integration.test.ts` covering: public user appears, private user excluded, demo persona excluded, ordering by recency, tiebreaker by id, "Active recently" 30-day filter, "Active recently" cap.
|
||||
|
||||
## 2. Route + UI
|
||||
|
||||
- [ ] 2.1 Register `/explore` in `apps/journal/app/routes.ts`.
|
||||
- [ ] 2.2 Create `apps/journal/app/routes/explore.tsx`:
|
||||
- Loader reads `?page=` (default 1) and `?perPage=` (default 20, clamp `[1, 100]`); fetches `listActiveRecently(5)`, `listDirectory({ page, perPage })`, `countDirectory()`, and (for signed-in viewers) per-row follow state via existing `getFollowState` batched.
|
||||
- Component renders the "Active recently" strip (if non-empty), then the main directory list, then "Previous" / "Next" links.
|
||||
- [ ] 2.3 Reuse `<FollowButton>` for the per-row action; render only when the loader returns a signed-in viewer.
|
||||
- [ ] 2.4 Bio truncation utility — first 120 characters + ellipsis, plain string slice (no markdown rendering yet).
|
||||
- [ ] 2.5 Empty-state copy when the directory itself is empty (no public users at all).
|
||||
- [ ] 2.6 Page-beyond-last empty state — render the empty list with a "Previous page" link, no 404.
|
||||
|
||||
## 3. Navbar + visitor home links
|
||||
|
||||
- [ ] 3.1 Add the "Explore" entry to the signed-in navbar in `apps/journal/app/root.tsx`, positioned alongside Feed / Routes / Activities. Visual treatment is plain for now; the navbar redesign (Stream C) refines it later.
|
||||
- [ ] 3.2 Add a link to `/explore` on the visitor home (`apps/journal/app/routes/home.tsx`, signed-out branch) — secondary to the Register / Sign In CTAs but on the same fold. Wording: e.g. "Browse who's here →".
|
||||
- [ ] 3.3 No new link on the signed-in home — the navbar entry covers it.
|
||||
|
||||
## 4. i18n
|
||||
|
||||
- [ ] 4.1 Add to `packages/i18n/src/locales/en.ts` and `de.ts`:
|
||||
- `explore.title`, `explore.heading`, `explore.empty`, `explore.activeRecently.heading`
|
||||
- `nav.explore`
|
||||
- `home.exploreLink` (visitor-home wording)
|
||||
- [ ] 4.2 No i18n changes needed for FollowButton — it already uses the `social.*` keys.
|
||||
|
||||
## 5. Spec promotion + index
|
||||
|
||||
- [ ] 5.1 At archive time, promote `openspec/changes/add-explore-page/specs/explore/spec.md` to `openspec/specs/explore/spec.md` (no purpose section needed yet — the `## ADDED Requirements` content becomes the spec body).
|
||||
- [ ] 5.2 At archive time, merge the `journal-landing` delta into `openspec/specs/journal-landing/spec.md`.
|
||||
- [ ] 5.3 Add an entry for `explore` in `openspec/CAPABILITIES.md` under the Social section.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- [ ] 6.1 Integration tests for the directory query (covered in 1.6).
|
||||
- [ ] 6.2 Add an e2e test in `e2e/explore.test.ts`:
|
||||
- Anonymous loads `/explore` and sees the directory but no Follow buttons.
|
||||
- Signed-in loads `/explore` and sees Follow buttons; clicking Follow on a public profile transitions the row to Unfollow without a hard reload.
|
||||
- A private profile does not appear in the directory regardless of which user is viewing.
|
||||
- [ ] 6.3 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e` before opening the PR.
|
||||
|
||||
## 7. PR + rollout
|
||||
|
||||
- [ ] 7.1 Open a PR with the change, link to `openspec/changes/add-explore-page/`, enable auto-merge.
|
||||
- [ ] 7.2 After merge, archive the change (`/opsx:archive add-explore-page`) so the deltas promote into top-level specs.
|
||||
- [ ] 7.3 Update `docs/information-architecture.md` to flip Stream F to ✅ Shipped and refresh the navbar diagram to include "Explore".
|
||||
Loading…
Add table
Add a link
Reference in a new issue