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
23 changes: 15 additions & 8 deletions src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { useTheme } from "@/components/theme/theme-provider";
import { NotificationBell } from "@/components/shared/notification-bell";

export function Header() {
const { isAuthenticated, walletAddress } = useAuthStore();
const { isAuthenticated, walletAddress, disconnect } = useAuthStore();
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const { resolvedTheme, toggleTheme } = useTheme();
Expand Down Expand Up @@ -80,16 +80,23 @@ export function Header() {
<DropdownMenuContent align="end" className="w-56 dark:border-gray-800 dark:bg-gray-900">
<DropdownMenuLabel className="text-gray-900 dark:text-gray-100">My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
<DropdownMenuItem asChild>
<Link href="/profile" className="flex items-center w-full cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
<DropdownMenuItem asChild>
<Link href="/settings" className="flex items-center w-full cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600 focus:text-red-700">
<DropdownMenuItem
onClick={() => disconnect()}
className="text-red-600 focus:text-red-700 cursor-pointer"
>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
Expand Down
149 changes: 126 additions & 23 deletions src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,140 @@ 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, useEffect } from "react";
import { ChevronLeft, ChevronRight, Menu, X } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

export function Sidebar() {
const pathname = usePathname();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const hasHydrated = useAuthStore((s) => s.hasHydrated);
const [isCollapsed, setIsCollapsed] = useState(false);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);

// Close mobile drawer on route change
useEffect(() => {
setMobileDrawerOpen(false);
}, [pathname]);

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

return (
<aside className="hidden lg:flex flex-col w-64 border-r border-gray-200 bg-gray-50/50 min-h-[calc(100vh-4rem)] dark:border-gray-800 dark:bg-gray-900/50">
<nav aria-label="Course navigation" className="flex-1 px-3 py-4 space-y-1">
{navLinks.map((link) => {
const isActive = isNavLinkActive(pathname, link.href);
return (
<Link
key={link.href}
href={link.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
isActive
? "bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100"
)}
>
<link.icon className="h-5 w-5" />
{link.label}
</Link>
);
})}
</nav>
</aside>
<>
{/* Mobile Drawer Trigger Button */}
<div className="lg:hidden fixed bottom-4 right-4 z-40">
<button
type="button"
onClick={() => setMobileDrawerOpen(true)}
className="flex h-12 w-12 items-center justify-center rounded-full bg-stellar-purple text-white shadow-lg hover:bg-purple-700 transition-colors"
aria-label="Open navigation drawer"
>
<Menu className="h-6 w-6" />
</button>
</div>

{/* Mobile Drawer Backdrop Overlay */}
{mobileDrawerOpen && (
<div
className="lg:hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity"
onClick={() => setMobileDrawerOpen(false)}
/>
)}

{/* Mobile Drawer Content */}
<aside
className={cn(
"lg:hidden fixed inset-y-0 left-0 z-50 flex w-72 flex-col bg-white p-4 shadow-2xl transition-transform duration-300 dark:bg-gray-900",
mobileDrawerOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<div className="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-gray-800">
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">Navigation</span>
<button
type="button"
onClick={() => setMobileDrawerOpen(false)}
className="p-1 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
aria-label="Close drawer"
>
<X className="h-5 w-5" />
</button>
</div>
<nav aria-label="Mobile course navigation" className="mt-4 space-y-1">
{navLinks.map((link) => {
const isActive = isNavLinkActive(pathname, link.href);
return (
<Link
key={link.href}
href={link.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm font-semibold transition-all",
isActive
? "bg-stellar-purple text-white shadow-md shadow-stellar-purple/20"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100"
)}
>
<link.icon className="h-5 w-5" />
{link.label}
</Link>
);
})}
</nav>
</aside>

{/* Desktop Sidebar */}
<aside
className={cn(
"hidden lg:flex flex-col border-r border-gray-200 bg-gray-50/50 min-h-[calc(100vh-4rem)] transition-all duration-300 dark:border-gray-800 dark:bg-gray-900/50 relative",
isCollapsed ? "w-16" : "w-64"
)}
>
{/* Collapse / Expand Toggle Button */}
<button
type="button"
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-6 flex h-6 w-6 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-600 shadow-sm hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 z-10"
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{isCollapsed ? <ChevronRight className="h-3.5 w-3.5" /> : <ChevronLeft className="h-3.5 w-3.5" />}
</button>

<nav aria-label="Course navigation" className="flex-1 px-3 py-4 space-y-1">
{navLinks.map((link) => {
const isActive = isNavLinkActive(pathname, link.href);
const content = (
<Link
key={link.href}
href={link.href}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all",
isActive
? "bg-stellar-purple text-white shadow-sm font-semibold dark:bg-stellar-purple dark:text-white"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100",
isCollapsed && "justify-center px-0"
)}
>
<link.icon className="h-5 w-5 shrink-0" />
{!isCollapsed && <span>{link.label}</span>}
</Link>
);

if (isCollapsed) {
return (
<Tooltip key={link.href}>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="right">
<p>{link.label}</p>
</TooltipContent>
</Tooltip>
);
}

return content;
})}
</nav>
</aside>
</>
);
}
111 changes: 82 additions & 29 deletions src/components/wallet/balance-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import { useRewards } from "@/lib/hooks/use-rewards";
import { formatTokenBalance } from "@/lib/utils/format";
import { Coins, TrendingUp } from "lucide-react";
import { Coins, ExternalLink, Copy, Check } from "lucide-react";
import { cn } from "@/lib/utils/cn";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useState, useEffect } from "react";
import Link from "next/link";

