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
60 changes: 55 additions & 5 deletions src/components/layout/mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,66 @@ import { usePathname } from "next/navigation";
import { useAuthStore } from "@/store/auth-store";
import { cn } from "@/lib/utils/cn";
import { isNavLinkActive, navLinks } from "./nav-links";
import { useState, useRef, useEffect } from "react";
import { Avatar, AvatarFallback, getInitials } from "@/components/ui/avatar";
import { truncateAddress } from "@/lib/utils/format";

export function MobileNav() {
const pathname = usePathname();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const hasHydrated = useAuthStore((s) => s.hasHydrated);
const walletAddress = useAuthStore((s) => s.walletAddress);
const [swipeStartX, setSwipeStartX] = useState(0);
const touchRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleTouchStart = (e: TouchEvent) => {
setSwipeStartX(e.touches[0].clientX);
};

const handleTouchEnd = (e: TouchEvent) => {
const swipeEndX = e.changedTouches[0].clientX;
const diff = swipeEndX - swipeStartX;
if (Math.abs(diff) > 50 && diff > 0) {
window.scrollTo({ top: 0, behavior: "smooth" });
}
};

const nav = touchRef.current;
if (nav) {
nav.addEventListener("touchstart", handleTouchStart);
nav.addEventListener("touchend", handleTouchEnd);
return () => {
nav.removeEventListener("touchstart", handleTouchStart);
nav.removeEventListener("touchend", handleTouchEnd);
};
}
}, [swipeStartX]);

if (!hasHydrated || !isAuthenticated) return null;

return (
<nav aria-label="Bottom navigation" className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white lg:hidden">
<nav
ref={touchRef}
aria-label="Bottom navigation"
className="fixed bottom-0 left-0 right-0 z-50 border-t border-gray-200 bg-white/95 backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/95 lg:hidden transition-transform duration-300"
>
{/* User Info Section */}
<div className="border-b border-gray-200 px-4 py-3 dark:border-gray-800">
<div className="flex items-center gap-2">
<Avatar size="sm">
<AvatarFallback>{walletAddress ? getInitials(walletAddress) : "U"}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="text-xs font-medium text-gray-900 dark:text-gray-100">Account</p>
<p className="truncate text-[10px] text-gray-500 dark:text-gray-400">
{walletAddress ? truncateAddress(walletAddress) : "Not connected"}
</p>
</div>
</div>
</div>

{/* Navigation Links */}
<div className="flex items-center justify-evenly px-1 py-1">
{navLinks.map((link) => {
const isActive = isNavLinkActive(pathname, link.href);
Expand All @@ -25,13 +75,13 @@ export function MobileNav() {
aria-label={link.label}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex min-h-11 min-w-11 flex-col items-center gap-0.5 rounded-lg px-1.5 py-1.5 text-[10px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 sm:px-3 sm:py-2 sm:text-xs",
"flex h-14 w-14 flex-col items-center gap-0.5 rounded-lg text-[10px] font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 sm:h-16 sm:w-16 sm:text-xs",
isActive
? "text-primary-600"
: "text-gray-500 hover:text-gray-900"
? "bg-stellar-purple/10 text-stellar-purple dark:bg-stellar-purple/20"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
)}
>
<link.icon className="h-5 w-5 shrink-0" aria-hidden="true" />
<link.icon className="h-5 w-5 shrink-0 transition-transform duration-200" aria-hidden="true" />
<span className="max-sm:hidden">{link.label}</span>
</Link>
);
Expand Down
58 changes: 40 additions & 18 deletions src/components/shared/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function Toast({
}: ToastProps) {
const [visible, setVisible] = useState(true);
const [dragX, setDragX] = useState(0);
const [progress, setProgress] = useState(100);
const dragState = useRef<{ startX: number; dragging: boolean } | null>(null);
const Icon = variantIcons[variant];

Expand All @@ -84,8 +85,19 @@ export function Toast({

useEffect(() => {
if (autoClose > 0) {
const startTime = Date.now();
const interval = setInterval(() => {
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, 100 - (elapsed / autoClose) * 100);
setProgress(remaining);
if (remaining <= 0) clearInterval(interval);
}, 50);

const timer = setTimeout(dismiss, autoClose);
return () => clearTimeout(timer);
return () => {
clearInterval(interval);
clearTimeout(timer);
};
}
}, [autoClose, dismiss]);

Expand Down Expand Up @@ -118,7 +130,7 @@ export function Toast({
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
className={cn(
"fixed right-4 z-[100] flex items-center gap-3 rounded-lg border px-4 py-3 shadow-lg transition-all touch-pan-y",
"fixed right-4 z-[100] flex flex-col gap-0 rounded-lg border shadow-lg transition-all touch-pan-y overflow-hidden",
visible ? "opacity-100" : "translate-y-2 opacity-0",
variantStyles[variant],
className
Expand All @@ -129,26 +141,36 @@ export function Toast({
opacity: visible ? Math.max(1 - Math.abs(dragX) / 200, 0.2) : undefined,
}}
>
<Icon className="h-5 w-5 flex-shrink-0" />
<p className="text-sm font-medium">{message}</p>
{action && (
<div className="flex items-center gap-3 px-4 py-3">
<Icon className="h-5 w-5 flex-shrink-0" />
<p className="text-sm font-medium flex-1">{message}</p>
{action && (
<button
onClick={() => {
action.onClick();
dismiss();
}}
className="flex-shrink-0 text-sm font-semibold underline underline-offset-2 hover:opacity-70"
>
{action.label}
</button>
)}
<button
onClick={() => {
action.onClick();
dismiss();
}}
className="ml-1 flex-shrink-0 text-sm font-semibold underline underline-offset-2 hover:opacity-70"
onClick={dismiss}
aria-label="Dismiss notification"
className="flex-shrink-0 hover:opacity-70"
>
{action.label}
<X className="h-4 w-4" />
</button>
</div>
{progress < 100 && (
<div className="h-1 w-full bg-black/10 dark:bg-white/10">
<div
className="h-full transition-all duration-100 ease-linear bg-current opacity-50"
style={{ width: `${progress}%` }}
/>
</div>
)}
<button
onClick={dismiss}
aria-label="Dismiss notification"
className="ml-2 flex-shrink-0 hover:opacity-70"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
Expand Down