Skip to content

Commit 75e0b06

Browse files
authored
Merge pull request #576 from Akanimoh12/feat/frontend-ui-touchups-528-537-544-534
style(frontend): optimize Button, Sidebar, and wallet modal interactions
2 parents 9d82bd9 + 68fca8e commit 75e0b06

4 files changed

Lines changed: 45 additions & 37 deletions

File tree

frontend/src/components/Sidebar.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Link from "next/link";
44
import { usePathname } from "next/navigation";
55
import { useTranslations } from "next-intl";
66
import { motion } from "framer-motion";
7+
import { memo, useCallback, useMemo } from "react";
78

89
function getNavItems(t: ReturnType<typeof useTranslations>) {
910
return [
@@ -70,7 +71,7 @@ interface SidebarProps {
7071
onMobileOpenChange: (open: boolean) => void;
7172
}
7273

73-
function NavLinks({
74+
const NavLinks = memo(function NavLinks({
7475
pathname,
7576
t,
7677
onNavigate,
@@ -79,7 +80,7 @@ function NavLinks({
7980
t: ReturnType<typeof useTranslations>;
8081
onNavigate?: () => void;
8182
}) {
82-
const navItems = getNavItems(t);
83+
const navItems = useMemo(() => getNavItems(t), [t]);
8384

8485
return (
8586
<nav aria-label="Dashboard navigation" className="flex flex-1 flex-col gap-1 px-4 py-6">
@@ -91,7 +92,7 @@ function NavLinks({
9192
if (isExternal) {
9293
return (
9394
<a key={item.href} href={item.href} target="_blank" rel="noopener noreferrer" onClick={onNavigate}
94-
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)]">
95+
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)]">
9596
<span className="shrink-0">{item.icon}</span>
9697
<span className="text-xs font-semibold tracking-wide">{item.label}</span>
9798
<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>
@@ -105,12 +106,12 @@ function NavLinks({
105106
href={item.href}
106107
prefetch={true}
107108
onClick={onNavigate}
108-
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 transition-all ${
109+
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)] ${
109110
isActive
110111
? "bg-[var(--pluto-500)] text-white"
111112
: isHighlight
112-
? "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)]"
113-
: "text-[#6B6B6B] hover:bg-[var(--pluto-50)] hover:text-[var(--pluto-700)]"
113+
? "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"
114+
: "text-[#6B6B6B] hover:bg-[var(--pluto-100)] hover:text-[var(--pluto-800)]"
114115
}`}
115116
>
116117
<span className="shrink-0">{item.icon}</span>
@@ -121,7 +122,7 @@ function NavLinks({
121122

122123
<div className="mt-auto pt-4 border-t border-[#E8E8E8]">
123124
<Link href="/" onClick={onNavigate}
124-
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-[#6B6B6B] transition-all hover:bg-[#F5F5F5] hover:text-[#0A0A0A]"
125+
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)]"
125126
>
126127
<svg className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127128
<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" />
@@ -131,14 +132,17 @@ function NavLinks({
131132
</div>
132133
</nav>
133134
);
134-
}
135+
});
136+
137+
NavLinks.displayName = "NavLinks";
135138

136139
export default function Sidebar({
137140
mobileOpen,
138141
onMobileOpenChange,
139142
}: SidebarProps) {
140143
const t = useTranslations("sidebar");
141144
const pathname = usePathname();
145+
const handleNavigate = useCallback(() => onMobileOpenChange(false), [onMobileOpenChange]);
142146

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

@@ -153,7 +157,7 @@ export default function Sidebar({
153157
<NavLinks
154158
pathname={pathname}
155159
t={t}
156-
onNavigate={() => onMobileOpenChange(false)}
160+
onNavigate={handleNavigate}
157161
/>
158162
</>
159163
);

frontend/src/components/WalletSelector.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ function WalletConnectIcon() {
3131
);
3232
}
3333

34+
const ICONS: Record<string, React.ReactNode> = {
35+
freighter: <FreighterIcon />,
36+
walletconnect: <WalletConnectIcon />,
37+
};
38+
39+
const SUBTITLES: Record<string, string> = {
40+
freighter: "Browser extension wallet",
41+
walletconnect: "Mobile & desktop wallets",
42+
};
43+
3444
export default function WalletSelector({ networkPassphrase, onConnected }: WalletSelectorProps) {
3545
const t = useTranslations("walletSelector");
3646
const { providers, activeProvider, selectProvider } = useWallet();
@@ -99,16 +109,6 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
99109
}
100110
}
101111

102-
const ICONS: Record<string, React.ReactNode> = {
103-
freighter: <FreighterIcon />,
104-
walletconnect: <WalletConnectIcon />,
105-
};
106-
107-
const SUBTITLES: Record<string, string> = {
108-
freighter: "Browser extension wallet",
109-
walletconnect: "Mobile & desktop wallets",
110-
};
111-
112112
return (
113113
<div className="flex flex-col gap-4">
114114
<div>
@@ -130,10 +130,10 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
130130
type="button"
131131
disabled={isDisabled || connecting !== null}
132132
onClick={() => handleSelect(p.id)}
133-
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"
133+
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"
134134
>
135135
{/* Icon */}
136-
<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)]">
136+
<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)]">
137137
{ICONS[p.id] ?? (
138138
<svg className="h-5 w-5 text-[#6B6B6B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139139
<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" />
@@ -162,7 +162,7 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
162162

163163
{/* Arrow */}
164164
{!isConnecting && !isDisabled && (
165-
<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">
165+
<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">
166166
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
167167
</svg>
168168
)}
@@ -194,7 +194,7 @@ export default function WalletSelector({ networkPassphrase, onConnected }: Walle
194194
href="https://freighter.app"
195195
target="_blank"
196196
rel="noopener noreferrer"
197-
className="text-center text-[10px] font-bold uppercase tracking-widest text-[var(--pluto-500)] hover:text-[var(--pluto-700)] transition-colors"
197+
className="text-center text-[10px] font-bold uppercase tracking-widest text-[var(--pluto-500)] transition-colors duration-150 hover:text-[var(--pluto-700)]"
198198
>
199199
Don&apos;t have Freighter? Install it →
200200
</a>

frontend/src/components/ui/Button.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
66
isLoading?: boolean;
77
}
88

9-
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
9+
const BASE_CLASSES =
10+
"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";
11+
12+
const VARIANT_CLASSES: Record<NonNullable<ButtonProps["variant"]>, string> = {
13+
primary: "h-12 bg-mint text-black hover:bg-glow",
14+
secondary:
15+
"h-12 border border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white",
16+
};
17+
18+
const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonProps>(
1019
(
1120
{
1221
className = "",
@@ -18,22 +27,14 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1827
},
1928
ref,
2029
) => {
21-
const baseClasses =
22-
"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";
23-
24-
// For primary button, height 12 (h-12) was used typically, but let's allow override or set default
25-
const primaryClasses = "h-12 bg-mint text-black hover:bg-glow";
26-
const secondaryClasses =
27-
"h-12 border border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white";
28-
29-
const variantClasses =
30-
variant === "primary" ? primaryClasses : secondaryClasses;
30+
const variantClasses = VARIANT_CLASSES[variant];
31+
const showPrimaryGlow = variant === "primary";
3132

3233
return (
3334
<button
3435
ref={ref}
3536
disabled={disabled || isLoading}
36-
className={`${baseClasses} ${variantClasses} ${className}`}
37+
className={`${BASE_CLASSES} ${variantClasses} ${className}`}
3738
{...props}
3839
>
3940
{isLoading ? (
@@ -47,11 +48,14 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
4748
) : (
4849
children
4950
)}
50-
{variant === "primary" && (
51+
{showPrimaryGlow && (
5152
<div className="absolute inset-0 -z-10 bg-mint/20 opacity-0 blur-xl transition-opacity group-hover:opacity-100" />
5253
)}
5354
</button>
5455
);
5556
},
5657
);
58+
ButtonBase.displayName = "Button";
59+
60+
export const Button = React.memo(ButtonBase);
5761
Button.displayName = "Button";

frontend/tests/e2e/checkout.visual.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ test.describe("Checkout Visual Regression", () => {
1616
test("checkout layout remains stable across viewports", async ({ page }) => {
1717
const checkoutMain = page.locator("main");
1818
await expect(checkoutMain).toBeVisible();
19-
await expect(page.getByText("Complete Payment")).toBeVisible();
2019
await expect(page.getByText("Styled payment")).toBeVisible();
20+
await expect(page.locator("code", { hasText: "GRECIPIENTADDRESS" })).toBeVisible();
2121

2222
const noOverflow = await expectNoHorizontalOverflow(page);
2323
expect(noOverflow).toBeTruthy();

0 commit comments

Comments
 (0)