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
2 changes: 1 addition & 1 deletion frontend/src/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
});
</script>

<header class="bg-white relative z-50" style="border-bottom: 1.2px solid #e5e5e6;">
<header class="bg-white sticky top-0 z-50 flex-shrink-0" style="border-bottom: 1.2px solid #e5e5e6;">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how Sidebar renders on mobile (overlay vs. inline) and whether body/page scrolls on small screens
rg -n "overflow" frontend/src/components/Sidebar.svelte frontend/src/App.svelte
rg -n "fixed|position: fixed|md:hidden|mobile" frontend/src/components/Sidebar.svelte

Repository: genlayer-foundation/points

Length of output: 1904


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== App.svelte around navbar/main =="
sed -n '520,552p' frontend/src/App.svelte

echo
echo "== Navbar.svelte around header =="
sed -n '110,140p' frontend/src/components/Navbar.svelte

echo
echo "== Sidebar mobile shell around overlay/panel =="
sed -n '712,736p' frontend/src/components/Sidebar.svelte

Repository: genlayer-foundation/points

Length of output: 3801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

app = Path("frontend/src/App.svelte").read_text()
nav = Path("frontend/src/components/Navbar.svelte").read_text()
side = Path("frontend/src/components/Sidebar.svelte").read_text()

for name, text in [("App.svelte", app), ("Navbar.svelte", nav), ("Sidebar.svelte", side)]:
    print(f"\n== {name}: scroll/position markers ==")
    for i, line in enumerate(text.splitlines(), 1):
        if any(tok in line for tok in ["overflow", "sticky", "fixed", "Navbar", "main", "h-screen", "overflow-hidden", "overflow-y-auto", "top-0", "top: var(--mobile-navbar-height"]):
            print(f"{i}: {line}")
PY

Repository: genlayer-foundation/points

Length of output: 2045


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the navbar/header is inside the same flex column as the scrolling main,
# and whether the mobile sidebar panel sits below the navbar height.
rg -n "h-screen flex flex-col|overflow-hidden|overflow-y-auto|sticky top-0|top: var\(--mobile-navbar-height" frontend/src/App.svelte frontend/src/components/Navbar.svelte frontend/src/components/Sidebar.svelte

Repository: genlayer-foundation/points

Length of output: 1454


