Skip to content
Closed
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
3 changes: 2 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const TokenomicsPage = lazy(() => import('./pages/TokenomicsPage'));
const ContributorProfilePage = lazy(() => import('./pages/ContributorProfilePage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const CreatorDashboardPage = lazy(() => import('./pages/CreatorDashboardPage'));
const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));

// ── Loading spinner ──────────────────────────────────────────────────────────
function LoadingSpinner() {
Expand Down Expand Up @@ -70,7 +71,7 @@ function AppLayout() {
<Route path="/creator" element={<CreatorDashboardPage />} />

{/* Fallback */}
<Route path="*" element={<Navigate to="/bounties" replace />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</SiteLayout>
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/components/common/ScrollToTop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';

/**
* ScrollToTop - Floating scroll-to-top button component
*
* Appears when user scrolls down more than 300px.
* Smooth scroll animation to top on click.
* Fade-in/fade-out animation on appear/disappear.
*/
Comment on lines +4 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documented fade-out behavior is not actually implemented.

Lines 8-9 describe fade-in/fade-out, but Lines 36-38 unmount immediately when hidden, so there is no fade-out transition path.

As per coding guidelines, "frontend/**: React/TypeScript frontend. Check: Component structure and state management".

Also applies to: 36-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/ScrollToTop.tsx` around lines 4 - 9, The
component comment claims fade-in/fade-out but the ScrollToTop component
currently unmounts immediately; change the visibility logic to keep the
component mounted during fade-out by introducing a second state (e.g., isVisible
and shouldRender) inside the ScrollToTop component, toggle isVisible on scroll
and only set shouldRender false after the CSS fade-out transition ends (use a
timeout or onTransitionEnd in the component), and apply distinct CSS classes for
enter (fade-in) vs exit (fade-out) so the element animates out before being
removed; update useEffect scroll handler (handleScroll) and the unmount path to
respect shouldRender so fade-out can complete.

export function ScrollToTop() {
const [isVisible, setIsVisible] = useState(false);

// Toggle visibility based on scroll position
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
setIsVisible(scrollPosition > 300);
};

window.addEventListener('scroll', handleScroll);

Comment on lines +14 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/src/components/common/ScrollToTop.tsx

Repository: SolFoundry/solfoundry

Length of output: 2242


Address scroll performance and fix documentation mismatch.

The scroll listener and state update pattern has two issues:

  1. State updates on every scroll event (Line 17): Calling setIsVisible(scrollPosition > 300) on every scroll event is inefficient. While React batching prevents excessive re-renders when the boolean value hasn't changed, this pattern can still impact performance on lower-end devices. Consider throttling the scroll handler using a library like lodash.throttle or implementing a manual throttle with requestAnimationFrame.

  2. Documentation/implementation mismatch (Lines 7-8 vs 36-38): The JSDoc comment claims "Fade-in/fade-out animation on appear/disappear", but the implementation returns null to unmount the component instead of using CSS fade effects. Either implement actual fade animations with conditional opacity classes or update the documentation to reflect the unmounting behavior.

Minor: Adding { passive: true } to the addEventListener call (Line 20) is a best practice, though it has no functional impact on scroll events.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/ScrollToTop.tsx` around lines 14 - 21, The
scroll handler in the ScrollToTop component (useEffect -> handleScroll ->
setIsVisible) should be throttled and the event listener marked passive to
improve performance: wrap handleScroll with a requestAnimationFrame-based
throttle (or use lodash.throttle) so setIsVisible(scrollY > 300) only runs at
animation frame rate, and call window.addEventListener('scroll',
throttledHandler, { passive: true }); additionally, reconcile the JSDoc on the
ScrollToTop component—either implement real fade-in/out by keeping the component
mounted and toggling CSS opacity classes based on isVisible, or update the JSDoc
to state that the component mounts/unmounts when crossing the 300px threshold
(pick one and apply consistently).

// Initial check
handleScroll();

return () => window.removeEventListener('scroll', handleScroll);
}, []);

// Smooth scroll to top
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};

if (!isVisible) {
return null;
}

return (
<button
onClick={scrollToTop}
className="fixed bottom-6 right-6 z-50 p-3 rounded-full
bg-[#9945FF] hover:bg-[#8a3fe6] text-white
border border-[#9945FF]/30 shadow-lg shadow-[#9945FF]/20
transition-all duration-300 ease-in-out
hover:scale-110 active:scale-95
opacity-100"
aria-label="Scroll to top"
title="Scroll to top"
>
{/* Up arrow SVG icon */}
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 15l7-7 7 7" />
</svg>
</button>
);
}

export default ScrollToTop;
1 change: 1 addition & 0 deletions frontend/src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ScrollToTop } from './ScrollToTop';
4 changes: 4 additions & 0 deletions frontend/src/components/layout/SiteLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ScrollToTop } from '../common/ScrollToTop';

// ============================================================================
// Types
Expand Down Expand Up @@ -153,6 +154,9 @@ export function SiteLayout({

{/* Footer */}
<Footer />

{/* Scroll to Top Button */}
<ScrollToTop />
Comment on lines +158 to +159
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Global placement introduces an overlay interaction edge case.

With Line 159 mounting the floating button globally, it can remain clickable during mobile menu state because the overlay is z-40 (Line 136) while the button is z-50 (frontend/src/components/common/ScrollToTop.tsx, Line 43). That weakens modal/overlay interaction isolation.

As per coding guidelines, "frontend/**: React/TypeScript frontend. Check: Integration with existing components".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/layout/SiteLayout.tsx` around lines 158 - 159, The
floating ScrollToTop button is mounted globally in SiteLayout which keeps it
above the mobile menu overlay (ScrollToTop z-50 vs overlay z-40), allowing
clicks through the modal; update SiteLayout/ScrollToTop integration so the
button is not interactive while the mobile menu is open: either move the
<ScrollToTop /> into the main content container (so it sits below the overlay)
or add a boolean prop like isMobileMenuOpen from SiteLayout to ScrollToTop and
conditionally render or lower its z-index when true; reference the ScrollToTop
component and the mobile menu overlay state in SiteLayout to implement the
conditional render or z-index swap.

</div>
);
}
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/pages/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* 404 Not Found Page
* Displayed when user visits an invalid route.
* Matches SolFoundry dark theme styling.
*/
import { Link } from 'react-router-dom';

export function NotFoundPage() {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4 text-center">
{/* Logo */}
<div className="mb-8">
<div className="w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br from-[#9945FF] to-[#14F195] flex items-center justify-center">
<svg
className="w-10 h-10 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
</div>

{/* 404 Text */}
<h1 className="text-6xl font-bold text-white mb-2">404</h1>
<h2 className="text-xl font-semibold text-gray-300 mb-4">Page not found</h2>
<p className="text-sm text-gray-500 mb-8 max-w-md">
The page you're looking for doesn't exist or has been moved.
</p>

{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4">
<Link
to="/"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl bg-gradient-to-r from-[#9945FF] to-[#14F195] text-white font-semibold hover:opacity-90 transition-opacity"
>
<svg
className="w-5 h-5 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Back to Home
</Link>
<Link
to="/bounties"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl border border-gray-700 text-gray-300 font-semibold hover:bg-gray-800 hover:border-gray-600 transition-colors"
>
Browse open bounties
<svg
className="w-5 h-5 ml-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</Link>
</div>
</div>
);
}

export default NotFoundPage;
Loading