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
22 changes: 13 additions & 9 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslations } from "next-intl";
import { motion } from "framer-motion";
import { memo, useCallback, useMemo } from "react";

function getNavItems(t: ReturnType<typeof useTranslations>) {
return [
Expand Down Expand Up @@ -70,7 +71,7 @@ interface SidebarProps {
onMobileOpenChange: (open: boolean) => void;
}

function NavLinks({
const NavLinks = memo(function NavLinks({
pathname,
t,
onNavigate,
Expand All @@ -79,7 +80,7 @@ function NavLinks({
t: ReturnType<typeof useTranslations>;
onNavigate?: () => void;
}) {
const navItems = getNavItems(t);
const navItems = useMemo(() => getNavItems(t), [t]);

return (
<nav aria-label="Dashboard navigation" className="flex flex-1 flex-col gap-1 px-4 py-6">
Expand All @@ -91,7 +92,7 @@ function NavLinks({
if (isExternal) {
return (
<a key={item.href} href={item.href} target="_blank" rel="noopener noreferrer" onClick={onNavigate}
className="flex items-center gap-3 rounded-lg px-3 py-2.5 transition-all text-[#6B6B6B] hover:bg-[var(--pluto-50)] hover:text-[var(--pluto-700)]">
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-[#6B6B6B] transition-colors duration-150 hover:bg-[var(--pluto-50)] hover:text-[var(--pluto-800)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--pluto-300)]">
<span className="shrink-0">{item.icon}</span>
<span className="text-xs font-semibold tracking-wide">{item.label}</span>
<svg className="h-3 w-3 ml-auto opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
Expand All @@ -105,12 +106,12 @@ function NavLinks({
href={item.href}
prefetch={true}
onClick={onNavigate}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 transition-all ${
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--pluto-300)] ${
isActive
? "bg-[var(--pluto-500)] text-white"
: isHighlight
? "border border-[var(--pluto-200)] bg-[var(--pluto-50)] text-[var(--pluto-700)] hover:bg-[var(--pluto-500)] hover:text-white hover:border-[var(--pluto-500)]"
: "text-[#6B6B6B] hover:bg-[var(--pluto-50)] hover:text-[var(--pluto-700)]"
? "border border-[var(--pluto-200)] bg-[var(--pluto-50)] text-[var(--pluto-700)] hover:border-[var(--pluto-500)] hover:bg-[var(--pluto-500)] hover:text-white"
: "text-[#6B6B6B] hover:bg-[var(--pluto-100)] hover:text-[var(--pluto-800)]"
}`}
>
<span className="shrink-0">{item.icon}</span>
Expand All @@ -121,7 +122,7 @@ function NavLinks({

<div className="mt-auto pt-4 border-t border-[#E8E8E8]">
<Link href="/" onClick={onNavigate}
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-[#6B6B6B] transition-all hover:bg-[#F5F5F5] hover:text-[#0A0A0A]"
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-[#6B6B6B] transition-colors duration-150 hover:bg-[var(--pluto-50)] hover:text-[var(--pluto-800)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--pluto-300)]"
>
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
Expand All @@ -131,14 +132,17 @@ function NavLinks({
</div>
</nav>
);
}
});

NavLinks.displayName = "NavLinks";

export default function Sidebar({
mobileOpen,
onMobileOpenChange,
}: SidebarProps) {
const t = useTranslations("sidebar");
const pathname = usePathname();
const handleNavigate = useCallback(() => onMobileOpenChange(false), [onMobileOpenChange]);

// Note: Collapsible logic removed to fix linting as it's not currently used in the UI.

Expand All @@ -153,7 +157,7 @@ export default function Sidebar({
<NavLinks
pathname={pathname}
t={t}
onNavigate={() => onMobileOpenChange(false)}
onNavigate={handleNavigate}
/>
</>
);
Expand Down
28 changes: 14 additions & 14 deletions frontend/src/components/WalletSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ function WalletConnectIcon() {
);
}

const ICONS: Record<string, React.ReactNode> = {
freighter: <FreighterIcon />,
walletconnect: <WalletConnectIcon />,
};

const SUBTITLES: Record<string, string> = {
freighter: "Browser extension wallet",
walletconnect: "Mobile & desktop wallets",
};

export default function WalletSelector({ networkPassphrase, onConnected }: WalletSelectorProps) {
const t = useTranslations("walletSelector");
const { providers, activeProvider, selectProvider } = useWallet();
Expand Down Expand Up @@ -99,16 +109,6 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
}
}

const ICONS: Record<string, React.ReactNode> = {
freighter: <FreighterIcon />,
walletconnect: <WalletConnectIcon />,
};

const SUBTITLES: Record<string, string> = {
freighter: "Browser extension wallet",
walletconnect: "Mobile & desktop wallets",
};

return (
<div className="flex flex-col gap-4">
<div>
Expand All @@ -130,10 +130,10 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
type="button"
disabled={isDisabled || connecting !== null}
onClick={() => handleSelect(p.id)}
className="group relative flex h-16 w-full items-center gap-4 rounded-2xl border border-[#E8E8E8] bg-white px-5 text-left shadow-sm transition-all hover:border-[var(--pluto-400)] hover:shadow-[0_4px_20px_rgba(74,111,165,0.12)] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
className="group relative flex h-16 w-full items-center gap-4 rounded-2xl border border-[#E8E8E8] bg-white px-5 text-left shadow-sm transition-[border-color,box-shadow,transform] duration-200 hover:-translate-y-px hover:border-[var(--pluto-400)] hover:shadow-[0_6px_22px_rgba(74,111,165,0.12)] active:translate-y-0 active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--pluto-300)] disabled:cursor-not-allowed disabled:opacity-40"
>
{/* Icon */}
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] transition-all group-hover:border-[var(--pluto-200)] group-hover:bg-[var(--pluto-50)]">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] transition-colors duration-200 group-hover:border-[var(--pluto-200)] group-hover:bg-[var(--pluto-50)]">
{ICONS[p.id] ?? (
<svg className="h-5 w-5 text-[#6B6B6B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
Expand Down Expand Up @@ -162,7 +162,7 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle

{/* Arrow */}
{!isConnecting && !isDisabled && (
<svg className="h-4 w-4 shrink-0 text-[#C0C0C0] group-hover:text-[var(--pluto-500)] transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="h-4 w-4 shrink-0 text-[#C0C0C0] transition-colors duration-200 group-hover:text-[var(--pluto-500)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
Expand Down Expand Up @@ -194,7 +194,7 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
href="https://freighter.app"
target="_blank"
rel="noopener noreferrer"
className="text-center text-[10px] font-bold uppercase tracking-widest text-[var(--pluto-500)] hover:text-[var(--pluto-700)] transition-colors"
className="text-center text-[10px] font-bold uppercase tracking-widest text-[var(--pluto-500)] transition-colors duration-150 hover:text-[var(--pluto-700)]"
>
Don&apos;t have Freighter? Install it →
</a>
Expand Down
30 changes: 17 additions & 13 deletions frontend/src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const BASE_CLASSES =
"group relative flex items-center justify-center rounded-xl px-6 font-bold transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-mint focus-visible:ring-offset-2 focus-visible:ring-offset-night";

const VARIANT_CLASSES: Record<NonNullable<ButtonProps["variant"]>, string> = {
primary: "h-12 bg-mint text-black hover:bg-glow",
secondary:
"h-12 border border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white",
};

const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className = "",
Expand All @@ -18,22 +27,14 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
},
ref,
) => {
const baseClasses =
"group relative flex items-center justify-center rounded-xl px-6 font-bold transition-all disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-mint focus-visible:ring-offset-2 focus-visible:ring-offset-night";

// For primary button, height 12 (h-12) was used typically, but let's allow override or set default
const primaryClasses = "h-12 bg-mint text-black hover:bg-glow";
const secondaryClasses =
"h-12 border border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white";

const variantClasses =
variant === "primary" ? primaryClasses : secondaryClasses;
const variantClasses = VARIANT_CLASSES[variant];
const showPrimaryGlow = variant === "primary";

return (
<button
ref={ref}
disabled={disabled || isLoading}
className={`${baseClasses} ${variantClasses} ${className}`}
className={`${BASE_CLASSES} ${variantClasses} ${className}`}
{...props}
>
{isLoading ? (
Expand All @@ -47,11 +48,14 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
) : (
children
)}
{variant === "primary" && (
{showPrimaryGlow && (
<div className="absolute inset-0 -z-10 bg-mint/20 opacity-0 blur-xl transition-opacity group-hover:opacity-100" />
)}
</button>
);
},
);
ButtonBase.displayName = "Button";

export const Button = React.memo(ButtonBase);
Button.displayName = "Button";
2 changes: 1 addition & 1 deletion frontend/tests/e2e/checkout.visual.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ test.describe("Checkout Visual Regression", () => {
test("checkout layout remains stable across viewports", async ({ page }) => {
const checkoutMain = page.locator("main");
await expect(checkoutMain).toBeVisible();
await expect(page.getByText("Complete Payment")).toBeVisible();
await expect(page.getByText("Styled payment")).toBeVisible();
await expect(page.locator("code", { hasText: "GRECIPIENTADDRESS" })).toBeVisible();

const noOverflow = await expectNoHorizontalOverflow(page);
expect(noOverflow).toBeTruthy();
Expand Down
Loading