sticky won’t change the scroll behavior here Navbar sits outside the only scrolling container (main.app-main), so this class is effectively a no-op unless the app starts scrolling in a different ancestor. If the mobile fix depends on the header staying pinned, adjust the scroll container/layout instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Navbar.svelte` at line 127, The Navbar header’s
sticky positioning is ineffective because it sits outside the scrolling
container, so `sticky top-0` in `Navbar.svelte` does not affect the actual
scroll behavior. Update the layout so the element that actually scrolls (the
`main.app-main` container or its ancestor structure) is the one providing the
sticky context, or move the header into that scrolling hierarchy; use the
`Navbar` header markup and the `main.app-main` layout relationship to locate the
fix.

<div class="navbar-inner flex items-center justify-between p-3">
<!-- Left: Logo -->
<div class="navbar-logo-wrap flex items-center gap-1 px-1 min-w-0">
Expand Down
39 changes: 29 additions & 10 deletions frontend/src/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
}));
}

function closeMobileSidebar() {
if (window.innerWidth < 768) {
isOpen = false;
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
function navigate(path, requiresAuth = false, options = {}) {
if (options.track !== false) {
trackSidebarNav(path, {
Expand All @@ -101,18 +107,28 @@
if (requiresAuth && !$authState.isAuthenticated) {
setConnectWalletIntent({
surface: 'sidebar',
cta_id: 'protected_nav',
cta_id: options.ctaId || 'protected_nav',
target_route: path,
});
sessionStorage.setItem('redirectAfterLogin', path);
const authButton = document.querySelector('[data-auth-button]');
if (authButton) authButton.click();
closeMobileSidebar();
return;
}
push(path);
if (window.innerWidth < 768) {
isOpen = false;
}
closeMobileSidebar();
}

function handleSubmitContribution() {
trackEvent('submit_contribution_click', getAnalyticsContext({
surface: 'sidebar',
}));

navigate('/submit-contribution', true, {
ctaId: 'submit_contribution',
track: false,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// A signed-in user who has not earned this category's role: the role's
Expand Down Expand Up @@ -150,9 +166,7 @@
const authButton = document.querySelector('[data-auth-button]');
if (authButton) authButton.click();
}
if (window.innerWidth < 768) {
isOpen = false;
}
closeMobileSidebar();
}

// Check if a route is active
Expand Down Expand Up @@ -181,9 +195,7 @@
currentCategory.set(category);
trackSidebarNav(path, { roleContext: roleContextForCategory(category) });
push(path);
if (window.innerWidth < 768) {
isOpen = false;
}
closeMobileSidebar();
}

// Shorten address for display
Expand Down Expand Up @@ -1044,6 +1056,13 @@
</svg>
<span class="text-[14px] font-medium tracking-[0.28px] {isActive('/how-it-works') ? 'text-[#6D5DD3]' : 'text-[#656567]'}">How it works</span>
</button>
<button
onclick={handleSubmitContribution}
class="w-full flex items-center gap-2 px-3 py-2 rounded-[8px] transition-colors text-left {isActive('/submit-contribution') ? 'bg-[#eeedfb]' : 'hover:bg-[#f5f5f5]'}"
>
<img src="/assets/icons/add-line-sidebar.svg" alt="" class="w-4 h-4 flex-shrink-0">
<span class="text-[14px] font-medium tracking-[0.28px] {isActive('/submit-contribution') ? 'text-[#6D5DD3]' : 'text-[#656567]'}">Submit Contribution</span>
</button>
{#if $authState.isAuthenticated}
<button
onclick={() => navigate('/my-submissions')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
let loadingProjects = $state(false);
let projectsError = $state(false);
let selectedProject = $state("");
let showProjectDropdown = $state(false);
let focusedProjectIndex = $state(-1);
let showTypeDropdown = $state(false);
let searchQuery = $state("");

Expand Down Expand Up @@ -413,6 +415,14 @@
let selectedProjectData = $derived(
acceptedProjects.find((project) => String(project.id) === String(selectedProject)) || null
);
let selectedProjectLabel = $derived(
selectedProjectData
? `${selectedProjectData.title} (next v${selectedProjectData.next_milestone_version || 1})`
: "Select accepted project..."
);
let selectedProjectIndex = $derived(
acceptedProjects.findIndex((project) => String(project.id) === String(selectedProject))
);

function spotsLeftLabel(count) {
return `${count} ${Number(count) === 1 ? "spot" : "spots"} left`;
Expand Down Expand Up @@ -537,6 +547,7 @@
function selectCategory(cat) {
selectedCategory = cat;
showTypeDropdown = false;
showProjectDropdown = false;
}

function selectType(t) {
Expand All @@ -555,6 +566,7 @@
selectedMission = null;
selectedMissionData = null;
selectedProject = "";
showProjectDropdown = false;
formData.contribution_type = t.id;
showTypeDropdown = false;
searchQuery = t.name;
Expand All @@ -579,6 +591,7 @@
selectedMission = item.data.id;
selectedMissionData = item.data;
selectedProject = "";
showProjectDropdown = false;
formData.contribution_type = item.parentType.id;
showTypeDropdown = false;
searchQuery = item.data.name;
Expand Down Expand Up @@ -615,6 +628,99 @@
}, 200);
}

let projectDropdownButtonRef = $state(null);
let projectDropdownRef = $state(null);
let projectOptionRefs = $state([]);

function focusProjectOption(index, { defer = false } = {}) {
if (acceptedProjects.length === 0) return;
const nextIndex = (index + acceptedProjects.length) % acceptedProjects.length;
focusedProjectIndex = nextIndex;

const focusOption = () => {
projectOptionRefs[nextIndex]?.focus();
};

if (defer) {
requestAnimationFrame(focusOption);
} else {
focusOption();
}
}

function openProjectDropdown(index = selectedProjectIndex >= 0 ? selectedProjectIndex : 0) {
projectOptionRefs = [];
showProjectDropdown = true;
focusProjectOption(index, { defer: true });
}

function closeProjectDropdown({ focusTrigger = false } = {}) {
showProjectDropdown = false;
focusedProjectIndex = -1;
if (focusTrigger) {
requestAnimationFrame(() => {
projectDropdownButtonRef?.focus();
});
}
}

function toggleProjectDropdown() {
if (showProjectDropdown) {
closeProjectDropdown();
} else {
openProjectDropdown();
}
}

function selectProject(project, options = {}) {
selectedProject = String(project.id);
closeProjectDropdown({ focusTrigger: options.focusTrigger ?? true });
if (error === "Please select the accepted project this milestone belongs to.") {
error = "";
}
}

function handleProjectTriggerKeydown(event) {
if (event.key === "ArrowDown") {
event.preventDefault();
openProjectDropdown(selectedProjectIndex >= 0 ? selectedProjectIndex : 0);
} else if (event.key === "ArrowUp") {
event.preventDefault();
openProjectDropdown(selectedProjectIndex >= 0 ? selectedProjectIndex : acceptedProjects.length - 1);
} else if (event.key === "Escape" && showProjectDropdown) {
event.preventDefault();
closeProjectDropdown();
}
}

function handleProjectOptionKeydown(event, index) {
if (event.key === "ArrowDown") {
event.preventDefault();
focusProjectOption(index + 1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
focusProjectOption(index - 1);
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectProject(acceptedProjects[index]);
} else if (event.key === "Escape") {
event.preventDefault();
closeProjectDropdown({ focusTrigger: true });
}
}

function handleProjectDropdownFocusOut() {
setTimeout(() => {
if (
showProjectDropdown &&
projectDropdownRef &&
!projectDropdownRef.contains(document.activeElement)
) {
closeProjectDropdown();
}
}, 0);
}

// Evidence functions
function addEvidenceSlot() {
evidenceSlots = [
Expand Down Expand Up @@ -1114,7 +1220,7 @@
}

// Click outside listener for dropdown
let dropdownRef;
let dropdownRef = $state(null);
function handleClickOutside(event) {
if (
showTypeDropdown &&
Expand All @@ -1123,6 +1229,13 @@
) {
showTypeDropdown = false;
}
if (
showProjectDropdown &&
projectDropdownRef &&
!projectDropdownRef.contains(event.target)
) {
closeProjectDropdown();
}
}
</script>

Expand Down Expand Up @@ -1493,7 +1606,7 @@

{#if selectedType && isMilestoneType(selectedType)}
<div
class="submit-panel flex flex-col gap-[12px] items-start p-[24px] rounded-[16px] shadow-[0px_4px_20px_0px_rgba(0,0,0,0.02)] bg-white border border-[#f5f5f5] w-full"
class="submit-panel linked-project-panel relative z-20 flex flex-col gap-[12px] items-start p-[24px] rounded-[16px] shadow-[0px_4px_20px_0px_rgba(0,0,0,0.02)] bg-white border border-[#f5f5f5] w-full overflow-visible"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
<div class="flex items-start justify-between gap-3 w-full">
<div>
Expand Down Expand Up @@ -1539,18 +1652,73 @@
</p>
</div>
{:else}
<select
bind:value={selectedProject}
class="w-full h-[44px] rounded-[8px] border border-[#f5f5f5] bg-white px-[12px] font-['Switzer'] text-[14px] text-black tracking-[0.28px] focus:outline-none focus:border-black"
required={isMilestoneType(selectedType)}
<div
class="project-dropdown-wrapper relative w-full"
bind:this={projectDropdownRef}
onfocusout={handleProjectDropdownFocusOut}
>
<option value="">Select accepted project...</option>
{#each acceptedProjects as project}
<option value={project.id}>
{project.title} (next v{project.next_milestone_version || 1})
</option>
{/each}
</select>
<button
type="button"
class="project-dropdown-trigger flex h-[44px] w-full items-center justify-between gap-3 rounded-[8px] border border-[#f5f5f5] bg-white px-[12px] text-left font-['Switzer'] text-[14px] text-black tracking-[0.28px] transition-colors hover:border-gray-300 focus:outline-none focus:border-black"
aria-haspopup="listbox"
aria-expanded={showProjectDropdown}
aria-controls="project-dropdown-menu"
bind:this={projectDropdownButtonRef}
onclick={toggleProjectDropdown}
onkeydown={handleProjectTriggerKeydown}
>
<span class="min-w-0 truncate {selectedProject ? 'text-black' : 'text-[#6b6b6b]'}">
{selectedProjectLabel}
</span>
<svg
class="h-4 w-4 flex-shrink-0 text-gray-500 transition-transform {showProjectDropdown ? 'rotate-180' : ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>

{#if showProjectDropdown}
<div
id="project-dropdown-menu"
class="project-dropdown-menu absolute left-0 right-0 top-[48px] z-[60] max-h-[280px] overflow-y-auto rounded-[8px] border border-[#f0f0f0] bg-white shadow-[0_18px_48px_rgba(31,42,68,0.16)]"
role="listbox"
>
{#each acceptedProjects as project, index}
<button
type="button"
class="project-dropdown-option flex w-full flex-col items-start border-b border-[#f5f5f5] p-[12px] text-left last:border-0 transition-colors hover:bg-gray-50 {String(project.id) === String(selectedProject) ? 'bg-[#f0f0ff]' : ''}"
role="option"
aria-selected={String(project.id) === String(selectedProject)}
tabindex={focusedProjectIndex === index ? 0 : -1}
bind:this={projectOptionRefs[index]}
onclick={() => selectProject(project)}
onkeydown={(event) => handleProjectOptionKeydown(event, index)}
>
<span class="font-['Switzer'] text-[14px] font-medium text-black tracking-[0.2px]">
{project.title}
</span>
<span class="mt-1 font-['Switzer'] text-[12px] text-[#6b6b6b]">
Next milestone v{project.next_milestone_version || 1}
</span>
{#if project.github_url}
<span class="mt-0.5 max-w-full truncate font-['Switzer'] text-[11px] text-gray-400">
{project.github_url.replace(/^https?:\/\//, "")}
</span>
{/if}
</button>
{/each}
</div>
{/if}
</div>

{#if selectedProjectData}
<div class="w-full rounded-[8px] border border-[#f0f0f0] bg-[#fafafa] p-[12px]">
Expand Down Expand Up @@ -2023,6 +2191,14 @@
</div>

<style>
.project-dropdown-wrapper {
isolation: isolate;
}

.project-dropdown-menu {
overscroll-behavior: contain;
}

@media (max-width: 767px) {
.submit-form-shell {
max-width: 100% !important;
Expand Down Expand Up @@ -2074,6 +2250,7 @@
}

.type-selector-control,
.project-dropdown-trigger,
.field-control {
min-width: 0;
}
Expand All @@ -2091,6 +2268,19 @@
width: 100%;
}

.project-dropdown-menu {
max-height: min(48vh, 280px);
}

.project-dropdown-option {
min-width: 0;
}

.project-dropdown-option span {
max-width: 100%;
overflow-wrap: anywhere;
}

.type-dropdown-item {
min-width: 0;
}
Expand Down