refactor: implement tab navigation in BuyMorePage and enhance button styles in BuyPagesContent and BuyTokensContent

This commit is contained in:
Anish Sarkar 2026-05-17 23:29:41 +05:30
parent 88a43cdd65
commit 10527ddb7c
6 changed files with 114 additions and 96 deletions

View file

@ -4,8 +4,7 @@ import { motion } from "motion/react";
import { useState } from "react"; import { useState } from "react";
import { BuyPagesContent } from "@/components/settings/buy-pages-content"; import { BuyPagesContent } from "@/components/settings/buy-pages-content";
import { BuyTokensContent } from "@/components/settings/buy-tokens-content"; import { BuyTokensContent } from "@/components/settings/buy-tokens-content";
import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
const TABS = [ const TABS = [
{ id: "pages", label: "Pages" }, { id: "pages", label: "Pages" },
@ -22,29 +21,34 @@ export default function BuyMorePage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="w-full select-none space-y-6" className="w-full select-none"
> >
<div className="flex items-center justify-center rounded-lg border bg-muted/30 p-1"> <Tabs
{TABS.map((tab) => ( value={activeTab}
<Button onValueChange={(value) => {
key={tab.id} setActiveTab(value as TabId);
type="button" }}
variant="ghost" className="relative min-h-[37rem] w-full"
size="sm" >
onClick={() => setActiveTab(tab.id)} <TabsList className="absolute top-20 left-1/2 -translate-x-1/2 rounded-xl bg-accent p-1">
className={cn( {TABS.map((tab) => (
"h-auto flex-1 px-3 py-1.5 text-sm", <TabsTrigger
activeTab === tab.id key={tab.id}
? "bg-background text-foreground shadow-sm" value={tab.id}
: "text-muted-foreground hover:text-accent-foreground" className="h-8 rounded-lg px-4 text-sm font-semibold text-accent-foreground transition-colors hover:bg-transparent hover:text-white data-[state=active]:bg-[#4a4a4a] data-[state=active]:text-white data-[state=active]:shadow-none"
)} >
> {tab.label}
{tab.label} </TabsTrigger>
</Button> ))}
))} </TabsList>
</div>
{activeTab === "pages" ? <BuyPagesContent /> : <BuyTokensContent />} <TabsContent value="pages" className="mt-0 flex min-h-[37rem] items-center pt-14">
<BuyPagesContent />
</TabsContent>
<TabsContent value="tokens" className="mt-0 flex min-h-[37rem] items-center pt-14">
<BuyTokensContent />
</TabsContent>
</Tabs>
</motion.div> </motion.div>
); );
} }

View file

@ -1,17 +1,11 @@
"use client"; "use client";
import { motion } from "motion/react";
import { MorePagesContent } from "@/components/settings/more-pages-content"; import { MorePagesContent } from "@/components/settings/more-pages-content";
export default function MorePagesPage() { export default function MorePagesPage() {
return ( return (
<motion.div <div className="w-full select-none space-y-6">
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full select-none space-y-6"
>
<MorePagesContent /> <MorePagesContent />
</motion.div> </div>
); );
} }

View file

