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
12 changes: 7 additions & 5 deletions src/app/farm/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
useToast,
} from "@chakra-ui/react";
import { useOwnConnectButton } from "@/context/OwnConnectButtonContext";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";

const ACCENT = "#4AE292";

Expand Down Expand Up @@ -534,15 +534,17 @@ export default function Farm() {
});
}, [pools, userPositions]);

const handleDepositClick = (pool: LivePoolRow) => {
// Stable references so the memoized FarmPoolRow children
// don't receive a new callback on every wallet or query update.
const handleDepositClick = useCallback((pool: LivePoolRow) => {
setSelectedFarm(pool);
setIsDepositOpen(true);
};
}, []);

const handleDepositClose = () => {
const handleDepositClose = useCallback(() => {
setIsDepositOpen(false);
setSelectedFarm(null);
};
}, []);

const hasPositions = myPositions.length > 0;

Expand Down
6 changes: 3 additions & 3 deletions src/app/history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useMemo, useState } from "react";
import { memo, useEffect, useMemo, useState } from "react";
import {
Alert,
AlertIcon,
Expand Down Expand Up @@ -77,7 +77,7 @@ function EmptyState({ children }: { children: React.ReactNode }) {
);
}

function HistoryCard({ entry }: { entry: TxHistoryEntry }) {
const HistoryCard = memo(function HistoryCard({ entry }: { entry: TxHistoryEntry }) {
return (
<Box
w="100%"
Expand Down Expand Up @@ -134,7 +134,7 @@ function HistoryCard({ entry }: { entry: TxHistoryEntry }) {
</VStack>
</Box>
);
}
});

export default function HistoryPage() {
// Chakra's `ssr` option is inverted from what the name suggests: `ssr: false`
Expand Down
13 changes: 7 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useEffect } from "react";
import { memo, useEffect } from "react";
import {
Flex,
HStack,
Expand Down Expand Up @@ -37,9 +37,10 @@ const fadeInUp = {
// A layer of slow-drifting, multi-hue gradient blobs plus a faint dot grid,
// scoped to this page's own stacking context so it never touches the shared
// AppShell chrome (nav/footer) that other routes' visual-regression
// screenshots depend on.
// screenshots depend on. Memoized so wallet / stats updates on the page
// don't re-render the infinitely-animating orbs.

function GradientOrb({
const GradientOrb = memo(function GradientOrb({
color,
size,
top,
Expand Down Expand Up @@ -82,9 +83,9 @@ function GradientOrb({
transition={{ duration, delay, repeat: Infinity, ease: "easeInOut" }}
/>
);
}
});

function AmbientBackground() {
const AmbientBackground = memo(function AmbientBackground() {
return (
<Box
position="absolute"
Expand Down Expand Up @@ -133,7 +134,7 @@ function AmbientBackground() {
/>
</Box>
);
}
});

// ── Icons ────────────────────────────────────────────────────────────────────
// Small inline line icons — no new icon-package dependency for four glyphs.
Expand Down
7 changes: 6 additions & 1 deletion src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Flex, Link as ChakraLink, Text } from "@chakra-ui/react";
import NextLink from "next/link";
import { memo } from "react";

export default function Footer() {
function Footer() {
return (
<Flex
as="footer"
Expand Down Expand Up @@ -37,3 +38,7 @@ export default function Footer() {
</Flex>
);
}

// Memoized: AppShell re-renders on every wallet / own-CTA change, and this
// component doesn't depend on either.
export default memo(Footer);
1 change: 1 addition & 0 deletions src/components/Navbar/Navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const walletMock = vi.hoisted(() => ({

vi.mock("@/context/StellarWalletContext", () => ({
useStellarWallet: vi.fn(() => walletMock),
useStellarWalletActions: vi.fn(() => walletMock),
}));

vi.mock("@/hooks/useSorobanQuery", () => ({
Expand Down
37 changes: 20 additions & 17 deletions src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import {
useDisclosure,
} from "@chakra-ui/react";
import { ChevronDownIcon, HamburgerIcon } from "@chakra-ui/icons";
import { useStellarWallet } from "@/context/StellarWalletContext";
import {
useStellarWallet,
useStellarWalletActions,
} from "@/context/StellarWalletContext";
import NextLink from "next/link";
import { usePathname } from "next/navigation";
import ThemeToggle from "@/components/ThemeToggle/ThemeToggle";
import { usePlatformStats } from "@/hooks/useSorobanQuery";
import { useToast } from "@/hooks/useToast";
import { useEffect, useState } from "react";
import { memo, useCallback, useEffect, useState } from "react";

const MORE_LINKS = [
{ href: "/prices", label: "Prices" },
Expand All @@ -49,7 +52,7 @@ function shortenStellarAddress(address: string) {
return `${address.slice(0, 4)}…${address.slice(-4)}`;
}

function Logo() {
const Logo = memo(function Logo() {
return (
<NextLink
href="/"
Expand All @@ -68,9 +71,9 @@ function Logo() {
</span>
</NextLink>
);
}
});

function NavLink({
const NavLink = memo(function NavLink({
href,
children,
onClick,
Expand All @@ -95,9 +98,9 @@ function NavLink({
{children}
</NextLink>
);
}
});

function MoreMenu() {
const MoreMenu = memo(function MoreMenu() {
const pathname = usePathname();
const isActive = MORE_LINKS.some((link) => isActiveRoute(pathname, link.href));
return (
Expand Down Expand Up @@ -130,9 +133,9 @@ function MoreMenu() {
</MenuList>
</Menu>
);
}
});

function StatPill({ label, value, pulse = false }: { label: string; value: string; pulse?: boolean }) {
const StatPill = memo(function StatPill({ label, value, pulse = false }: { label: string; value: string; pulse?: boolean }) {
return (
<div className="flex items-center gap-1.5 rounded-full border border-[color:var(--chakra-colors-app-border)] bg-[color:var(--chakra-colors-app-surface)] px-3 py-1 text-xs whitespace-nowrap">
{pulse && (
Expand All @@ -142,7 +145,7 @@ function StatPill({ label, value, pulse = false }: { label: string; value: strin
<span className="font-bold text-[color:var(--chakra-colors-app-text)]">{value}</span>
</div>
);
}
});

function formatCount(value: number | undefined | null): string {
return value || value === 0 ? value.toLocaleString() : "—";
Expand All @@ -152,12 +155,12 @@ function formatCount(value: number | undefined | null): string {
// wallet pill used to be a plain, non-interactive display of the shortened
// address, with the only disconnect affordance living in the separate
// floating ConnectWalletButton. Fold both into a menu on the pill itself.
function WalletMenu({ publicKey }: { publicKey: string }) {
const { disconnect } = useStellarWallet();
const WalletMenu = memo(function WalletMenu({ publicKey }: { publicKey: string }) {
const { disconnect } = useStellarWalletActions();
const [copied, setCopied] = useState(false);
const toast = useToast();
const { error: toastError } = useToast();

const handleCopy = async () => {
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(publicKey);
setCopied(true);
Expand All @@ -166,9 +169,9 @@ function WalletMenu({ publicKey }: { publicKey: string }) {
// navigator.clipboard is unavailable in non-secure/hardened contexts —
// surface it instead of failing silently, and include the address so
// the user can still copy it manually.
toast.error("Couldn't copy address", publicKey);
toastError("Couldn't copy address", publicKey);
}
};
}, [publicKey, toastError]);

return (
<Menu placement="bottom-end">
Expand Down Expand Up @@ -201,7 +204,7 @@ function WalletMenu({ publicKey }: { publicKey: string }) {
</MenuList>
</Menu>
);
}
});

function MobileNavDrawer({
isOpen,
Expand Down
8 changes: 6 additions & 2 deletions src/components/OfflineBanner/OfflineBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { Alert, AlertIcon } from "@chakra-ui/react";
import { useSyncExternalStore } from "react";
import { memo, useSyncExternalStore } from "react";

function subscribe(callback: () => void) {
window.addEventListener("online", callback);
Expand All @@ -23,7 +23,7 @@ export function useOnlineStatus(): boolean {

// Issue #528: tell the user plainly when the device is offline, instead of
// leaving every query to fail with its own error.
export default function OfflineBanner() {
function OfflineBanner() {
const isOnline = useOnlineStatus();

if (isOnline) return null;
Expand All @@ -44,3 +44,7 @@ export default function OfflineBanner() {
</Alert>
);
}

// Memoized: AppShell re-renders on every wallet / own-CTA change, and this
// component doesn't depend on either.
export default memo(OfflineBanner);
7 changes: 6 additions & 1 deletion src/components/RefetchIndicator/RefetchIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { keyframes } from "@emotion/react";
import { Box } from "@chakra-ui/react";
import { useIsFetching } from "@tanstack/react-query";
import { memo } from "react";

const slide = keyframes`
0% { transform: translateX(-100%); }
Expand All @@ -16,7 +17,7 @@ const slide = keyframes`
* this shows it is being updated. The animation is dropped for users who
* prefer reduced motion, leaving a static bar.
*/
export default function RefetchIndicator() {
function RefetchIndicator() {
const refetching =
useIsFetching({ predicate: (query) => query.state.data !== undefined }) > 0;

Expand Down Expand Up @@ -44,3 +45,7 @@ export default function RefetchIndicator() {
</Box>
);
}

// Memoized: AppShell re-renders on every wallet / own-CTA change, and this
// component doesn't depend on either.
export default memo(RefetchIndicator);
7 changes: 6 additions & 1 deletion src/components/RpcUnreachableBanner/RpcUnreachableBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { useRpcHealth } from "@/hooks/useSorobanQuery";
import { Alert, AlertIcon } from "@chakra-ui/react";
import { memo } from "react";

// Issue #248: when the Soroban RPC is down, every page independently shows
// its own query error instead of one unified, immediate signal. A single
// global banner is a clearer, faster way for a user to understand "this is
// a whole-app connectivity issue," not something wrong with one page.
export default function RpcUnreachableBanner() {
function RpcUnreachableBanner() {
const { isUnreachable } = useRpcHealth();

if (!isUnreachable) return null;
Expand All @@ -29,3 +30,7 @@ export default function RpcUnreachableBanner() {
</Alert>
);
}

// Memoized: AppShell re-renders on every wallet / own-CTA change, and this
// component doesn't depend on either.
export default memo(RpcUnreachableBanner);
7 changes: 6 additions & 1 deletion src/components/SkipLink/SkipLink.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Link } from "@chakra-ui/react";
import { memo } from "react";

export const MAIN_CONTENT_ID = "main-content";

Expand All @@ -12,7 +13,7 @@ export const MAIN_CONTENT_ID = "main-content";
* keyboard focus, so the next Tab would land back in the navbar. Focus the
* target explicitly (it has tabIndex={-1}) so the skip actually takes effect.
*/
export default function SkipLink() {
function SkipLink() {
return (
<Link
href={`#${MAIN_CONTENT_ID}`}
Expand Down Expand Up @@ -41,3 +42,7 @@ export default function SkipLink() {
</Link>
);
}

// Memoized: AppShell re-renders on every wallet / own-CTA change, and this
// component doesn't depend on either.
export default memo(SkipLink);
5 changes: 4 additions & 1 deletion src/context/ErrorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createContext,
useContext,
useEffect,
useMemo,
type ReactNode,
} from "react";

Expand All @@ -31,8 +32,10 @@ export function ErrorProvider({ children }: { children: ReactNode }) {
return cleanup;
}, []);

const value = useMemo(() => ({ toast }), [toast]);

return (
<ErrorContext.Provider value={{ toast }}>
<ErrorContext.Provider value={value}>
{children}
</ErrorContext.Provider>
);
Expand Down
Loading