interface BalanceDisplayProps {
className?: string;
Expand All @@ -13,16 +15,39 @@ interface BalanceDisplayProps {

export function BalanceDisplay({ className, compact = false }: BalanceDisplayProps) {
const { balances, loading } = useRewards();
const [usdRate, setUsdRate] = useState<number | null>(null);
const [priceLoading, setPriceLoading] = useState(true);
const [copied, setCopied] = useState(false);

const primaryBalance = balances[0];
const balance = primaryBalance
? formatTokenBalance(primaryBalance.balance, primaryBalance.decimals)
: "0";
const tokenCode = primaryBalance?.tokenCode || "LEARN";
const contractAddress = primaryBalance?.contractAddress || "CD5...LEARNCONTRACT";
const balanceVal = primaryBalance ? primaryBalance.balance : 0;
const balanceStr = formatTokenBalance(balanceVal, primaryBalance?.decimals || 7);

if (loading) {
useEffect(() => {
// Simulate/fetch USD price conversion rate
setPriceLoading(true);
const timer = setTimeout(() => {
setUsdRate(0.15); // 1 LEARN = $0.15 USD
setPriceLoading(false);
}, 500);
return () => clearTimeout(timer);
}, [primaryBalance]);

const handleCopyContract = (e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(contractAddress);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

const usdValue = usdRate !== null ? (balanceVal * usdRate).toFixed(2) : "0.00";

if (loading || priceLoading) {
return (
<div className={cn("animate-pulse", className)}>
<div className="h-8 w-24 rounded bg-gray-200" />
<div className="h-20 w-full rounded-xl bg-gray-200 dark:bg-gray-800" />
</div>
);
}
Expand All @@ -33,41 +58,69 @@ export function BalanceDisplay({ className, compact = false }: BalanceDisplayPro
<TooltipTrigger asChild>
<div className={cn("flex items-center gap-1.5 text-sm font-medium cursor-help", className)}>
<Coins className="h-4 w-4 text-stellar-purple" />
<span>{balance}</span>
<span className="text-gray-500">{primaryBalance?.tokenCode || "LEARN"}</span>
<span>{balanceStr}</span>
<span className="text-gray-500">{tokenCode}</span>
<span className="text-xs text-gray-400">($ {usdValue})</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Available {primaryBalance?.tokenCode || "LEARN"} balance</p>
<p>Available {tokenCode} balance (${usdValue} USD)</p>
</TooltipContent>
</Tooltip>
);
}

return (
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"rounded-xl border border-gray-200 bg-gradient-to-br from-stellar-purple/5 to-stellar-blue/5 p-4 cursor-help",
className
)}
<div
className={cn(
"rounded-xl border border-gray-200 bg-gradient-to-br from-stellar-purple/5 to-stellar-blue/5 p-4 dark:border-gray-800 dark:bg-gray-900/50",
className
)}
>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400 mb-1">
<div className="flex items-center gap-2">
<Coins className="h-4 w-4 text-stellar-purple" />
Token Balance
</div>
<Link
href="/transactions"
className="flex items-center gap-1 text-xs text-stellar-purple hover:underline dark:text-purple-400"
>
<div className="flex items-center gap-2 text-sm text-gray-500 mb-1">
<Coins className="h-4 w-4" />
Token Balance
</div>
View Transactions <ExternalLink className="h-3 w-3" />
</Link>
</div>

<div className="flex items-baseline justify-between">
<div>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-gray-900">{balance}</span>
<span className="text-sm text-gray-500">
{primaryBalance?.tokenCode || "LEARN"}
</span>
<span className="text-2xl font-bold text-gray-900 dark:text-gray-100">{balanceStr}</span>
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400">{tokenCode}</span>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 font-medium mt-0.5">
≈ ${usdValue} USD
</div>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Your total {primaryBalance?.tokenCode || "LEARN"} balance</p>
</TooltipContent>
</Tooltip>

<div className="flex flex-col items-end gap-1 text-xs text-gray-400">
<button
type="button"
onClick={handleCopyContract}
className="flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
title="Copy contract address"
>
{copied ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
<span className="font-mono text-[10px]">
{contractAddress.length > 12
? `${contractAddress.slice(0, 4)}...${contractAddress.slice(-4)}`
: contractAddress}
</span>
</button>
</div>
</div>
</div>
);
}
Loading