@ -23,6 +23,7 @@ import {
Wrench, Wrench,
X, X,
} from "lucide-react"; } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image"; import Image from "next/image";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
@ -271,59 +272,76 @@ const ConnectToolsBanner: FC<{
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
return localStorage.getItem(BANNER_DISMISSED_KEY) === "true"; return localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
}); });
const [dismissRequested, setDismissRequested] = useState(false);
const hasConnectors = (connectors?.length ?? 0) > 0; const hasConnectors = (connectors?.length ?? 0) > 0;
const isVisible = !dismissed && !hasConnectors && isThreadEmpty; const isVisible = !dismissed && !hasConnectors && isThreadEmpty;
const shouldShowTray = isVisible && !dismissRequested;
useEffect(() => { useEffect(() => {
onVisibleChange?.(isVisible); onVisibleChange?.(isVisible);
}, [isVisible, onVisibleChange]); }, [isVisible, onVisibleChange]);
if (!isVisible) return null;
const handleDismiss = (e: React.MouseEvent) => { const handleDismiss = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
setDismissed(true); setDismissRequested(true);
localStorage.setItem(BANNER_DISMISSED_KEY, "true");
}; };
return ( return (
<div className="relative z-0 -mt-5 flex min-w-0 items-center gap-2 rounded-b-3xl border border-input bg-muted/40 px-4 pt-7 pb-3 shadow-sm shadow-black/5 dark:shadow-black/10"> <AnimatePresence
<Button initial={false}
type="button" onExitComplete={() => {
variant="ghost" if (!dismissRequested) return;
size="sm" setDismissed(true);
className="h-7 min-w-0 cursor-pointer justify-start gap-2 rounded-md px-0 text-[13px] font-normal text-muted-foreground select-none hover:bg-transparent hover:text-foreground" localStorage.setItem(BANNER_DISMISSED_KEY, "true");
onClick={() => setConnectorDialogOpen(true)} }}
> >
<Unplug className="size-4 shrink-0" /> {shouldShowTray ? (
<span className="truncate">Connect your tools</span> <motion.div
</Button> key="connect-tools-tray"
<div className="min-w-0 flex-1" /> initial={{ opacity: 0, y: -10 }}
<AvatarGroup className="shrink-0"> animate={{ opacity: 1, y: 0 }}
{BANNER_CONNECTORS.map(({ type }, i) => ( exit={{ opacity: 0, y: -14 }}
<Avatar transition={{ duration: 0.18, ease: "easeOut" }}
key={type} className="relative z-0 -mt-5 flex min-w-0 items-center gap-2 rounded-b-3xl border border-input bg-muted/40 px-4 pt-7 pb-3 shadow-sm shadow-black/5 dark:shadow-black/10"
className="size-5" >
style={{ zIndex: BANNER_CONNECTORS.length - i }} <Button
type="button"
variant="ghost"
size="sm"
className="h-7 min-w-0 cursor-pointer justify-start gap-2 rounded-md px-0 text-[13px] font-normal text-muted-foreground select-none hover:bg-transparent hover:text-foreground"
onClick={() => setConnectorDialogOpen(true)}
> >
<AvatarFallback className="bg-accent text-[10px]"> <Unplug className="size-4 shrink-0" />
{getConnectorIcon(type, "size-3")} <span className="truncate">Connect your tools</span>
</AvatarFallback> </Button>
</Avatar> <div className="min-w-0 flex-1" />
))} <AvatarGroup className="shrink-0">
</AvatarGroup> {BANNER_CONNECTORS.map(({ type }, i) => (
<Button <Avatar
type="button" key={type}
onClick={handleDismiss} className="size-5"
variant="ghost" style={{ zIndex: BANNER_CONNECTORS.length - i }}
size="icon" >
className="size-7 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground" <AvatarFallback className="bg-accent text-[10px]">
aria-label="Dismiss" {getConnectorIcon(type, "size-3")}
> </AvatarFallback>
<X className="size-3.5" /> </Avatar>
</Button> ))}
</div> </AvatarGroup>
<Button
type="button"
onClick={handleDismiss}
variant="ghost"
size="icon"
className="size-7 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-transparent hover:text-foreground"
aria-label="Dismiss"
>
<X className="size-3.5" />
</Button>
</motion.div>
) : null}
</AnimatePresence>
); );
}; };

View file

