Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ frontend/src/
- Handles tooltip positioning
- Manages route changes
- How it works page (`/how-it-works`) renders full-bleed (no `px-3 py-3` padding on `<main>`)
- Calls `normalizeLocation(window)` from `src/lib/normalizePath.js` during app startup to normalize direct/path-based URLs for the hash router
- **Navigation**: `src/components/Navbar.svelte`
- Top navigation bar
- Auth button integration
Expand All @@ -300,6 +301,13 @@ frontend/src/
- **How it works** (bottom pinned area, above Submit Contribution) - Links to `/how-it-works`
- **Profile** - User profile and submissions

### Hash Route Normalization
The portal uses `svelte-spa-router`, so app routes must be represented as hash URLs such as `/#/testnets`. `src/App.svelte` imports `normalizeLocation` from `src/lib/normalizePath.js` and invokes `normalizeLocation(window)` once at initial app load.

`normalizeLocation` reads `window.location.pathname`, `window.location.search`, and `window.location.hash`. If a hash is already present, the path is `/`, the path looks like a static file, or the path starts with a reserved server/static prefix (`/api`, `/oauth`, `/static`, `/assets`, `/media`), it does nothing. Otherwise it rewrites the current URL with `window.history.replaceState({}, '', '/#' + pathname + search)`, so `/metrics?range=30d` becomes `/#/metrics?range=30d`.

Use this normalization when debugging direct links, copied links, refreshes, or server-served deep links that arrive as plain paths. New external entry points to SPA routes should either emit hash URLs directly (`/#/route`) or be normalized by invoking `normalizeLocation(window)` before the router resolves the page.

### Routes/Pages
All routes are defined in `src/App.svelte`:
```javascript
Expand Down Expand Up @@ -807,4 +815,4 @@ $location // reactive store with current path
- Pages go in `src/routes/`
- Reusable components in `src/components/`
- API functions in `src/lib/api.js`
- New API endpoints need both frontend (api.js) and backend implementation
- New API endpoints need both frontend (api.js) and backend implementation
23 changes: 7 additions & 16 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { location } from 'svelte-spa-router';
import { resetPageMeta } from './lib/meta.js';
import { authState, verifyAuth } from './lib/auth.js';
import { normalizeLocation } from './lib/normalizePath.js';
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Early OAuth result detection — runs before routes mount.
// Backend redirects here with ?oauth_platform=X&oauth_verified=true/false&oauth_error=...
Expand Down Expand Up @@ -49,22 +50,12 @@
}
}

// The portal uses hash routing. Normalize pasted/local links such as
// /claim/poap/:token so mint links work on localhost and 127.0.0.1.
{
const path = window.location.pathname;
const shouldNormalize =
!window.location.hash &&
(path.startsWith('/claim/poap/') || path.startsWith('/community/poaps/'));

if (shouldNormalize) {
window.history.replaceState(
{},
'',
`/#${path}${window.location.search || ''}`
);
}
}
// The portal uses hash routing. Direct/path-based links (sidebar hrefs
// opened in a new tab, refreshes of a path route, shared or indexed links)
// arrive without a hash and would otherwise 404. Rewrite any such path into
// its hash equivalent so the router can resolve it; unknown paths still
// fall through to the router's own NotFound view.
normalizeLocation(window);

