Merge pull request #3 from nexa-sol/ui-improv-and-landing-page

UI improvements and landing page
This commit is contained in:
2026-02-13 01:45:27 +01:00
committed by GitHub
10 changed files with 467 additions and 57 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { TooltipProvider } from "@/components/ui/tooltip";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -27,7 +28,7 @@ export default function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
{children} <TooltipProvider>{children}</TooltipProvider>
</body> </body>
</html> </html>
); );
+14 -5
View File
@@ -3,8 +3,15 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Settings2Icon, ZapIcon } from "lucide-react"; import { Settings2Icon, ZapIcon } from "lucide-react";
import TokenCard from "./TokenCard"; import TokenCard from "./TokenCard";
import { Token } from "@/modules/tokens/types";
export default function PairsCard({ title }: { title: string }) { export default function PairsCard({
title,
tokens,
}: {
title: string;
tokens: Token[];
}) {
return ( return (
<Card className="py-3 gap-0!"> <Card className="py-3 gap-0!">
<CardHeader className="px-4 pb-3! border-b flex justify-between items-center"> <CardHeader className="px-4 pb-3! border-b flex justify-between items-center">
@@ -39,10 +46,12 @@ export default function PairsCard({ title }: { title: string }) {
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-4 py-4 border-b border-border hover:bg-muted"> <CardContent className="p-0">
<TokenCard {tokens.map((token) => (
token={{ symbol: "SYM", name: "My token", mint: "myMint" }} <div key={token.mint} className="px-4 py-4 border-b border-border hover:bg-muted">
/> <TokenCard token={token} />
</div>
))}
</CardContent> </CardContent>
</Card> </Card>
); );
+153 -44
View File
@@ -1,5 +1,7 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import MinimalTooltip from "@/components/utils/MinimalTooltip";
import { humanizeNumber } from "@/lib/renderUtils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Token, TokenLiveData } from "@/modules/tokens/types"; import { Token, TokenLiveData } from "@/modules/tokens/types";
import { import {
@@ -10,12 +12,14 @@ import {
CrosshairIcon, CrosshairIcon,
EyeIcon, EyeIcon,
GhostIcon, GhostIcon,
IconNode,
LucideIcon, LucideIcon,
UserIcon,
UsersIcon, UsersIcon,
UserStarIcon, UserStarIcon,
ZapIcon, ZapIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
export default function TokenCard({ token }: { token: Token }) { export default function TokenCard({ token }: { token: Token }) {
const live: TokenLiveData = { const live: TokenLiveData = {
@@ -26,53 +30,158 @@ export default function TokenCard({ token }: { token: Token }) {
bundleHold: 3, bundleHold: 3,
}; };
const router = useRouter();
return ( return (
<div className="flex gap-4"> <div
<div className="h-full overflow-hidden min-w-0"> className="
{token.logoURI ? ( flex flex-col gap-3 cursor-pointer
<img md:flex-row md:gap-4
src={token.logoURI} "
alt="Token" onClick={(e) => {
className="w-16 h-16 rounded-lg" if (e.ctrlKey || e.metaKey) {
/> window.open(`/token/${token.mint}`, "_blank", "noopener,noreferrer");
) : ( } else {
<div className="w-16 h-16 bg-primary/20 rounded-lg flex items-center justify-center"> router.push(`/token/${token.mint}`);
<CoinsIcon className="text-primary" /> }
}}
onContextMenu={() =>
window.open(`/token/${token.mint}`, "_blank", "noopener,noreferrer")
}
>
{/* Left: logo */}
<div className="flex gap-3 md:gap-4 min-w-0">
<div className="h-full overflow-hidden">
{token.logoURI ? (
<img
src={token.logoURI}
alt="Token"
className="w-14 h-14 md:w-16 md:h-16 rounded-lg"
/>
) : (
<div className="w-14 h-14 md:w-16 md:h-16 bg-primary/20 rounded-lg flex items-center justify-center">
<CoinsIcon className="text-primary" />
</div>
)}
</div>
{/* Main content */}
<div className="flex flex-col gap-1 min-w-0">
{/* Symbol + name */}
<div className="flex gap-1.5 items-center flex-wrap">
<p className="font-semibold">{token.symbol}</p>
<p
className="text-muted-foreground flex gap-1 items-center cursor-pointer hover:text-accent truncate"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(token.mint);
}}
>
{token.name}
<CopyIcon className="w-4 h-4 shrink-0" />
</p>
</div>
{/* Stats */}
<div className="items-center flex gap-3 flex-wrap text-xs md:text-sm">
<p className="text-green-500">20m</p>
<MinimalTooltip label="Holders">
<p className="flex gap-1 items-center">
<UsersIcon className="w-3.5 h-3.5 text-muted-foreground" />
10
</p>
</MinimalTooltip>
<MinimalTooltip label="Live NEXA viewers">
<p className="flex gap-1 items-center">
<EyeIcon className="w-3.5 h-3.5" />
10
</p>
</MinimalTooltip>
</div>
{/* Twitter */}
<div className="flex gap-2 items-center text-xs text-[#5DBCFF] flex-wrap">
{token.twitterPost ? (
<>
<MinimalTooltip label="Twitter author">
<Link
className="hover:underline"
target="_blank"
href={`https://x.com/${token.twitterPost.author?.username}`}
onClick={(e) => e.stopPropagation()}
>
by @{token.twitterPost.author?.username}
</Link>
</MinimalTooltip>
<MinimalTooltip label="Followers">
<p className="flex gap-0.5 items-center">
<UserIcon className="w-3 h-3" />
{humanizeNumber(token.twitterPost.author?.followers)}
</p>
</MinimalTooltip>
</>
) : (
<span className="invisible">by @username</span>
)}
</div>
{/* Badges */}
<div className="flex gap-1.5 items-center flex-wrap">
<MinimalTooltip independent bottom label="Top 10 Holders">
<TokenBadge value={live.topHolder} icon={UserStarIcon} />
</MinimalTooltip>
<MinimalTooltip independent bottom label="Dev Holdings">
<TokenBadge value={live.devHold} icon={ChefHatIcon} />
</MinimalTooltip>
<MinimalTooltip independent bottom label="Snipers">
<TokenBadge value={live.snipersHold} icon={CrosshairIcon} />
</MinimalTooltip>
<MinimalTooltip independent bottom label="Insiders">
<TokenBadge value={live.insidersHold} icon={GhostIcon} />
</MinimalTooltip>
<MinimalTooltip independent bottom label="Bundlers">
<TokenBadge value={live.bundleHold} icon={BoxesIcon} />
</MinimalTooltip>
</div> </div>
)}
<p className="mt-1 truncate text-xs text-muted-foreground"></p>
</div>
<div className="flex flex-col">
<div className="flex gap-1.5">
<p className="font-semibold">{token.symbol}</p>
<p className="text-muted-foreground flex gap-1 items-center h-max cursor-pointer hover:text-accent">
{token.name} <CopyIcon className="w-4 h-4" />
</p>
</div>
<div className="items-center flex gap-3">
<p className="text-green-500 text-sm">20m</p>
<p className="text-sm flex gap-1 items-center">
<UsersIcon className="w-3.5 h-3.5 text-muted-foreground" />
10
</p>
<p className="text-sm flex gap-1 items-center">
<EyeIcon className="w-3.5 h-3.5" />
10
</p>
</div>
<div className="items-center flex gap-2">
<TokenBadge value={live.topHolder} icon={UserStarIcon} />
<TokenBadge value={live.devHold} icon={ChefHatIcon} />
<TokenBadge value={live.snipersHold} icon={CrosshairIcon} />
<TokenBadge value={live.insidersHold} icon={GhostIcon} />
<TokenBadge value={live.bundleHold} icon={BoxesIcon} />
</div> </div>
</div> </div>
<div className="ml-auto flex flex-col">
<TokenDigit name="MC" value="$15.6k" /> {/* Right column */}
<TokenDigit name="V" value="$42k" /> <div
<Button className="mt-auto ml-auto p-1! h-max w-max rounded-full bg-primary/20 text-primary hover:text-black gap-1 text-xs"> className="
<ZapIcon />1 SOL flex flex-row justify-between items-center gap-3
md:ml-auto md:flex-col md:items-end
"
>
<div className="flex gap-4 md:flex-col md:gap-1">
<MinimalTooltip label="Market Cap">
<TokenDigit name="MC" value="$15.6k" />
</MinimalTooltip>
<MinimalTooltip label="Volume" bottom>
<TokenDigit name="V" value="$42k" />
</MinimalTooltip>
</div>
<Button
className="
ml-auto rounded-full bg-primary/20 text-primary gap-1 text-xs
py-1.5! h-max md:rounded-full hover:text-accent-foreground
"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<ZapIcon className="w-4 h-4" />1 SOL
</Button> </Button>
</div> </div>
</div> </div>
+52 -4
View File
@@ -9,10 +9,58 @@ export default function Markets() {
<main className="h-svh flex flex-col"> <main className="h-svh flex flex-col">
<Navbar /> <Navbar />
<section className="w-full h-full pt-10 pb-5 px-5"> <section className="w-full h-full pt-10 pb-5 px-5">
<div className="w-full h-full grid grid-cols-3 gap-5"> <div className="w-full h-full grid grid-cols-1 md:grid-cols-3 gap-5">
<PairsCard title="New Pairs" /> <PairsCard
<PairsCard title="Final Stretch" /> title="New Pairs"
<PairsCard title="Migrated" /> tokens={[
{
symbol: "SYM",
name: "My token",
mint: "myMint",
twitterPost: {
link: "https://x.com/selimajdev/status/2013291898836758968",
author: {
username: "selimajdev",
followers: 1250000,
},
},
},
]}
/>
<PairsCard
title="Final Stretch"
tokens={[
{
symbol: "TEST",
name: "A test token",
mint: "myMint2",
twitterPost: {
link: "https://x.com/selimajdev/status/2013291898836758968",
author: {
username: "selimajdev",
followers: 1250000,
},
},
},
]}
/>
<PairsCard
title="Migrated"
tokens={[
{
symbol: "MIG",
name: "Are we migrated yet?",
mint: "migratedMint",
twitterPost: {
link: "https://x.com/selimajdev/status/2013291898836758968",
author: {
username: "selimajdev",
followers: 1250000,
},
},
},
]}
/>
</div> </div>
</section> </section>
<Statusbar /> <Statusbar />
+145 -2
View File
@@ -1,3 +1,146 @@
export default function Home() { import Link from "next/link";
return <div>hello world</div>;
export default function Landing() {
return (
<main className="min-h-screen bg-background text-foreground overflow-hidden">
{/* Background gradients */}
<div className="pointer-events-none fixed inset-0">
<div className="absolute -top-40 -left-40 h-150 w-150 rounded-full bg-primary/20 blur-[120px]" />
<div className="absolute top-1/3 -right-40 h-125 w-125 rounded-full bg-accent/20 blur-[120px]" />
</div>
{/* Hero */}
<section className="relative z-10 px-6 pt-32 pb-24 max-w-7xl mx-auto">
<div className="max-w-3xl">
<h1 className="text-5xl md:text-6xl font-semibold leading-tight">
Trade memecoins <br />
<span className="text-transparent bg-clip-text bg-linear-to-r from-primary to-accent">
without trading blind
</span>
</h1>
<p className="mt-6 text-lg text-muted-foreground max-w-xl">
Nexa filters noise, intercepts high-rug setups, and executes faster,
so you spend less time guessing and more time surviving.
</p>
<div className="mt-10 flex gap-4">
<Link href="/markets">
<button className="px-6 py-3 rounded-lg bg-primary text-primary-foreground font-medium hover:opacity-90 transition">
Open Terminal
</button>
</Link>
<Link
href="https://github.com/nexa-sol/nexa-trade?tab=readme-ov-file#-nexa-roadmap--alpha-release"
target="_blank"
>
<button className="px-6 py-3 rounded-lg border border-border hover:bg-accent/10 transition">
View Roadmap
</button>
</Link>
</div>
</div>
</section>
{/* Live signal strip */}
{/* <section className="relative z-10 border-y border-border/60 bg-card/40 backdrop-blur">
<div className="max-w-7xl mx-auto px-6 py-4 flex gap-8 text-sm text-muted-foreground overflow-x-auto">
<span>🟢 12 rugs intercepted today</span>
<span>⚡ Median execution: 420ms</span>
<span>📉 Top 10 holder risk flagged</span>
<span>🔍 Bundler detected pre-launch</span>
</div>
</section> */}
{/* Why Nexa */}
<section className="relative z-10 px-6 py-24 max-w-7xl mx-auto">
<div className="grid md:grid-cols-3 gap-10">
<div>
<h3 className="text-xl font-medium mb-2">Risk-aware by default</h3>
<p className="text-muted-foreground">
Every trade is evaluated against holder concentration, bundlers,
deployer behavior, and liquidity signals, before execution.
</p>
</div>
<div>
<h3 className="text-xl font-medium mb-2">AI where it matters</h3>
<p className="text-muted-foreground">
No magic predictions. Just automated checks that experienced
traders already do, instantly and consistently.
</p>
</div>
<div>
<h3 className="text-xl font-medium mb-2">Built for speed</h3>
<p className="text-muted-foreground">
Optimized request batching, Solana-native execution, and Jupiter
routing keep latency low even during peak volume.
</p>
</div>
</div>
</section>
{/* Risk interception highlight */}
<section className="relative z-10 px-6 py-24 max-w-7xl mx-auto">
<div className="rounded-2xl border border-border bg-card/60 backdrop-blur p-10">
<h2 className="text-3xl font-semibold mb-4">
Nexa can stop you before you buy a rug
</h2>
<p className="text-muted-foreground max-w-2xl">
When high-risk patterns are detected, Nexa blocks execution by
default and tells you exactly why. If you still want in, that's your
call. But it won't be blind.
</p>
</div>
</section>
{/* How it works */}
<section className="relative z-10 px-6 py-24 max-w-7xl mx-auto">
<h2 className="text-3xl font-semibold mb-12">How it works</h2>
<div className="grid md:grid-cols-4 gap-8 text-sm">
<div className="text-muted-foreground">
<span className="block text-foreground font-medium mb-1">
1. Detect
</span>
New pairs, migrations, and liquidity events are tracked in real
time.
</div>
<div className="text-muted-foreground">
<span className="block text-foreground font-medium mb-1">
2. Analyze
</span>
Risk signals, holder distribution, and deployer behavior are scored.
</div>
<div className="text-muted-foreground">
<span className="block text-foreground font-medium mb-1">
3. Decide
</span>
AI filters or manual overrides determine execution eligibility.
</div>
<div className="text-muted-foreground">
<span className="block text-foreground font-medium mb-1">
4. Execute
</span>
Trades are routed through Jupiter with minimal latency.
</div>
</div>
</section>
{/* Fees & alignment */}
<section className="relative z-10 px-6 py-24 max-w-7xl mx-auto">
<div className="max-w-2xl">
<h2 className="text-3xl font-semibold mb-4">
Fees that scale with success
</h2>
<p className="text-muted-foreground">
Nexa charges a small platform fee on executed trades, with optional
AI and quant features available per-use. No subscriptions. No hidden
incentives.
</p>
</div>
</section>
</main>
);
} }
+9 -1
View File
@@ -1,3 +1,5 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
@@ -8,6 +10,8 @@ import {
WalletIcon, WalletIcon,
} from "lucide-react"; } from "lucide-react";
import { SolanaLogo } from "../logos"; import { SolanaLogo } from "../logos";
import { cn } from "@/lib/utils";
import { usePathname } from "next/navigation";
const links = [ const links = [
{ {
@@ -23,6 +27,7 @@ const links = [
export default function Navbar() { export default function Navbar() {
const personalItemClass = const personalItemClass =
"rounded-full bg-card p-2! aspect-square group hover:bg-accent/20 hover:text-accent"; "rounded-full bg-card p-2! aspect-square group hover:bg-accent/20 hover:text-accent";
const pathname = usePathname();
return ( return (
<nav className="border-b border-b-border py-4 px-5 flex gap-5 items-center"> <nav className="border-b border-b-border py-4 px-5 flex gap-5 items-center">
@@ -36,7 +41,10 @@ export default function Navbar() {
<li key={l.path}> <li key={l.path}>
<Link <Link
href={l.path} href={l.path}
className="px-3 py-2 rounded-lg hover:bg-primary/20 hover:text-primary transition-colors duration-300" className={cn(
"px-3 py-2 rounded-lg hover:bg-accent/20 hover:text-accent transition-colors duration-300",
l.path === pathname && "text-accent",
)}
> >
{l.name} {l.name}
</Link> </Link>
+57
View File
@@ -0,0 +1,57 @@
"use client";
import * as React from "react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-background text-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-background fill-background z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+22
View File
@@ -0,0 +1,22 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export default function MinimalTooltip({
children,
label,
independent = false,
bottom = false,
}: {
children: React.ReactNode;
label: React.ReactNode;
independent?: boolean;
bottom?: boolean;
}) {
return (
<Tooltip>
<TooltipTrigger asChild={!independent}>
<span className="inline-flex">{children}</span>
</TooltipTrigger>
<TooltipContent side={bottom ? "bottom" : "top"}>{label}</TooltipContent>
</Tooltip>
);
}
+6
View File
@@ -0,0 +1,6 @@
export const humanizeNumber = (num: number) => {
return new Intl.NumberFormat("en", {
notation: "compact",
maximumFractionDigits: 1,
}).format(num);
};
+7
View File
@@ -3,6 +3,13 @@ export interface Token {
symbol: string; symbol: string;
name: string; name: string;
logoURI?: string; logoURI?: string;
twitterPost: {
link: string;
author: {
username: string;
followers: number;
};
};
} }
export interface TokenLiveData { export interface TokenLiveData {