@ -74,11 +74,11 @@ export function BuyPagesContent() {
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
size="icon" size="icon"
onClick={() => setQuantity((q) => Math.max(1, q - 1))} onClick={() => setQuantity((q) => Math.max(1, q - 1))}
disabled={quantity <= 1 || purchaseMutation.isPending} disabled={quantity <= 1 || purchaseMutation.isPending}
className="size-8 shadow-none transition-colors hover:bg-muted disabled:opacity-40" className="size-8 text-muted-foreground shadow-none transition-colors hover:bg-muted hover:text-white disabled:opacity-40"
> >
<Minus className="h-3.5 w-3.5" /> <Minus className="h-3.5 w-3.5" />
</Button> </Button>
@ -87,11 +87,11 @@ export function BuyPagesContent() {
</span> </span>
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
size="icon" size="icon"
onClick={() => setQuantity((q) => Math.min(100, q + 1))} onClick={() => setQuantity((q) => Math.min(100, q + 1))}
disabled={quantity >= 100 || purchaseMutation.isPending} disabled={quantity >= 100 || purchaseMutation.isPending}
className="size-8 shadow-none transition-colors hover:bg-muted disabled:opacity-40" className="size-8 text-muted-foreground shadow-none transition-colors hover:bg-muted hover:text-white disabled:opacity-40"
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
</Button> </Button>
@ -107,10 +107,10 @@ export function BuyPagesContent() {
onClick={() => setQuantity(m)} onClick={() => setQuantity(m)}
disabled={purchaseMutation.isPending} disabled={purchaseMutation.isPending}
className={cn( className={cn(
"h-auto rounded-md border px-2.5 py-1 text-xs font-medium tabular-nums transition-colors hover:text-foreground disabled:opacity-60", "h-auto rounded-md px-2.5 py-1 text-xs font-medium tabular-nums transition-colors disabled:opacity-60",
quantity === m quantity === m
? "border-emerald-500 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" ? "bg-accent text-accent-foreground"
: "border-border hover:border-emerald-500/40 hover:bg-muted/40" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)} )}
> >
{(m * PAGE_PACK_SIZE).toLocaleString()} {(m * PAGE_PACK_SIZE).toLocaleString()}
@ -126,7 +126,7 @@ export function BuyPagesContent() {
</div> </div>
<Button <Button
className="w-full bg-emerald-600 text-white hover:bg-emerald-700" className="w-full"
disabled={purchaseMutation.isPending || !hasValidSearchSpace} disabled={purchaseMutation.isPending || !hasValidSearchSpace}
onClick={handleBuyNow} onClick={handleBuyNow}
> >

View file

@ -96,7 +96,7 @@ export function BuyTokensContent() {
</span> </span>
<span className="font-medium">{usagePercentage.toFixed(0)}%</span> <span className="font-medium">{usagePercentage.toFixed(0)}%</span>
</div> </div>
<Progress value={usagePercentage} className="h-1.5" /> <Progress value={usagePercentage} className="h-1.5 [&>div]:bg-purple-500" />
<p className="text-[11px] text-muted-foreground"> <p className="text-[11px] text-muted-foreground">
{formatUsd(remaining)} of credit remaining {formatUsd(remaining)} of credit remaining
</p> </p>
@ -107,11 +107,11 @@ export function BuyTokensContent() {
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
size="icon" size="icon"
onClick={() => setQuantity((q) => Math.max(1, q - 1))} onClick={() => setQuantity((q) => Math.max(1, q - 1))}
disabled={quantity <= 1 || purchaseMutation.isPending} disabled={quantity <= 1 || purchaseMutation.isPending}
className="size-8 shadow-none transition-colors hover:bg-muted disabled:opacity-40" className="size-8 text-muted-foreground shadow-none transition-colors hover:bg-muted hover:text-white disabled:opacity-40"
> >
<Minus className="h-3.5 w-3.5" /> <Minus className="h-3.5 w-3.5" />
</Button> </Button>
@ -120,11 +120,11 @@ export function BuyTokensContent() {
</span> </span>
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
size="icon" size="icon"
onClick={() => setQuantity((q) => Math.min(100, q + 1))} onClick={() => setQuantity((q) => Math.min(100, q + 1))}
disabled={quantity >= 100 || purchaseMutation.isPending} disabled={quantity >= 100 || purchaseMutation.isPending}
className="size-8 shadow-none transition-colors hover:bg-muted disabled:opacity-40" className="size-8 text-muted-foreground shadow-none transition-colors hover:bg-muted hover:text-white disabled:opacity-40"
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-3.5 w-3.5" />
</Button> </Button>
@ -139,10 +139,10 @@ export function BuyTokensContent() {
onClick={() => setQuantity(m)} onClick={() => setQuantity(m)}
disabled={purchaseMutation.isPending} disabled={purchaseMutation.isPending}
className={cn( className={cn(
"h-auto rounded-md border px-2.5 py-1 text-xs font-medium tabular-nums transition-colors hover:text-foreground disabled:opacity-60", "h-auto rounded-md px-2.5 py-1 text-xs font-medium tabular-nums transition-colors disabled:opacity-60",
quantity === m quantity === m
? "border-purple-500 bg-purple-500/10 text-purple-600 dark:text-purple-400" ? "bg-accent text-accent-foreground"
: "border-border hover:border-purple-500/40 hover:bg-muted/40" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)} )}
> >
${m} ${m}
@ -158,7 +158,7 @@ export function BuyTokensContent() {
</div> </div>
<Button <Button
className="w-full bg-purple-600 text-white hover:bg-purple-700" className="w-full"
disabled={purchaseMutation.isPending} disabled={purchaseMutation.isPending}
onClick={() => purchaseMutation.mutate({ quantity, search_space_id: searchSpaceId })} onClick={() => purchaseMutation.mutate({ quantity, search_space_id: searchSpaceId })}
> >

View file

@ -76,15 +76,17 @@ export function MorePagesContent() {
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-semibold">Earn Bonus Pages</h3> <h3 className="text-sm font-semibold">Earn Bonus Pages</h3>
{isLoading ? ( {isLoading ? (
<Card> <div className="space-y-1.5">
<CardContent className="flex items-center gap-3 p-3"> {["github", "reddit", "discord"].map((task) => (
<Skeleton className="h-8 w-8 rounded-full" /> <Card key={task} className="bg-transparent">
<div className="flex-1 space-y-2"> <CardContent className="flex items-center gap-3 p-3">
<Skeleton className="h-4 w-3/4" /> <Skeleton className="h-8 w-8 rounded-full bg-muted" />
</div> <Skeleton className="h-4 flex-1 bg-muted" />
<Skeleton className="h-8 w-16" /> <Skeleton className="h-8 w-16 bg-muted" />
</CardContent> </CardContent>
</Card> </Card>
))}
</div>
) : ( ) : (
<div className="space-y-1.5"> <div className="space-y-1.5">
{data?.tasks.map((task) => ( {data?.tasks.map((task) => (