// State for sidebar toggle on mobile and collapse on desktop
let sidebarOpen = $state(false);
Expand Down
64 changes: 32 additions & 32 deletions frontend/src/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
{#if !collapsed && getActiveSection() === 'global'}
<div class="pl-5">
<a
href="/testnets"
href="#/testnets"
onclick={(e) => { e.preventDefault(); navigate('/testnets'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/testnets') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -196,7 +196,7 @@
Testnets
</a>
<a
href="/metrics"
href="#/metrics"
onclick={(e) => { e.preventDefault(); navigate('/metrics'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/metrics') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -240,7 +240,7 @@
{#if !collapsed && getActiveSection() === 'builder'}
<div class="pl-5">
<a
href="/builders/contributions"
href="#/builders/contributions"
onclick={(e) => { e.preventDefault(); navigate('/builders/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/contributions') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand All @@ -249,7 +249,7 @@
Contributions
</a>
<a
href="/builders/leaderboard"
href="#/builders/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/builders/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/leaderboard') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand All @@ -258,7 +258,7 @@
Leaderboard
</a>
<a
href="/builders/resources"
href="#/builders/resources"
onclick={(e) => { e.preventDefault(); navigate('/builders/resources'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/resources') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -293,7 +293,7 @@
{#if !collapsed && getActiveSection() === 'validator'}
<div class="pl-5">
<a
href="/validators/contributions"
href="#/validators/contributions"
onclick={(e) => { e.preventDefault(); navigate('/validators/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/contributions') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -302,7 +302,7 @@
Contributions
</a>
<a
href="/validators/leaderboard"
href="#/validators/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/validators/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/leaderboard') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -311,7 +311,7 @@
Leaderboard
</a>
<a
href="/validators/participants"
href="#/validators/participants"
onclick={(e) => { e.preventDefault(); navigate('/validators/participants'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/participants') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -320,7 +320,7 @@
Participants
</a>
<a
href="/validators/wall-of-shame"
href="#/validators/wall-of-shame"
onclick={(e) => { e.preventDefault(); navigate('/validators/wall-of-shame'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/wall-of-shame') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -329,7 +329,7 @@
Wall of Shame
</a>
<a
href="/validators/waitlist"
href="#/validators/waitlist"
onclick={(e) => { e.preventDefault(); navigate('/validators/waitlist'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/waitlist') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -364,7 +364,7 @@
{#if !collapsed && getActiveSection() === 'community'}
<div class="pl-5">
<a
href="/community/contributions"
href="#/community/contributions"
onclick={(e) => { e.preventDefault(); navigate('/community/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/contributions') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -373,7 +373,7 @@
Contributions
</a>
<a
href="/community/leaderboard"
href="#/community/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/community/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/leaderboard') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -382,7 +382,7 @@
Leaderboard
</a>
<a
href="/community/poaps"
href="#/community/poaps"
onclick={(e) => { e.preventDefault(); navigate('/community/poaps'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/poaps') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -418,7 +418,7 @@
{#if !collapsed && getActiveSection() === 'steward' && $userStore.user?.steward}
<div class="pl-5">
<a
href="/stewards/submissions"
href="#/stewards/submissions"
onclick={(e) => { e.preventDefault(); navigate('/stewards/submissions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/submissions') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand All @@ -428,7 +428,7 @@
</a>
{#if canAccessDiscordXP}
<a
href="/stewards/discord-xp"
href="#/stewards/discord-xp"
onclick={(e) => { e.preventDefault(); navigate('/stewards/discord-xp'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/discord-xp') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand All @@ -439,7 +439,7 @@
{/if}
{#if canAccessManageUsers}
<a
href="/stewards/manage-users"
href="#/stewards/manage-users"
onclick={(e) => { e.preventDefault(); navigate('/stewards/manage-users'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/manage-users') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -635,7 +635,7 @@
{#if getActiveSection() === 'global'}
<div class="pl-5">
<a
href="/testnets"
href="#/testnets"
onclick={(e) => { e.preventDefault(); navigate('/testnets'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/testnets') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -644,7 +644,7 @@
Testnets
</a>
<a
href="/metrics"
href="#/metrics"
onclick={(e) => { e.preventDefault(); navigate('/metrics'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/metrics') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -677,7 +677,7 @@
{#if getActiveSection() === 'builder'}
<div class="pl-5">
<a
href="/builders/contributions"
href="#/builders/contributions"
onclick={(e) => { e.preventDefault(); navigate('/builders/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/contributions') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand All @@ -686,7 +686,7 @@
Contributions
</a>
<a
href="/builders/leaderboard"
href="#/builders/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/builders/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/leaderboard') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand All @@ -695,7 +695,7 @@
Leaderboard
</a>
<a
href="/builders/resources"
href="#/builders/resources"
onclick={(e) => { e.preventDefault(); navigate('/builders/resources'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/builders/resources') ? 'border-[#EE8D24]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -723,7 +723,7 @@
{#if getActiveSection() === 'validator'}
<div class="pl-5">
<a
href="/validators/contributions"
href="#/validators/contributions"
onclick={(e) => { e.preventDefault(); navigate('/validators/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/contributions') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -732,7 +732,7 @@
Contributions
</a>
<a
href="/validators/leaderboard"
href="#/validators/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/validators/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/leaderboard') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -741,7 +741,7 @@
Leaderboard
</a>
<a
href="/validators/participants"
href="#/validators/participants"
onclick={(e) => { e.preventDefault(); navigate('/validators/participants'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/participants') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -750,7 +750,7 @@
Participants
</a>
<a
href="/validators/wall-of-shame"
href="#/validators/wall-of-shame"
onclick={(e) => { e.preventDefault(); navigate('/validators/wall-of-shame'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/wall-of-shame') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand All @@ -759,7 +759,7 @@
Wall of Shame
</a>
<a
href="/validators/waitlist"
href="#/validators/waitlist"
onclick={(e) => { e.preventDefault(); navigate('/validators/waitlist'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/validators/waitlist') ? 'border-[#387DE8]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -787,7 +787,7 @@
{#if getActiveSection() === 'community'}
<div class="pl-5">
<a
href="/community/contributions"
href="#/community/contributions"
onclick={(e) => { e.preventDefault(); navigate('/community/contributions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/contributions') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -796,7 +796,7 @@
Contributions
</a>
<a
href="/community/leaderboard"
href="#/community/leaderboard"
onclick={(e) => { e.preventDefault(); navigate('/community/leaderboard'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/leaderboard') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand All @@ -805,7 +805,7 @@
Leaderboard
</a>
<a
href="/community/poaps"
href="#/community/poaps"
onclick={(e) => { e.preventDefault(); navigate('/community/poaps'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/community/poaps') ? 'border-[#8D81E1]' : 'border-[#f5f5f5]'
Expand Down Expand Up @@ -833,7 +833,7 @@
{#if getActiveSection() === 'steward' && $userStore.user?.steward}
<div class="pl-5">
<a
href="/stewards/submissions"
href="#/stewards/submissions"
onclick={(e) => { e.preventDefault(); navigate('/stewards/submissions'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/submissions') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand All @@ -843,7 +843,7 @@
</a>
{#if canAccessDiscordXP}
<a
href="/stewards/discord-xp"
href="#/stewards/discord-xp"
onclick={(e) => { e.preventDefault(); navigate('/stewards/discord-xp'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/discord-xp') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand All @@ -854,7 +854,7 @@
{/if}
{#if canAccessManageUsers}
<a
href="/stewards/manage-users"
href="#/stewards/manage-users"
onclick={(e) => { e.preventDefault(); navigate('/stewards/manage-users'); }}
class="flex items-center border-l-[1.5px] px-3 py-2 text-[14px] font-medium text-black tracking-[0.28px] {
isActive('/stewards/manage-users') ? 'border-[#19A663]' : 'border-[#f5f5f5]'
Expand Down
Loading