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
1 change: 0 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { QueryProvider } from "@/providers/query-provider";
// replaced with resizable global navbar
import GlobalResizableNavbar from "@/components/ui/global-resizable-navbar";
import { ThemeProvider } from "@/components/theme-provider";
import { GlobalNavbar } from "@/components/global-navbar";

Expand Down
14 changes: 14 additions & 0 deletions app/wallet/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Wallet | Bounties",
description: "Manage your abstracted wallet, view assets, track earnings, and off-ramp funds directly to your bank account.",
};

export default function WalletLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
68 changes: 68 additions & 0 deletions app/wallet/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { WalletOverview } from "@/components/wallet/wallet-overview";
import { BalanceCard } from "@/components/wallet/balance-card";
import { AssetsList } from "@/components/wallet/assets-list";
import { TransactionHistory } from "@/components/wallet/transaction-history";
import { WithdrawalSection } from "@/components/wallet/withdrawal-section";
import { SecuritySection } from "@/components/wallet/security-section";
import { mockWalletWithAssets } from "@/lib/mock-wallet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

export default function WalletPage() {
return (
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Wallet</h1>
<p className="text-muted-foreground">
Manage your earnings, assets, and withdrawals.
</p>
</div>

<div className="grid gap-8 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-8">
<BalanceCard walletInfo={mockWalletWithAssets} />

<Tabs defaultValue="assets" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="assets">Assets</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="withdraw">Withdraw</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>

<TabsContent value="assets" className="space-y-4">
<AssetsList assets={mockWalletWithAssets.assets} />
</TabsContent>

<TabsContent value="activity" className="space-y-4">
<TransactionHistory activity={mockWalletWithAssets.recentActivity} />
</TabsContent>

<TabsContent value="withdraw" className="space-y-4">
<WithdrawalSection walletInfo={mockWalletWithAssets} />
</TabsContent>

<TabsContent value="security" className="space-y-4">
<SecuritySection walletInfo={mockWalletWithAssets} />
</TabsContent>
</Tabs>
</div>

<div className="space-y-8">
<WalletOverview walletInfo={mockWalletWithAssets} />

<div className="rounded-xl border border-border bg-card p-6">
<h3 className="font-semibold mb-4">Quick Links</h3>
<div className="grid gap-3">
{/* TODO: Implement these routes */}
<span className="text-sm text-muted-foreground cursor-not-allowed">How it works?</span>
<span className="text-sm text-muted-foreground cursor-not-allowed">Fee Schedule</span>
<span className="text-sm text-muted-foreground cursor-not-allowed">Support Center</span>
</div>
</div>
</div>
</div>
</div>
);
}
17 changes: 8 additions & 9 deletions components/animate-ui/primitives/effects/theme-toggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ type Direction = 'btt' | 'ttb' | 'ltr' | 'rtl';
type ChildrenRender =
| React.ReactNode
| ((state: {
resolved: Resolved;
effective: ThemeSelection;
toggleTheme: (theme: ThemeSelection) => void;
}) => React.ReactNode);
resolved: Resolved;
effective: ThemeSelection;
toggleTheme: (theme: ThemeSelection) => void;
}) => React.ReactNode);

