From 5608357c54431c5dd11f746857b49f96c32f542f Mon Sep 17 00:00:00 2001 From: junman140 Date: Thu, 25 Jun 2026 10:39:18 +0100 Subject: [PATCH] fyder --- app/dashboard/page.tsx | 21 +- app/explore/page.tsx | 158 ++++++++ app/globals.css | 239 ++++------- app/history/page.tsx | 4 +- app/invest/[launchId]/page.tsx | 6 +- app/invest/create/page.tsx | 6 +- app/invest/page.tsx | 151 +------ app/invest/projects/page.tsx | 6 +- app/lending/page.tsx | 2 +- app/liquidity/page.tsx | 2 +- app/mint/page.tsx | 6 +- app/my-tokens/page.tsx | 2 +- app/page.tsx | 349 ++++++++-------- app/profile/page.tsx | 613 +++++++++++------------------ app/profile/rewards/page.tsx | 4 +- app/savings/page.tsx | 2 +- app/send/page.tsx | 6 +- app/swap/page.tsx | 74 +++- app/token/[code]/page.tsx | 2 +- app/trade/page.tsx | 2 +- app/trustlines/page.tsx | 2 +- components/navbar.tsx | 129 +++--- components/ui/button.tsx | 22 +- components/ui/card.tsx | 6 +- components/ui/empty-state.tsx | 40 ++ components/ui/page-back-header.tsx | 35 ++ hooks/use-paste-from-clipboard.ts | 49 +++ 27 files changed, 982 insertions(+), 956 deletions(-) create mode 100644 app/explore/page.tsx create mode 100644 components/ui/empty-state.tsx create mode 100644 components/ui/page-back-header.tsx create mode 100644 hooks/use-paste-from-clipboard.ts diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index f21d4e4..23e3f1e 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -141,9 +141,8 @@ export default function DashboardPage() { const showWalletCta = !publicKey return ( -
-
-
+
+
{stats.map((stat) => ( @@ -168,12 +167,12 @@ export default function DashboardPage() {
- - - + + + + diff --git a/app/explore/page.tsx b/app/explore/page.tsx new file mode 100644 index 0000000..e6475a5 --- /dev/null +++ b/app/explore/page.tsx @@ -0,0 +1,158 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { + Compass, + ArrowRightLeft, + PiggyBank, + CreditCard, + TrendingUp, + Coins, + Droplets, + Loader2, + Star, + ChevronRight, +} from "lucide-react" +import { useTokenRegistry } from "@/hooks/useTokenRegistry" + +export default function ExplorePage() { + const router = useRouter() + const { tokens, isLoading: tokensLoading, error: tokensError } = useTokenRegistry() + const [search, setSearch] = useState("") + + const filteredTokens = tokens.filter(token => + !search || token.assetCode.toLowerCase().includes(search.toLowerCase()) || token.name?.toLowerCase().includes(search.toLowerCase()) + ).slice(0, 12) + + const exploreCategories = [ + { + title: "Trade", + description: "Swap tokens instantly at the best rates", + icon: ArrowRightLeft, + href: "/swap", + color: "from-green-600 to-mint-500", + }, + { + title: "Savings", + description: "Earn yield with time-locked deposits", + icon: PiggyBank, + href: "/savings", + color: "from-blue-600 to-cyan-500", + }, + { + title: "Borrow & Lend", + description: "Supply assets or borrow against collateral", + icon: CreditCard, + href: "/lending", + color: "from-purple-600 to-pink-500", + }, + { + title: "Launchpad", + description: "Discover and participate in new launches", + icon: TrendingUp, + href: "/invest", + color: "from-amber-600 to-orange-500", + }, + { + title: "Liquidity", + description: "Provide liquidity and earn fees", + icon: Droplets, + href: "/liquidity", + color: "from-teal-600 to-cyan-500", + }, + ] + + return ( +
+
+ {/* Header */} +
+

+ + Explore +

+

Discover tokens, launchpads, and earning opportunities

+
+ + {/* Categories Grid */} +
+ {exploreCategories.map((cat) => ( + + + +
+ +
+

+ {cat.title} +

+

{cat.description}

+
+
+ + ))} +
+ + {/* Token Discovery */} +
+
+
+

Available Tokens

+

Discover tokens on the network

+
+
+ + {tokensLoading ? ( +
+ + Loading tokens... +
+ ) : tokensError ? ( + + +

Unable to load tokens right now.

+ +
+
+ ) : ( +
+ {filteredTokens.length === 0 && !tokensLoading ? ( +
+

No tokens found.

+
+ ) : ( + filteredTokens.map((token) => ( + + + +
+ +
+
+

+ {token.assetCode} +

+ {token.name && ( +

{token.name}

+ )} +
+ +
+
+ + )) + )} +
+ )} +
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index 41abf18..32c5c23 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,91 +4,69 @@ @custom-variant dark (&:is(.dark *)); :root { - --background: oklch(0.98 0.01 264); - --foreground: oklch(0.15 0.02 264); - --card: oklch(0.99 0.005 264); - --card-foreground: oklch(0.15 0.02 264); - --popover: oklch(0.99 0.005 264); - --popover-foreground: oklch(0.15 0.02 264); - --primary: hsl(142, 76%, 36%); - --primary-foreground: oklch(0.99 0.005 264); - --secondary: oklch(0.65 0.18 45); - --secondary-foreground: oklch(0.15 0.02 264); - --muted: oklch(0.95 0.01 264); - --muted-foreground: oklch(0.5 0.02 264); - --accent: hsl(142, 76%, 36%); - --accent-foreground: oklch(0.99 0.005 264); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.99 0.005 264); - --border: oklch(0.9 0.01 264); - --input: oklch(0.9 0.01 264); - --ring: hsl(142, 76%, 36%); - --chart-1: hsl(142, 76%, 36%); - --chart-2: oklch(0.65 0.18 45); - --chart-3: oklch(0.5 0.15 160); - --chart-4: oklch(0.6 0.2 320); - --chart-5: oklch(0.7 0.15 200); - --radius: 0.5rem; - --sidebar: oklch(0.99 0.005 264); - --sidebar-foreground: oklch(0.15 0.02 264); - --sidebar-primary: hsl(142, 76%, 36%); - --sidebar-primary-foreground: oklch(0.99 0.005 264); - --sidebar-accent: oklch(0.95 0.01 264); - --sidebar-accent-foreground: oklch(0.15 0.02 264); - --sidebar-border: oklch(0.9 0.01 264); - --sidebar-ring: hsl(142, 76%, 36%); - - /* Custom vibrant green variables for consistent usage */ - --green-primary: hsl(142, 76%, 36%); - --green-secondary: hsl(142, 76%, 42%); - --green-tertiary: hsl(142, 76%, 48%); - --green-light: hsl(142, 76%, 52%); - --green-dark: hsl(142, 76%, 30%); - --green-darker: hsl(142, 76%, 24%); - - /* Premium gradient classes for buttons */ - --gradient-primary: linear-gradient(to right, hsl(142, 76%, 36%), hsl(142, 76%, 42%)); - --gradient-primary-hover: linear-gradient(to right, hsl(142, 76%, 30%), hsl(142, 76%, 36%)); + --background: #fafbfc; + --foreground: #0f172a; + --card: #ffffff; + --card-foreground: #0f172a; + --popover: #ffffff; + --popover-foreground: #0f172a; + --primary: #10b981; + --primary-foreground: #ffffff; + --secondary: #64748b; + --secondary-foreground: #ffffff; + --muted: #f1f5f9; + --muted-foreground: #64748b; + --accent: #f1f5f9; + --accent-foreground: #0f172a; + --destructive: #ef4444; + --destructive-foreground: #ffffff; + --border: #e2e8f0; + --input: #f1f5f9; + --ring: #10b981; + --chart-1: #10b981; + --chart-2: #6366f1; + --chart-3: #f59e0b; + --chart-4: #0ea5e9; + --chart-5: #14b8a6; + --radius: 0.625rem; + --sidebar: #ffffff; + --sidebar-foreground: #0f172a; + --sidebar-primary: #10b981; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #f1f5f9; + --sidebar-accent-foreground: #0f172a; + --sidebar-border: #e2e8f0; + --sidebar-ring: #10b981; } -/* Premium Raydium-inspired dark theme with gradient backgrounds */ .dark { - --background: #0a0a0b; - --foreground: #e4e4e7; - --card: #111113; - --card-foreground: #e4e4e7; - --popover: #111113; - --popover-foreground: #e4e4e7; - --primary: hsl(142, 76%, 36%); - --primary-foreground: #0a0a0b; - --secondary: #1a1a1d; - --secondary-foreground: #e4e4e7; - --muted: #111113; - --muted-foreground: #71717a; - --accent: hsl(142, 76%, 36%); - --accent-foreground: #0a0a0b; + --background: #0f172a; + --foreground: #f8fafc; + --card: #1e293b; + --card-foreground: #f8fafc; + --popover: #1e293b; + --popover-foreground: #f8fafc; + --primary: #10b981; + --primary-foreground: #ffffff; + --secondary: #475569; + --secondary-foreground: #f8fafc; + --muted: #334155; + --muted-foreground: #94a3b8; + --accent: #1e293b; + --accent-foreground: #f8fafc; --destructive: #ef4444; - --destructive-foreground: #e4e4e7; - --border: #1a1a1d; - --input: #111113; - --ring: hsl(142, 76%, 36%); - --chart-1: hsl(142, 76%, 36%); - --chart-2: #60a5fa; - --chart-3: #a78bfa; - --chart-4: #f472b6; - --chart-5: #fbbf24; - --sidebar: #0a0a0b; - --sidebar-foreground: #e4e4e7; - --sidebar-primary: hsl(142, 76%, 36%); - --sidebar-primary-foreground: #0a0a0b; - --sidebar-accent: #111113; - --sidebar-accent-foreground: #e4e4e7; - --sidebar-border: #1a1a1d; - --sidebar-ring: hsl(142, 76%, 36%); - - /* Premium gradient classes for buttons (same in dark mode) */ - --gradient-primary: linear-gradient(to right, hsl(142, 76%, 36%), hsl(142, 76%, 42%)); - --gradient-primary-hover: linear-gradient(to right, hsl(142, 76%, 30%), hsl(142, 76%, 36%)); + --destructive-foreground: #f8fafc; + --border: #334155; + --input: #1e293b; + --ring: #10b981; + --sidebar: #0f172a; + --sidebar-foreground: #f8fafc; + --sidebar-primary: #10b981; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #1e293b; + --sidebar-accent-foreground: #f8fafc; + --sidebar-border: #334155; + --sidebar-ring: #10b981; } @theme inline { @@ -139,90 +117,45 @@ body { @apply bg-background text-foreground; } - - /* Premium gradient backgrounds inspired by Raydium */ - .dark body { - background: linear-gradient(135deg, - #0a0a0b 0%, - #0d0d0f 25%, - #0a0a0b 50%, - #0c0c0e 75%, - #0a0a0b 100% - ); - background-attachment: fixed; - } - - /* Enhanced gradient backgrounds for pages */ - .premium-gradient { - background: linear-gradient(135deg, - #ffffff 0%, - #f8fafc 25%, - #f1f5f9 50%, - #e2e8f0 75%, - #cbd5e1 100% - ); - } - - .dark .premium-gradient { - background: linear-gradient(135deg, - #0a0a0b 0%, - #0d0d0f 20%, - #111113 40%, - #0c0c0e 60%, - #0a0a0b 80%, - #080809 100% - ); - } - - /* Card gradient overlays */ - .card-gradient { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.05) 0%, - rgba(255, 255, 255, 0.02) 100% - ); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - } - - .dark .card-gradient { - background: linear-gradient(135deg, - rgba(0, 0, 0, 0.3) 0%, - rgba(0, 0, 0, 0.1) 100% - ); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + html { + @apply bg-background; } + + /* Typography standards */ + h1 { @apply text-4xl font-bold tracking-tight; } + h2 { @apply text-2xl font-bold; } + h3 { @apply text-xl font-semibold; } } -/* Custom glassmorphism effect */ -.glass { - background: rgba(255, 255, 255, 0.05); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); +/* Premium gradient page background */ +.premium-gradient { + background: linear-gradient(135deg, + #fafbfc 0%, + #f1f5f9 25%, + #f8fafc 50%, + #f1f5f9 75%, + #fafbfc 100% + ); } -.dark .glass { - background: rgba(0, 0, 0, 0.3); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); +.dark .premium-gradient { + background: linear-gradient(135deg, + #0f172a 0%, + #111827 50%, + #0f172a 100% + ); } -/* Premium gradient button classes */ +/* Primary action button - GREEN ONLY for primary CTAs */ .btn-gradient-primary { - background: var(--gradient-primary); + background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.25); } .btn-gradient-primary:hover { - background: var(--gradient-primary-hover); - transform: scale(1.02); - box-shadow: 0 10px 25px -5px rgba(34, 197, 94, 0.25); -} - -.btn-gradient-primary:active { - transform: scale(0.98); + background: linear-gradient(135deg, #059669 0%, #047857 100%); + box-shadow: 0 4px 16px rgba(16, 185, 129, 0.35); } diff --git a/app/history/page.tsx b/app/history/page.tsx index fb17a9b..9c8c560 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -72,7 +72,7 @@ export default function HistoryPage() { if (!publicKey) { return ( -
+
@@ -94,7 +94,7 @@ export default function HistoryPage() { } return ( -
+
diff --git a/app/invest/[launchId]/page.tsx b/app/invest/[launchId]/page.tsx index a131628..83c6139 100644 --- a/app/invest/[launchId]/page.tsx +++ b/app/invest/[launchId]/page.tsx @@ -182,7 +182,7 @@ export default function InvestLaunchDetailPage() { if (!launchId) { return ( -
+

Invalid launch

) @@ -190,7 +190,7 @@ export default function InvestLaunchDetailPage() { if (launchLoading || !launch) { return ( -
+
{launchError ? ( @@ -216,7 +216,7 @@ export default function InvestLaunchDetailPage() { launch.status === "allocation_running" return ( -
+
- - - ) -} - -export default function InvestPage() { - const [statusFilter, setStatusFilter] = useState(undefined) - const { launches, error, isLoading } = useLaunches({ limit: 50, status: statusFilter }) - - const statusOptions = useMemo( - () => [ - { value: undefined, label: "All" }, - ...Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label })), - ], - [] - ) - - return ( -
-
-
-
-

- - Invest -

-

- Launchpad launches and equity-style investments. Participate and earn. -

-
-
- {statusOptions.map((opt) => ( - - ))} - - -
-
- - {error && ( - - -

{error.message}

-
-
- )} - - {isLoading && ( -
- -
- )} - - {!isLoading && !error && ( - <> - {launches.length === 0 ? ( - - - No launches found. Check back later. - - - ) : ( -
- {launches.map((launch) => ( - - ))} -
- )} - - )}
) diff --git a/app/invest/projects/page.tsx b/app/invest/projects/page.tsx index 37a45ff..e0c0ff4 100644 --- a/app/invest/projects/page.tsx +++ b/app/invest/projects/page.tsx @@ -77,7 +77,7 @@ export default function MyProjectsPage() { if (profileLoading) { return ( -
+

Loading...

) @@ -85,7 +85,7 @@ export default function MyProjectsPage() { if (!profile) { return ( -
+
+

+ Connect your wallet in Profile to track holdings and execute trades +

+
+
+
+ ) } + // AUTHENTICATED: Full dashboard return ( -
-
- {/* Wallet Header - Compact Mobile Design */} -
- {/* Wallet Address */} -
-
- Wallet - {truncatedKey || "No wallet connected"} - {publicKey && ( - - )} +
+
+ {/* Balance Card */} +
+
+
+
+ +
+
+ Wallet + {truncatedKey || "Not connected"} +
+ {publicKey && ( + + )}
- {/* Balance Display - Centered, Pi Primary */} -
+
{balancesLoading || pricesLoading ? ( -
- +
+
) : ( <> -

- {totalBalance.toLocaleString(undefined, { maximumFractionDigits: 4 })} Pi +

+ {totalBalance.toLocaleString(undefined, { maximumFractionDigits: 4 })}

+

+ Pi Network +

{usdBalance !== null && piPrice && ( -

- ${usdBalance.toLocaleString(undefined, { maximumFractionDigits: 2 })} +

+ ≈ ${usdBalance.toLocaleString(undefined, { maximumFractionDigits: 2 })} USD

)} - {priceLoading && usdBalance === null && piPrice === null && ( -

Loading price...

- )} - {!priceLoading && piPrice === null && totalBalance > 0 && ( -

Price unavailable

- )} )} -
+
- {/* Action Buttons - Wallet Style Compact */} -
+ {/* Action Buttons - Swap FIRST */} +
- {/* Tokens List with Tabs */} - - + {/* Holdings List */} + + -
- - Tokens - History - - +
+
+

Your Holdings

+

Track all your assets

+
+
+ + +
- +
{balancesLoading ? ( -
- - Loading balances... +
+ + Loading holdings...
) : balances.length === 0 ? ( -
- {publicKey ? "No balances found" : "Connect a wallet to view your holdings"} +
+
+ +
+

No holdings yet

+

Ready for your first swap?

+
) : ( balances.map((balance, index) => { const isNative = balance.assetType === "native" - const displayName = isNative ? "Test Pi" : balance.assetCode + const displayName = isNative ? "Pi" : balance.assetCode const amount = Number(balance.amount) const priceInPi = getPrice(balance.assetCode, balance.assetIssuer || undefined, balance.assetType) const valueInPi = priceInPi !== null ? amount * priceInPi : null - const usdValue = piPrice && (isNative || valueInPi !== null) - ? (isNative ? amount : valueInPi!) * piPrice + const usdValue = piPrice && (isNative || valueInPi !== null) + ? (isNative ? amount : valueInPi!) * piPrice : null return ( @@ -308,43 +330,22 @@ export default function HomePage() {
- -
- {operationsLoading ? ( -
- - Loading activity... -
- ) : !publicKey ? ( -
- Connect a wallet to view transaction history -
- ) : ( - - )} -
+ + {operationsLoading ? ( +
+ + Loading activity... +
+ ) : ( + + )}
- - {!publicKey && ( - - - Connect Your Wallet - Connect a wallet to view your balance and holdings - - - - - - )}
-
) } diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 149197e..c92495f 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -3,29 +3,22 @@ import React, { useState, useEffect, useMemo } from "react" import Link from "next/link" import { - Plug, Settings, - FileText, - BookOpen, - ChevronRight, Copy, - MessageCircle, LogOut, Loader2, - Users, - Star, - Rocket, - LayoutDashboard, Coins, - Droplets, User, Shield, History, - Heart, + Droplets, + ArrowRightLeft, + Gift, Github, Send, Share2, - Gift, + ChevronRight, + Wallet, } from "lucide-react" import { usePi } from "@/components/providers/pi-provider" import { Button } from "@/components/ui/button" @@ -48,13 +41,13 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Checkbox } from "@/components/ui/checkbox" +import { usePiPrice } from "@/hooks/usePiPrice" const getStoredWallet = () => { if (typeof window === "undefined") return null return localStorage.getItem("zyradex-wallet-address") } - const ProfilePage: React.FC = () => { const { user, isAuthenticated, authenticate, signOut } = usePi() const { profile, isLoading: profileLoading, refresh: refreshProfile } = useUserProfile() @@ -68,43 +61,32 @@ const ProfilePage: React.FC = () => { const [successReplaceWarning, setSuccessReplaceWarning] = useState(null) const [showChangeWalletWarning, setShowChangeWalletWarning] = useState(false) const [changeWalletConfirm, setChangeWalletConfirm] = useState(false) + const { price: piPrice } = usePiPrice() useEffect(() => { - // Only set wallet address if user is authenticated if (!isAuthenticated) { setStoredWalletAddress(null) if (typeof window !== "undefined") { - // Clear localStorage when not authenticated localStorage.removeItem("zyradex-wallet-address") } return } - - // Priority 1: Use wallet from user profile (database) - this is the source of truth if (profile?.public_key && profile.public_key.trim() !== '') { setStoredWalletAddress(profile.public_key) if (typeof window !== "undefined") { - // Only store in localStorage if it matches the database localStorage.setItem("zyradex-wallet-address", profile.public_key) } return } - - // Priority 2: Check localStorage, but only if it matches what's in the database - // If user has no wallet in database, clear localStorage to avoid using stale data const storedWallet = getStoredWallet() if (storedWallet && profile?.public_key && storedWallet === profile.public_key) { - // Only use localStorage if it matches the database setStoredWalletAddress(storedWallet) } else { - // Clear localStorage if it doesn't match or user has no wallet setStoredWalletAddress(null) if (typeof window !== "undefined") { localStorage.removeItem("zyradex-wallet-address") } } - - // Priority 3: Fallback to user.wallet_address (from Pi SDK) if no database wallet if (!storedWalletAddress && !profile?.public_key && user?.wallet_address) { setStoredWalletAddress(user.wallet_address) } @@ -113,23 +95,16 @@ const ProfilePage: React.FC = () => { const { balances, totalBalance, isLoading: balancesLoading, refresh: refreshBalances } = useAccountBalances(storedWalletAddress ?? user?.wallet_address ?? undefined) const { refreshBalances: refreshBalancesGlobal } = useBalanceRefresh() ?? {} - const stats = useMemo( - () => [ - { label: "Assets", value: balances.length, icon: Users }, - { label: "Total Balance", value: totalBalance.toLocaleString(undefined, { maximumFractionDigits: 2 }), icon: Coins }, - { label: "Liquidity Pools", value: "–", icon: Droplets }, - ], - [balances.length, totalBalance] - ) + const usdBalance = useMemo(() => { + if (!piPrice || !totalBalance) return null + return totalBalance * piPrice + }, [piPrice, totalBalance]) const handleCopyWalletAddress = () => { const address = storedWalletAddress || user?.wallet_address if (address) { navigator.clipboard.writeText(address) - toast({ - title: "Copied!", - description: "Wallet address copied to clipboard", - }) + toast({ title: "Copied", description: "Wallet address copied to clipboard" }) } } @@ -146,11 +121,7 @@ const ProfilePage: React.FC = () => { await authenticate() } catch (error) { console.error("Pi authentication failed:", error) - toast({ - title: "Authentication Failed", - description: "Please try again or enter your wallet address manually", - variant: "destructive", - }) + toast({ title: "Authentication Failed", description: "Please try again.", variant: "destructive" }) } finally { setIsLoading(false) } @@ -158,30 +129,17 @@ const ProfilePage: React.FC = () => { const handleCreateWallet = async () => { if (!isAuthenticated) { - toast({ - title: "Authentication Required", - description: "Please authenticate first to create a wallet.", - variant: "destructive" - }) + toast({ title: "Connect wallet first", description: "Please authenticate with Pi to create a wallet.", variant: "destructive" }) return } - try { const response = await createWallet() - - // Store the secret seed to show to user setNewWalletSecret(response.secret) setSuccessReplaceWarning(response.replacedPreviousWallet ? (response.warning ?? null) : null) handleWalletPersist(response.publicKey) refreshProfile().catch(() => undefined) - - // Show dialog with wallet details setShowSecretDialog(true) - - toast({ - title: "Wallet Created!", - description: "Your new wallet has been created and seeded with test Pi. Please save your secret seed securely.", - }) + toast({ title: "Wallet Created", description: "Save your secret seed securely." }) } catch (err) { const message = err && typeof err === "object" && "message" in err ? (err as any).message : "Wallet creation failed" toast({ title: "Creation failed", description: message, variant: "destructive" }) @@ -206,28 +164,14 @@ const ProfilePage: React.FC = () => { refreshBalances() refreshBalancesGlobal?.() setShowSecretDialog(true) - toast({ - title: response.replacedPreviousWallet ? "Wallet replaced" : "Wallet created", - description: "Save your new secret seed securely.", - }) + toast({ title: "Wallet replaced", description: "Save your new secret seed securely." }) } catch (err: unknown) { const apiError = err && typeof err === "object" && "code" in err ? (err as { code?: string; message?: string }) : null - const code = apiError?.code - if (code === "NO_WALLET_TO_REPLACE") { + if (apiError?.code === "NO_WALLET_TO_REPLACE") { setShowChangeWalletWarning(false) setChangeWalletConfirm(false) refreshProfile().catch(() => undefined) - toast({ - title: "No wallet to replace", - description: "Use Create wallet below to set up your first wallet.", - variant: "destructive", - }) - } else if (code === "CONFIRM_REQUIRED") { - toast({ - title: "Confirmation required", - description: "Please confirm that you understand the warning before replacing your wallet.", - variant: "destructive", - }) + toast({ title: "No wallet to replace", description: "Use Create wallet to set up your first wallet.", variant: "destructive" }) } else { const message = apiError && "message" in apiError ? (apiError as { message: string }).message : "Failed to replace wallet" toast({ title: "Change wallet failed", description: message, variant: "destructive" }) @@ -238,399 +182,304 @@ const ProfilePage: React.FC = () => { const handleCopySecret = () => { if (newWalletSecret) { navigator.clipboard.writeText(newWalletSecret) - toast({ - title: "Copied!", - description: "Secret seed copied to clipboard. Keep it safe!", - }) + toast({ title: "Copied", description: "Secret seed copied. Keep it safe!" }) } } - const mainNavItems = [ - { - title: "My Assets", - description: "Manage your assets", - icon: Coins, - href: "/my-tokens", - showChevron: true, - }, - ...(profile?.role === "admin" - ? [ - { - title: "Launchpad (Mint)", - description: "Create new tokens (admin)", - icon: Rocket, - href: "/mint", - showChevron: true, - }, - ] - : []), - { - title: "Liquidity Pools", - description: "Manage LP Holdings", - icon: Droplets, - href: "/liquidity", - showChevron: true, - }, - { - title: "Swap Assets", - description: "Exchange assets", - icon: Users, - href: "/swap", - showChevron: true, - }, - { - title: "Transaction History", - description: "View your transaction history", - icon: History, - href: "/history", - showChevron: true, - }, - { - title: "Donate", - description: "Support ZYRADEX with Pi donations", - icon: Heart, - href: "/donate", - showChevron: true, - }, - { - title: "Rewards & Referral", - description: "Earn points and refer friends", - icon: Gift, - href: "/profile/rewards", - showChevron: true, - }, + const navItems = [ + { title: "Swap Assets", description: "Exchange tokens", icon: ArrowRightLeft, href: "/swap" }, + { title: "Transaction History", description: "View past transactions", icon: History, href: "/history" }, + { title: "Liquidity Pools", description: "Manage LP positions", icon: Droplets, href: "/liquidity" }, + { title: "Rewards & Referral", description: "Earn and refer friends", icon: Gift, href: "/profile/rewards" }, + ...(profile?.role === "admin" ? [{ title: "Launchpad (Admin)", description: "Create new tokens", icon: Shield, href: "/mint" }] : []), ] - // const additionalMenuItems = [ - // { - // title: "Terms of Service", - // description: "Read our terms and conditions", - // icon: FileText, - // href: "/terms", - // showChevron: true, - // }, - // { - // title: "Privacy Policy", - // description: "Learn about our privacy practices", - // icon: FileText, - // href: "/privacy", - // showChevron: true, - // }, - // // { - // // title: "API Documentation", - // // description: "Pi Network Oracle API docs", - // // icon: BookOpen, - // // href: "/api-docs", - // // showChevron: true, - // // }, - // { - // title: "Contact Us", - // description: "Get help and support", - // icon: MessageCircle, - // href: "/contact", - // showChevron: true, - // }, - // ] - const walletDisplay = storedWalletAddress || user?.wallet_address const truncatedWallet = walletDisplay ? `${walletDisplay.slice(0, 6)}...${walletDisplay.slice(-6)}` : null + // UNAUTHENTICATED + if (!isAuthenticated) { + return ( +
+
+
+

Profile

+

Connect your wallet to view your profile

+
+ + + +
+ +
+
+

Not connected

+

+ Connect your Pi wallet to view holdings, track assets, and manage your profile. +

+
+ +
+
+ + {/* Settings (available even without auth) */} + + + + + Settings + + + +

Theme: Use the toggle in the top navigation bar

+

Network: Pi Network (Testnet)

+
+
+
+
+ ) + } + + // AUTHENTICATED return ( -
-
+
+
+
+

Profile

+
- - - - - Wallet - - Your ZyraDex Profile - - + {/* Wallet Identity Card */} + +
- + {profile?.avatarUrl && !profileLoading && ( - + )} - - {(user?.username || "Guest").slice(0, 2).toUpperCase()} + + {(user?.username || "Z").slice(0, 2).toUpperCase()}
-
- {isLoading ? "Authenticating..." : user?.username ? `@${user.username}` : "Unauthenticated"} -
-
- Wallets tracked: {balancesLoading ? "..." : stats[0].value} +
+ {user?.username ? `@${user.username}` : "Connected"}
{walletDisplay && ( -
- +
+ {truncatedWallet} -
)}
- -
- {stats.map((stat) => ( -
-

{stat.label}

-

{balancesLoading ? "..." : stat.value}

-
- ))} + + + +
+
+

Assets

+

{balances.length}

+
+
+

Total Balance

+

+ {totalBalance.toLocaleString(undefined, { maximumFractionDigits: 2 })} +

+
+ {usdBalance !== null && ( +

+ ≈ ${usdBalance.toLocaleString(undefined, { maximumFractionDigits: 2 })} USD +

+ )}
- {!storedWalletAddress && isAuthenticated && ( - - - - - Create New Wallet - - Generate a new wallet and receive test Pi - - - {createWalletError && ( - - - {createWalletError.message} - - - )} - - - A new wallet will be created and seeded with test Pi. You will receive your secret seed - - save it securely as you'll need it for all transactions. We don't store your secret seed. - - - - - - )} - - + {/* Holdings Summary */} + - - + + Holdings - Your holdings on the ZyraDex + Your assets on ZyraDex - -
-

Assets Tracked

-

{balancesLoading ? "Loading..." : balances.length ? `${balances.length} assets` : "None yet"}

-
-
-

Total Value

-

{balancesLoading ? "Loading..." : totalBalance.toLocaleString(undefined, { maximumFractionDigits: 2 })}

-
+ + {balancesLoading ? ( +
+ +
+ ) : balances.length === 0 ? ( +
+

No assets tracked yet

+ +
+ ) : ( +
+ {balances.map((b, i) => ( +
+ {b.assetType === "native" ? "Pi" : b.assetCode} + {Number(b.amount).toLocaleString(undefined, { maximumFractionDigits: 6 })} +
+ ))} +
+ )}
+ {/* Navigation Links */}
-

Navigation

- {mainNavItems.map((item) => ( +

Quick Actions

+ {navItems.map((item) => ( -
+
-
- +
+
-
{item.title}
-
{item.description}
+
{item.title}
+
{item.description}
- {item.showChevron && } +
))} -
-
-

Connect with us

+ {/* Create Wallet (if no wallet yet) */} + {!storedWalletAddress && ( + + + + + Create Wallet + + Generate a new wallet and receive test Pi + + + {createWalletError && ( + + {createWalletError.message} + + )} + + + A new wallet will be created and seeded with test Pi. Save your secret seed securely — we don't store it. + + + + + + )} + + {/* Logout */} + + + {/* Social Links */} + -
- { - setShowSecretDialog(open) - if (!open) { - setNewWalletSecret(null) - setSuccessReplaceWarning(null) - } - }}> + {/* Secret Seed Dialog */} + { setShowSecretDialog(open); if (!open) { setNewWalletSecret(null); setSuccessReplaceWarning(null) } }}> Save Your Secret Seed - - Your wallet has been created! Please save your secret seed securely. You'll need it for all transactions. - + Your wallet has been created. Save your secret seed securely — you'll need it for all transactions.
- {successReplaceWarning && ( - - - {successReplaceWarning} - - - )} - - - IMPORTANT: Save this secret seed in a secure location. We don't store it, - and you cannot recover it if lost. You'll need to provide it for every transaction. - - + {successReplaceWarning && {successReplaceWarning}} + IMPORTANT: We don't store your secret seed. Save it in a secure location.
- -
- {storedWalletAddress} - + +
+ {storedWalletAddress} +
- -
- {newWalletSecret} - + +
+ {newWalletSecret} +
- +
- { - setShowChangeWalletWarning(open) - if (!open) setChangeWalletConfirm(false) - }}> - + {/* Change Wallet Warning */} + { setShowChangeWalletWarning(open); if (!open) setChangeWalletConfirm(false) }}> + Changing your wallet - - Your current wallet will be replaced. The old wallet address will no longer be linked to this account. - + Your current wallet will be replaced. Funds in the old wallet are not automatically moved.
- - -
    -
  • Any funds in the old wallet are not automatically moved to the new one.
  • -
  • You will need to save the new secret key securely; the old one will no longer be used for this account.
  • -
-

Are you sure you want to replace your wallet?

-
-
- {changeWalletError && ( - - {changeWalletError.message} - - )} -
- setChangeWalletConfirm(checked === true)} - /> - + Funds in the old wallet are not automatically moved. Save the new secret key securely. + {changeWalletError && {changeWalletError.message}} +
+ setChangeWalletConfirm(c === true)} /> +
- - +
diff --git a/app/profile/rewards/page.tsx b/app/profile/rewards/page.tsx index fcb772f..4fb0b3b 100644 --- a/app/profile/rewards/page.tsx +++ b/app/profile/rewards/page.tsx @@ -85,7 +85,7 @@ export default function RewardsPage() { if (!isAuthenticated) { return ( -
+
@@ -104,7 +104,7 @@ export default function RewardsPage() { } return ( -
+
+
+ + {showMobileChart && ( +
+ +
+ )} + +
+

Recent Transactions

+
+ +
+
) -} \ No newline at end of file +} diff --git a/app/token/[code]/page.tsx b/app/token/[code]/page.tsx index 9bca256..b87ae76 100644 --- a/app/token/[code]/page.tsx +++ b/app/token/[code]/page.tsx @@ -139,7 +139,7 @@ export default function TokenDetailPage({ params }: { params: { code: string } } const displayName = isNative ? "Test Pi" : tokenCode return ( -
+
{/* Back Button */} + {!mounted ? ( - ) : isAuthenticated ? (
- -
) : ( - )}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx index f64632d..b22ce14 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -5,28 +5,28 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', + default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm', destructive: 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground shadow-sm', secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm', ghost: - 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', - lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', - icon: 'size-9', - 'icon-sm': 'size-8', - 'icon-lg': 'size-10', + default: 'h-10 px-4 py-2 has-[>svg]:px-3', + sm: 'h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-xs', + lg: 'h-11 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-10', + 'icon-sm': 'size-9', + 'icon-lg': 'size-11', }, }, defaultVariants: { diff --git a/components/ui/card.tsx b/components/ui/card.tsx index c5718b4..7490d68 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
) {
) { return (
) diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx new file mode 100644 index 0000000..f5a3e8d --- /dev/null +++ b/components/ui/empty-state.tsx @@ -0,0 +1,40 @@ +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import { type LucideIcon } from "lucide-react" + +interface EmptyStateProps { + icon: LucideIcon + title: string + description: string + actionLabel?: string + actionHref?: string + onAction?: () => void + className?: string +} + +export function EmptyState({ + icon: Icon, + title, + description, + actionLabel, + actionHref, + onAction, + className, +}: EmptyStateProps) { + return ( +
+
+ +
+

{title}

+

{description}

+ {actionLabel && (actionHref ? ( + + ) : onAction ? ( + + ) : null)} +
+ ) +} diff --git a/components/ui/page-back-header.tsx b/components/ui/page-back-header.tsx new file mode 100644 index 0000000..88f0d50 --- /dev/null +++ b/components/ui/page-back-header.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { ArrowLeft } from "lucide-react" + +interface PageBackHeaderProps { + title?: string + /** Optional right-side content (e.g. actions) */ + children?: React.ReactNode + className?: string +} + +export function PageBackHeader({ title, children, className = "" }: PageBackHeaderProps) { + const router = useRouter() + return ( +
+
+ + {title && ( +

{title}

+ )} +
+ {children} +
+ ) +} diff --git a/hooks/use-paste-from-clipboard.ts b/hooks/use-paste-from-clipboard.ts new file mode 100644 index 0000000..c92b438 --- /dev/null +++ b/hooks/use-paste-from-clipboard.ts @@ -0,0 +1,49 @@ +"use client" + +import { useCallback } from "react" +import { useToast } from "@/hooks/use-toast" + +interface UsePasteFromClipboardOptions { + /** Called with trimmed clipboard text on success */ + onPaste: (text: string) => void + /** Optional success message (default: "Pasted from clipboard") */ + successTitle?: string + /** Optional label for the pasted content (e.g. "Address") for success description */ + successLabel?: string +} + +export function usePasteFromClipboard({ + onPaste, + successTitle = "Pasted", + successLabel = "Content", +}: UsePasteFromClipboardOptions) { + const { toast } = useToast() + + const handlePaste = useCallback(async () => { + try { + const text = await navigator.clipboard.readText() + const trimmed = text?.trim() + if (trimmed) { + onPaste(trimmed) + toast({ + title: successTitle, + description: successLabel ? `${successLabel} pasted from clipboard.` : "Pasted from clipboard.", + }) + } else { + toast({ + title: "Clipboard empty", + description: "No text found in clipboard.", + variant: "destructive", + }) + } + } catch { + toast({ + title: "Paste failed", + description: "Could not read clipboard. Check permissions.", + variant: "destructive", + }) + } + }, [onPaste, successTitle, successLabel, toast]) + + return { handlePaste } +}