feat: complete frontend UI - comparison views, profile, explore, layout
This commit is contained in:
@@ -1,50 +1,174 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { Sparkles } from "lucide-react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Sparkles, Home, BarChart3, Compass, User, Menu, X, Search } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", label: "Home", icon: Home },
|
||||
{ href: "/compare", label: "Compare", icon: BarChart3 },
|
||||
{ href: "/explore", label: "Explore", icon: Compass },
|
||||
{ href: "/profile", label: "Profile", icon: User },
|
||||
]
|
||||
|
||||
function NavSidebar({ className }: { className?: string }) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className={cn("flex flex-col gap-1", className)}>
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
className={cn(
|
||||
"w-full justify-start gap-2",
|
||||
isActive && "bg-muted text-primary font-medium"
|
||||
)}
|
||||
>
|
||||
<item.icon className="size-4" />
|
||||
{item.label}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:hidden">
|
||||
<div className="flex items-center justify-around h-14">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link key={item.href} href={item.href} className="flex flex-col items-center gap-1 py-2 px-3">
|
||||
<item.icon className={cn("size-5", isActive ? "text-primary" : "text-muted-foreground")} />
|
||||
<span className={cn("text-xs", isActive ? "text-primary font-medium" : "text-muted-foreground")}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-14 items-center px-4 mx-auto">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold mr-8">
|
||||
<div className="container flex h-14 items-center px-4 mx-auto gap-4">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold shrink-0">
|
||||
<Sparkles className="size-5 text-primary" />
|
||||
<span className="text-lg">ComparAIson</span>
|
||||
<span className="text-lg hidden sm:inline">ComparAIson</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex-1 max-w-md">
|
||||
<div className="flex-1 max-w-md mx-auto hidden md:block">
|
||||
<div className="relative">
|
||||
<input
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search comparisons..."
|
||||
className="w-full h-8 rounded-lg border border-input bg-muted/50 px-3 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 ml-auto">
|
||||
<Link href="/compare">
|
||||
<button className="h-8 px-3 rounded-lg text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/80 transition-colors">
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Link href="/compare" className="hidden sm:block">
|
||||
<Button size="sm" className="gap-2">
|
||||
<Sparkles className="size-3.5" />
|
||||
New Comparison
|
||||
</button>
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="size-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground">
|
||||
U
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="rounded-full">
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src="/placeholder-avatar.png" />
|
||||
<AvatarFallback className="bg-primary/10 text-primary font-medium">U</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link href="/profile" className="w-full">Profile</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href="/compare" className="w-full">New Comparison</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link href="/sign-in" className="w-full">Sign Out</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? <X className="size-5" /> : <Menu className="size-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mobileMenuOpen && (
|
||||
<div className="border-t md:hidden bg-background p-4">
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search comparisons..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<NavSidebar />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<main className="flex-1">{children}</main>
|
||||
<div className="flex flex-1">
|
||||
<aside className="hidden md:block w-52 border-r bg-muted/20 p-4 shrink-0">
|
||||
<NavSidebar />
|
||||
</aside>
|
||||
|
||||
<footer className="border-t py-4">
|
||||
<main className="flex-1 pb-16 md:pb-0">{children}</main>
|
||||
</div>
|
||||
|
||||
<MobileNav />
|
||||
|
||||
<footer className="border-t py-4 hidden md:block">
|
||||
<div className="container mx-auto px-4 text-center text-xs text-muted-foreground">
|
||||
ComparAIson — AI-powered deep research comparisons
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user