function getSystemEffective(): Resolved {
if (typeof window === 'undefined') return 'light';
Expand Down Expand Up @@ -53,7 +53,6 @@ function ThemeToggler({
onImmediateChange,
direction = 'ltr',
children,
...props
}: ThemeTogglerProps) {
const [preview, setPreview] = React.useState<null | {
effective: ThemeSelection;
Expand Down Expand Up @@ -129,10 +128,10 @@ function ThemeToggler({
<React.Fragment>
{typeof children === 'function'
? children({
effective: current.effective,
resolved: current.resolved,
toggleTheme,
})
effective: current.effective,
resolved: current.resolved,
toggleTheme,
})
: children}
<style>{`::view-transition-old(root), ::view-transition-new(root){animation:none;mix-blend-mode:normal;}`}</style>
</React.Fragment>
Expand Down
24 changes: 15 additions & 9 deletions components/global-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,40 @@ export function GlobalNavbar() {
<div className="hidden md:flex items-center gap-6 text-sm font-medium">
<Link
href="/bounty"
className={`transition-colors hover:text-foreground/80 ${
pathname.startsWith("/bounty")
className={`transition-colors hover:text-foreground/80 ${pathname.startsWith("/bounty")
? "text-foreground"
: "text-foreground/60"
}`}
}`}
>
Explore
</Link>
<Link
href="/projects"
className={`transition-colors hover:text-foreground/80 ${
pathname.startsWith("/projects")
className={`transition-colors hover:text-foreground/80 ${pathname.startsWith("/projects")
? "text-foreground"
: "text-foreground/60"
}`}
}`}
>
Projects
</Link>
<Link
href="/leaderboard"
className={`transition-colors hover:text-foreground/80 ${
pathname.startsWith("/leaderboard")
className={`transition-colors hover:text-foreground/80 ${pathname.startsWith("/leaderboard")
? "text-foreground"
: "text-foreground/60"
}`}
}`}
>
Leaderboard
</Link>
<Link
href="/wallet"
className={`transition-colors hover:text-foreground/80 ${pathname.startsWith("/wallet")
? "text-foreground"
: "text-foreground/60"
}`}
>
Wallet
</Link>
</div>
</div>

Expand Down
16 changes: 8 additions & 8 deletions components/ui/resizable-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "motion/react";

import React, { useRef, useState } from "react";
import Image from "next/image";

interface NavbarProps {
children: React.ReactNode;
Expand Down Expand Up @@ -73,9 +74,9 @@ export const Navbar = ({ children, className }: NavbarProps) => {
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(
child as React.ReactElement<{ visible?: boolean }>,
{ visible },
)
child as React.ReactElement<{ visible?: boolean }>,
{ visible },
)
: child,
)}
</motion.div>
Expand Down Expand Up @@ -194,7 +195,6 @@ export const MobileNavMenu = ({
children,
className,
isOpen,
onClose,
}: MobileNavMenuProps) => {
return (
<AnimatePresence>
Expand Down Expand Up @@ -240,7 +240,7 @@ export const NavbarLogo = () => {
href="#"
className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-black"
>
<img src="/logo.png" alt="Bounties logo" width={30} height={30} />
<Image src="/logo.png" alt="Bounties logo" width={30} height={30} />
<span className="font-medium text-black dark:text-white">Bounties</span>
</a>
);
Expand All @@ -260,9 +260,9 @@ export const NavbarButton = ({
className?: string;
variant?: "primary" | "secondary" | "dark" | "gradient";
} & (
| React.ComponentPropsWithoutRef<"a">
| React.ComponentPropsWithoutRef<"button">
)) => {
| React.ComponentPropsWithoutRef<"a">
| React.ComponentPropsWithoutRef<"button">
)) => {
const baseStyles =
"px-4 py-2 rounded-md bg-white button bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";

Expand Down
156 changes: 156 additions & 0 deletions components/wallet/assets-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"use client";

import { useState } from "react";
import { WalletAsset } from "@/types/wallet";
import { Input } from "@/components/ui/input";
import { Search, ArrowUpDown, Filter } from "lucide-react";
import { Button } from "@/components/ui/button";

interface AssetsListProps {
assets: WalletAsset[];
}

export function AssetsList({ assets }: AssetsListProps) {
const [search, setSearch] = useState("");
const [hideSmallBalances, setHideSmallBalances] = useState(false);
const [sortConfig, setSortConfig] = useState<{
key: 'tokenSymbol' | 'amount' | 'usdValue';
direction: 'asc' | 'desc';
}>({ key: 'usdValue', direction: 'desc' });

const handleSort = (key: 'tokenSymbol' | 'amount' | 'usdValue') => {
setSortConfig(prev => ({
key,
direction: prev.key === key && prev.direction === 'desc' ? 'asc' : 'desc'
}));
};

const filteredAndSortedAssets = [...assets]
.filter(asset => {
const matchesSearch = asset.tokenName.toLowerCase().includes(search.toLowerCase()) ||
asset.tokenSymbol.toLowerCase().includes(search.toLowerCase());
const passesFilter = !hideSmallBalances || asset.usdValue >= 1;
return matchesSearch && passesFilter;
})
.sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];

if (typeof aValue === 'string' && typeof bValue === 'string') {
return sortConfig.direction === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
}

if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortConfig.direction === 'asc'
? aValue - bValue
: bValue - aValue;
}

return 0;
});

const totalUsd = assets.reduce((acc, c) => acc + c.usdValue, 0);

const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};

return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div className="relative w-full sm:max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search assets..."
className="pl-9 bg-muted/50"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<Button
variant="outline"
size="sm"
className="flex-1 sm:flex-none"
onClick={() => handleSort('tokenSymbol')}
title="Sort by Symbol"
>
<ArrowUpDown className="mr-2 h-4 w-4" />
Sort Symbol
</Button>
<Button
variant={hideSmallBalances ? "default" : "outline"}
size="sm"
className="flex-1 sm:flex-none"
onClick={() => setHideSmallBalances(!hideSmallBalances)}
title={hideSmallBalances ? "Showing only >$1" : "Filter small balances"}
>
<Filter className="mr-2 h-4 w-4" />
{hideSmallBalances ? "Filtering" : "Filter"}
</Button>
</div>
</div>

<div className="rounded-xl border border-border overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-muted/50 border-b border-border">
<tr>
<th className="text-left py-3 px-4 font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Asset</th>
<th className="text-right py-3 px-4 font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Balance</th>
<th className="text-right py-3 px-4 font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Price</th>
<th className="text-right py-3 px-4 font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Value</th>
<th className="text-right py-3 px-4 font-medium text-muted-foreground uppercase tracking-wider text-[10px] hidden md:table-cell">Portfolio %</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{filteredAndSortedAssets.length === 0 ? (
<tr>
<td colSpan={5} className="py-12 text-center text-muted-foreground">
No assets found matching your search.
</td>
</tr>
) : (
filteredAndSortedAssets.map((asset) => (
<tr key={asset.id} className="hover:bg-muted/30 transition-colors cursor-pointer group">
<td className="py-4 px-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-xs font-bold text-primary-foreground dark:text-primary">
{asset.tokenSymbol}
</div>
<div>
<div className="font-semibold">{asset.tokenSymbol}</div>
<div className="text-xs text-muted-foreground">{asset.tokenName}</div>
</div>
</div>
</td>
<td className="py-4 px-4 text-right">
<div className="font-medium">{asset.amount.toLocaleString()}</div>
<div className="text-xs text-muted-foreground">{asset.tokenSymbol}</div>
</td>
<td className="py-4 px-4 text-right">
{formatCurrency(asset.amount ? asset.usdValue / asset.amount : 0)}
</td>
<td className="py-4 px-4 text-right">
<div className="font-medium">{formatCurrency(asset.usdValue)}</div>
</td>
<td className="py-4 px-4 text-right hidden md:table-cell">
<div className="text-xs font-medium">
{(totalUsd ? (asset.usdValue / totalUsd) * 100 : 0).toFixed(1)}%
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
Loading