jc-main-develop
Use when building new pages or components for the JapaConnect Main App (apps/main, port 3000, SSR). Covers SvelteKit 2 + Svelte 5 Runes + Bits UI + Tailwind v4 + LayerChart 2.0.1, the unified X.com-st
SKILL.md.claude/skills/jc-main-develop/SKILL.md
GitHubJapaConnect Main App — Development Skill
You are building JapaConnect's public-facing community platform. SSR via adapter-node + Bun, port 3000. The single most important rule for layout: everything renders inside the unified app shell (below). This is the structural ground truth — do not build any surface as a standalone layout except the three documented exceptions.
⛔ API-READINESS GATE — RUN BEFORE ANY IMPLEMENTATION
Before writing a single line for any page or component:
- List every API endpoint the feature will consume (reads + mutations).
- Verify each exists — check the JEOS
api-routes-catalog.md, or probe the running backend (curl -s http://localhost:8080/api/v1/...). - If ANY endpoint is missing or returns 404: STOP. Do not implement. Do not mock. Do not hardcode placeholder data. Output the alert and wait:
⚠️ MISSING BACKEND API ROUTES — implementation paused
Feature: [feature name]
Missing routes:
- GET /api/v1/...
Action needed: implement these routes in the Go backend before frontend work continues.
Never work around a missing endpoint. Rule 16 (no hardcoded data) makes mocking a violation, not a shortcut.
Non-Negotiable Stack
- SvelteKit 2.x, adapter-node, Bun runtime · Svelte 5 strict Runes (
$state,$derived,$props,$effect) - Bits UI (latest stable), dot-notation (
<Dialog.Root>,<Select.Trigger>) for ALL interactive primitives - Tailwind CSS v4 — utilities via
class; theme via CSS custom properties; dark mode =[data-theme="dark"]fromjc_themecookie (never Tailwinddark:) - LayerChart
2.0.1(pinned;bun add layerchart@2.0.1) for ALL data visualization · Lucide Svelte icons · TypeScript, zeroany - Forbidden:
export let,$:,on:click,createEventDispatcher, localStorage (auth or UI state), AI attribution in comments/commits
THE APP SHELL — Structural Ground Truth (ADR-058)
The main app is one shell with a short, closed list of exceptions. Build every surface inside it. Never revert to a top-nav bar or a per-page bespoke layout.
Desktop: three columns
┌──────────────┬────────────────────────────┬──────────────────────────┐
│ LEFT NAV │ CENTER COLUMN │ RIGHT SIDEBAR │
│ collapsible │ tab bar → sort bar → │ sticky search + avatar │
│ 260 / 72px │ scrolling content ~600px │ then contextual widgets │
│ (fixed) │ │ ~350px │
└──────────────┴────────────────────────────┴──────────────────────────┘
Grid: grid-template-columns: 260px 1fr 350px (nav expanded) / 72px 1fr 350px (collapsed). Below 1280px hide the right sidebar; below 768px collapse to a single column + bottom tab bar. Per-route change happens inside the center column (tabs) and inside the right sidebar (widgets) — never by discarding the shell.
Components: AppShell.svelte (grid) wraps LeftNav.svelte + center <slot> + RightSidebar.svelte.
Left nav — collapsible (Hashnode-style)
- Expanded (~260px): icon + label; Spaces sub-nav visible; full Compose button.
- Collapsed (~72px): icons only; Spaces → icon + hover-flyout; Compose → circular FAB.
- Chevron toggle by the logo. Persist state server-side via
PATCH /api/v1/me/accountunderui.nav_collapsed— never localStorage. Default expanded desktop / collapsed tablet. - Items from
GET /api/v1/menus/primary_nav(RBAC +visibility-gated; supportsis_ad_slotitems). Seed order: Explore · Home · Discover · Notifications · Messages · Profile — divider — Spaces (sub-nav) · Forum · Blog · Universities · Compare · [More] — divider — Compose — avatar + Space switcher (bottom; Space switcher stays here, not in the account menu).
Center column
Tab bar at top swaps content per route (Home: Following/For You · Space: Feed/Forum/Members/About · Profile: Posts/Replies/Media/Likes/Spaces · Forum: All/Q&A/Discussions/Solved · Blog: Latest/Featured/Series · Discover: Spaces/People/Organizations/Tags/Universities). Sort bar (Relevant/Latest/Popular/Rising) sits below the tab bar on feed surfaces, position: sticky; top: var(--nav-height).
FeedCard is ONE variant-driven component — never a different card per content type. Identical base for every type: avatar, name, Space badge, timestamp, content-type pill, preview (title+excerpt or text), engagement bar. Type extras are additive props (showVoteRail, showFeatureImage), not layout forks. Native ad slots (feed_native_*) and inline form embeds render as cards inside the stream.
Right sidebar
Sticky top bar: global search input + user avatar at far right (→ account dropdown). Below: route-aware widgets — global defaults (trending tags, suggested spaces, sidebar_top ad, optional form embed) + per-route overlays via a sidebar slot.
Mobile (≤768px)
Single column. Left nav → hamburger (top-left). Right sidebar hidden (widgets via "More info" affordance). Bottom tab bar: Home · Spaces · Compose · Notifications · Profile. Compose opens the modal composer.
Compose flow
Compose button/tab → ComposeModal.svelte with a content-type selector. Discussion-type posts compose inline in the modal; blog/CPT route to /create. /forum/[category]/new remains for full-page new threads.
THE THREE EXCEPTIONS (only these break the shell)
1. Blog single (/blog/[slug]) & CPT singles — dev.to reading layout
- Left nav auto-collapses to 72px.
ReactionRail.svelte: a borderless, transparent vertical strip in the article's LEFT margin (react + count, bookmark, comment count, share). It belongs to the article — must read as distinct from the bordered 72px nav, separated by a clear gap. Do NOT box it or make it look like a second nav.- Center: article (cover image, title, author line,
.proseTipTap HTML with.prose-table-wrappertables, inline ad slotscontent_top/content_inline_*). - Right:
CreatorCard.svelteREPLACES the search/avatar bar — avatar, name,@handle, bio, Primary Space badge, follower count,<FollowButton>, then "More from author". - Below article: Reddit-style nested comments (unified comment engine, Section 49).
2. Create page (/create) — dev.to /new layout
- Left nav collapsed. Form is the main LEFT working area (cover → title → tags · required Space selector → TipTap body → sticky Save draft / Publish bar).
- Right panel is a tabbed switcher:
[ Guide | Settings/SEO ].CreateGuidePanel(default): dynamic help that reacts to focused field — Space→primary-space rule, Tags→discovery tip, Series→grouping, body→/ @ # [[reference. Not a static tips list.CreateRightPanel(Settings/SEO): feature image, tags, space assignment, schedule, canonical, visibility, custom fields (Section 06), SEO (meta title/description/OG/index).
- Space selector must require ≥1 primary space before Publish enables.
3. Guest homepage (/ unauthenticated) — Hashnode-style landing
- NOT the shell. Rendered by the Page Builder from the
homepageroute-assignment (falls back to/explorewhen none assigned). - Slim marketing header → hero/slider → "Explore by destination" grid → trending discussions → featured universities → latest blog → newsletter form embed → CTA banner → footer. Ad slots:
hero_below,landing_inline_1,landing_footer. - Geo-aware (ADR-064): the "Explore by destination" grid and trending discussions are geo-weighted server-side via
VisitorContext.ContentCountry— a guest from Nigeria sees "planning to relocate" content; a guest from the UK sees "settling in / community" content; a global traveler is routed toward Japa Academy. This is SSR (no client flicker) and degrades gracefully whenGeoContextis absent (falls back to global). The?geo=param always wins over the resolvedContentCountry. - Authenticated
/is the Primary timeline in the shell (ADR-032, unchanged).
UNIVERSITY DISCOVERY (Section 39 — inside the shell)
Universities are a first-class space type (space_type = 'university') with a dedicated discovery page and single-page profile. Both render inside AppShell — they are NOT exceptions.
/universities — discovery list (center column)
- Filters: country (Geo Space selector), ranking tier, tuition range, programs, scholarship — all from
GET /api/v1/universities?country=&q=&page= - Sort: world ranking | country ranking | name | member count | follower count
- Layout: grid of university cards (logo, name, country, ranking, top programs) or list toggle
- Each card:
<FollowButton>, View button →/universities/[slug] - SEO: JSON-LD
ItemListofEducationalOrganization - Geo-aware: default country filter pre-populated from
VisitorContext.ContentCountry(a UK visitor sees UK universities first). Explicit filter always wins.
/universities/[slug] — single university page (center column + right sidebar)
- Header: logo (profile image), name, country flag, rankings, quick stats bar
- Center tabs: Overview (rich wiki TipTap) · Posts (content in this Space) · Forums (threads) · Courses (linked
academy_tracksfrom Section 36) · Members · Reviews (Section 29 ratings) · Compare (link to Section 30 comparison) - Right sidebar: key custom field values (ranking, tuition, programs, etc.)
- "Compare" button → opens Section 30 comparison with this university pre-selected as Item 1
- SEO: JSON-LD
EducationalOrganization+AggregateRatingfrom Section 29 - All data from
/api/v1/universities/:slug— no hardcoded university data (Rule 16)
GEO-AWARE PERSONALIZATION (ADR-064 — consumed in the frontend)
The backend resolves VisitorContext (ADR-064) and exposes ContentCountry via GET /api/v1/geo/me. The frontend consumes it as a default, never a hard filter:
- Feed header override control: an inline control in the feed header ("Showing: Nigeria · Change") — NOT buried in settings. Surfaced from
VisitorContext.ContentCountry; changing it callsPATCH /api/v1/me/profile(content_country_preference) and invalidates the feed. Important for a diaspora audience that straddles two countries. - Feed sort: the
?geo=param on feed/trending/discovery routes defaults toContentCountrywhen absent. Explicit?geo=allfor global. Never hide non-local content — re-weight only. - Guest homepage: geo-weighted SSR (above). No client flicker — the geo-resolved content is in the initial HTML.
- Killswitch: if
geo_personalization.enabled = false, all surfaces show global content. The frontend does not need to check this — the backend returns global results when the killswitch is off.
Full Visual Design Language
Design references per surface (applied WITHIN the shell): Ghost.org for the blog list (magazine cards, large feature image, serif-ish title, reading time); Reddit for forum threads (vote rail, nested threaded replies, Best/Top/New tabs); dev.to for reading & profiles/dashboards (the blog single, /create, profile analytics). These are visual cues layered onto the one shell — not separate layouts.
Tokens (CSS custom properties, dark-mode-safe — never hardcode hex):
--color-bg(#FAFAFA / #0A0A0A),--color-surface,--color-surface-subtle,--color-border,--color-muted,--color-primary(#3B49DF),--color-text,--chart-1..5- Radius:
--radius(8px controls, 12px cards). Nav height:--nav-height.
Typography: sentence case everywhere; two weights (400/500) in chrome; blog/article body may use a reading serif. Body 16px / line-height 1.7. No ALL CAPS.
Spacing: generous whitespace; hairline 1px solid var(--color-border); cards = surface bg + border + 12px radius + ~1rem 1.25rem padding.
Profiles (/u/[username], /org/[slug]): cover + avatar header, name/bio/Primary Space badge/join date/follow counts, then center tabs (Posts/Replies/Media/Likes/Spaces; org adds About/Members). Right sidebar: follow button, mutual spaces, org affiliations. Earned certificates render as badges here (social proof), linking to public cert pages.
Profile learning hub — cross-app entry point (ADR-060). The dashboard (/dashboard) is the single entry into the Learn and Docs apps; all tabs consume existing routes (no new backend), and cross-app links work off the shared session cookie + the ADR-054 cross-app menu system:
- Learning tab (
/dashboard/learning): continue-learning cards fromGET /api/v1/me/enrollments, resume links deep-linking intoacademy.japa.to; progress via LayerChartLineChart. - Certificates tab (
/dashboard/certificates): grid fromGET /api/v1/me/certificates; each links to its public, shareable cert page/certificates/:id(same certs badge the public profile). - Knowledge Base tab (
/dashboard/knowledge-base): deep-links into Docs (users audience,docs.japa.to/docs/users) + the user's bookmarked doc pages.
Learn and Docs are separate SvelteKit apps (owned by the jc-learn-* / jc-docs-* skills) that share this app's session and theme — link to them by absolute subdomain URL; don't re-implement their surfaces here.
Empty/loading: shared EmptyState (404/403/500/empty-list; icon + message + primary action) — visually DISTINCT from LoadingSkeleton (still-loading ≠ nothing-here). Every async section gets a skeleton.
LayerChart — Data Visualization (ADR-059)
Pinned layerchart@2.0.1 (Svelte 5; d3-scale bundled). Used on user/org dashboards and profile analytics tabs. Prefer high-level compound charts; component-to-widget map:
| Widget | Component | Source |
|---|---|---|
| Impressions over time | AreaChart (bisect-x tooltip) |
me/analytics |
| Follower/member growth | LineChart |
me/analytics/followers |
| Top posts / spaces | horizontal BarChart |
me/analytics/content |
| Reactions breakdown | stacked BarChart / radial arc |
me/analytics |
| Readers by country | horizontal BarChart |
Umami (Section 40) |
<script lang="ts">
import { AreaChart } from 'layerchart';
let { data }: { data: { date: Date; views: number }[] } = $props();
</script>
<div class="h-64 w-full">
<AreaChart {data} x="date" y="views"
props={{ area: { class: 'fill-[var(--chart-1)]/20 stroke-[var(--chart-1)]' } }}
tooltip />
</div>
Rules: chart colors ONLY via CSS custom properties (dark-mode safe) · data ONLY from API (Rule 16) · every chart in a fixed-height container · per-chart skeleton at final dimensions · dashboard shell + metric cards SSR, charts hydrate client-side · round every displayed number · dashboards are private (no ad slots). Do not confuse LayerChart's <Tooltip.Root> with Bits UI's — check the import source.
API Contract (Rule 16)
All data from /api/v1 — +page.server.ts load() fetches from API, never returns static data. Mutations: fetch + CSRF token from $page.data.csrfToken; auth from $page.data.session. Lists: { data: T[], meta: { total, page, limit, next_cursor? } }. Errors: throw error(status, message).
Rendering Strategy & Routing (ADR-032/058)
- Root
/: authenticated → Primary timeline (shell); guest → Page Builder homepage, fallback/explore. Resolve in root+layout.server.ts— no client redirect flash. - SSR for indexable routes (
/explore,/forum/*,/blog/*,/spaces/*,/u/*,/org/*, CPT singles); auth feed and/discover//notifications//messagesare auth-gated. Blog/CPT single and/createrender their exception layouts (nav auto-collapsed). /discoverconsumes existing/api/v1/suggestions/*— no new backend route.- Optimistic UI + rollback for reactions/bookmarks/follows.
data-sveltekit-preload-data="hover"on<body>.
TipTap Triggers (Locked)
/ slash commands · @ mentions · # tags · [[ interlinking.
i18n
Every user-facing string: $t('key'); add keys to en.json AND fr.json.
Deliverables Per Task
- API-Readiness Gate output · 2. surface built INSIDE
AppShell(or one of the three exceptions, explicitly) · 3. typed+page.server.tsload()· 4.+page.svelte(Runes, Bits UI, skeletons, dark mode) · 5. sub-components in$lib/components/· 6. types in$lib/types/index.ts. V2 main app phases (33–38): Identity Tools UI (Phase 33) → Jobs/Gamification/Referral (Phase 34) → Newsletter/Import/Travel (Phase 35) → QA (Phase 36) → Community Features UI (Phase 37) → Mentorship Matching UI (Phase 38). SeeROADMAP-MAIN.mdfor task breakdown.
V2 community features UI (Phase 37, §§79–89, §98): Community Story Wall (§79 — filterable testimonials by origin/destination/visa/profession), Destination Wiki (§80 — collaborative versioned KB pages with reputation-gated editing), Scam Alert Board (§81 — moderated scam reporting with subscriptions), Japa Planning Groups (§82 — private peer groups with milestones and check-ins), Visa Timeline Tracker (§83 — personal milestone logging with anonymous sharing), Community Digest Widget (§84 — auto-generated digest), Mutual Aid Board (§85 — Needs/Offers/Skill Swaps), Community Calendar (§86 — institutional dates calendar), Japa Stage Profiles (§87 — seven-stage relocation journey on profiles), Ask-an-Expat AMA (§88 — specialized forum AMA threads), Achievement Feed (§89 — opt-in milestone feed), Service Directory (§98 — geo-indexed business directory). All community feature pages must follow the geo-aware tool pattern: destination country selector is the first input, no hardcoded nationality, ?dest= filter on list endpoints.
V2 mentorship UI (Phase 38, §102, ADR-110): Mentor browsing/search, mentor profile pages, mentorship request flow, incoming/outgoing requests management, connection active/complete/cancel flows. Match scoring display (60% keyword overlap + 40% pgvector cosine similarity). DM auto-open on connection accept (§48).
V2 tool pages on main app (/tools/*): Visa Checklist, Readiness Assessment, Cost of Living, Scholarship Finder, Remittance Rate Tracker, JapaBot (full-page). Each tool is also available at its own domain/subdomain via apps/tools (§78) — the main app renders these within the community layout; apps/tools renders them tool-first without community nav.
V2 Feature Areas (Implement Only After Corresponding Backend Phase Is Complete)
| Feature | Backend phase | Key sections | Gates |
|---|---|---|---|
| JapaBot (RAG-grounded) | Phase 41 | §69 | /ai/chat amended |
| AI Content Studio (admin) | Phase 42 | §75 | studio.* RBAC |
| Participation queue (admin) | Phase 42 | §76 | participation.review RBAC |
| Visa Checklist | Phase 43 | §70 | checklist.use RBAC |
| Readiness Assessment | Phase 43 | §71 | assessment.take RBAC |
| Cost of Living Calculator | Phase 43 | §72 | col_calculator.use RBAC |
| Scholarship Finder | Phase 43 | §73 | scholarship.browse RBAC |
| Remittance Rate Tracker | Phase 44 | §74 | remittance.view RBAC |
| Referral system | Phase 45 | §60 | referral.use RBAC |
| Self-serve ads | Phase 45 | §61 | ads.self_serve.create RBAC |
| Newsletter | Phase 45 | §62 | newsletter.subscribe RBAC |
| Jobs board | Phase 45 | §63 | jobs.browse RBAC |
| Gamification | Phase 45 | §64 | gamification.view RBAC |
| Content import | Phase 46 | §65 | import.use RBAC |
| Advanced analytics | Phase 46 | §66 | analytics.advanced RBAC |
| Travel search | Phase 46 | §67 | travel.search RBAC |
API-Readiness Gate applies to all v2 features. Verify against api-routes-catalog.md — all v2 routes are marked as "V2 planned" in the catalog. Once the corresponding backend phase lands, the planned routes become available.
Removed surface: apps/tools was consolidated into apps/main (/tools/{tool}) — ADR-086 superseded. tools.japa.to and per-tool subdomains 301 to japa.to/tools/{tool}; the jc-tools-* skills were deleted.
V2 Community Feature Pages — Development Notes (Phase 37, §§79–89, §98)
All 11 community feature pages render inside AppShell (center column + right sidebar widgets). They are NOT exceptions to the shell — they follow the same +page.server.ts load() → API → +page.svelte (Runes, Bits UI, skeletons) pattern as every other surface.
Geo-aware tool pattern (mandatory on all §§79–101 pages):
- The destination country selector is the first input on every community feature page — before any other filter or content. Use a Bits UI
<Select.Root>or<Combobox.Root>populated fromGET /api/v1/geo/countries(never hardcoded). - All list endpoints consume
?dest=as the primary filter axis. The?dest=value drives the API query (e.g.GET /api/v1/community/stories?dest=GB). - No hardcoded nationality anywhere in the UI — the destination country is the axis, not the user's origin nationality. Origin is a secondary filter where applicable (e.g. Story Wall §79 supports origin filtering, but destination is primary).
- When the user has a
content_country_preference(ADR-064), pre-select the destination selector to that value; explicit selection always wins.
Per-feature development notes:
- Story Wall (§79): filterable testimonial cards (origin/destination/visa/profession filters). Use
FeedCardvariant for each story.?dest=is the primary filter; origin/visa/profession are secondary. - Destination Wiki (§80): collaborative versioned KB pages. Reputation-gated editing — check
can_editfrom the API response before showing the edit affordance. Render wiki content via TipTap HTML (.prose). Version history in a right-sidebar widget or tab. - Scam Alert Board (§81): moderated scam reports with subscription. Subscription toggle via
PATCHon the report or board. Moderation status badge from API. - Japa Planning Groups (§82): private peer groups with milestones and check-ins. Group detail page inside the shell; milestone progress via LayerChart if charts are needed.
- Visa Timeline Tracker (§83): personal milestone logging with optional anonymous sharing. Timeline visualization (LayerChart
LineChartor custom). Anonymous-share toggle must be explicit and opt-in. - Community Digest Widget (§84): auto-generated digest — render as a right-sidebar widget or a dedicated
/community/digestpage. Content from API; no client-side aggregation. - Mutual Aid Board (§85): Needs/Offers/Skill Swaps — tabbed board (Bits UI
<Tabs.Root>) with three tabs. Each listing is aFeedCardvariant. - Community Calendar (§86): institutional dates calendar. Use a calendar component (not a chart); events from
GET /api/v1/community/calendar?dest=. - Japa Stage Profiles (§87): seven-stage relocation journey rendered on user profiles (
/u/[username]). Adds a "Japa Stage" tab or section to the existing profile page. Stage data from API. - Ask-an-Expat AMA (§88): specialized forum AMA threads. Extends the existing forum engine (Section 49) with AMA-specific UI (question voting, answered status, host highlight).
- Achievement Feed (§89): opt-in milestone feed.
FeedCardvariants for achievement events. Opt-in toggle viaPATCH /api/v1/me/profile. - Service Directory (§98): geo-indexed business directory. List view with
?dest=primary filter; detail pages for each listing. JSON-LDLocalBusinesson detail pages.
ADR-088 HITL review gate: Any AI-generated content on community feature pages (e.g. wiki summaries, digest text, scam alert summaries) must pass through a Human-In-The-Loop review before publish. The UI must show a "Pending Review" state for AI-generated content and a review/approve/reject flow for moderators. Never publish AI-generated content directly — the HITL gate is mandatory.
V2 Mentorship Pages — Development Notes (Phase 38, §102, ADR-110)
Mentorship pages render inside AppShell. All data from /api/v1/mentorship/* endpoints.
- Mentor browsing/search (
/mentorship): list of mentors with filters (destination country first, then profession, visa type, availability).?dest=primary filter. Search via Meilisearch-backed endpoint. Match score display where applicable (60% keyword overlap + 40% pgvector cosine similarity) — render the score as a percentage badge on each mentor card. - Mentor profile page (
/mentorship/[id]): mentor bio, expertise areas, destination country, availability, past connections count. "Request Mentorship" button (Bits UI<Dialog.Root>for the request flow). - Mentorship request flow: modal with message field, goals field, preferred schedule. Submits to
POST /api/v1/mentorship/requests. CSRF token required. - Requests management (
/mentorship/requests): two tabs (Incoming / Outgoing) via Bits UI<Tabs.Root>. Actions: Accept / Decline / Cancel / Complete — each aPATCHon the request resource. - Connection active/complete/cancel flows: once accepted, a connection is active. The UI must show connection status badges (Pending → Active → Completed / Cancelled). "Complete" and "Cancel" actions require confirmation (Bits UI
<Dialog.Root>). - DM auto-open on accept (§48): when a mentorship request is accepted (either party), the UI must automatically open the DM thread between the two parties. This is wired via the existing DM system (§48) — on accept response, navigate to
/messages?thread=[connection_id]or open the DM modal. Do not require the user to manually start a conversation.