feat: implement NotFound page with search functionality and UI components
feat: add Card, Input, and Label components for UI consistency refactor: update Button component to include cursor pointer style
This commit is contained in:
parent
0110bf782b
commit
d6624b1ff2
100
apps/web/src/app/not-found.tsx
Normal file
100
apps/web/src/app/not-found.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { Card, CardContent } from "@workspace/ui/components/card";
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { ArrowLeft, BookOpen, Home, Search } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function NotFound() {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const router = useRouter()
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (searchQuery.trim()) {
|
||||
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* 404 Illustration */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="relative mb-8">
|
||||
<div className="text-9xl font-bold text-primary/20 select-none">404</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-32 h-32 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<BookOpen className="h-16 w-16 text-primary/60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4">Page Not Found</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Oops! The page you{"'"}re looking for seems to have wandered off into another dimension.
|
||||
Don{"'"}t worry, we{"'"}ll help you find your way back to amazing stories!
|
||||
</p>
|
||||
</div>
|
||||
{/* Search Section */}
|
||||
<Card className="mb-12">
|
||||
<CardContent className="p-8">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-2xl font-semibold mb-2">Search for Stories</h2>
|
||||
<p className="text-muted-foreground">Looking for a specific novel? Try searching for it below.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="max-w-md mx-auto">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search novels, authors, genres..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-4 h-12 text-base"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-10"
|
||||
disabled={!searchQuery.trim()}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Button onClick={() => router.back()} variant="outline" size="lg" className="gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
|
||||
<Link href="/">
|
||||
<Button size="lg" className="gap-2">
|
||||
<Home className="h-4 w-4" />
|
||||
Return Home
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Help Text */}
|
||||
<div className="text-center mt-12 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Still can{"'"}t find what you{"'"}re looking for?{" "}
|
||||
<Link href="/contact" className="text-primary hover:underline">
|
||||
Contact our support team
|
||||
</Link>{" "}
|
||||
for assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] 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-md text-sm font-medium transition-[color,box-shadow] 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 cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
92
packages/ui/src/components/card.tsx
Normal file
92
packages/ui/src/components/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
21
packages/ui/src/components/input.tsx
Normal file
21
packages/ui/src/components/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
24
packages/ui/src/components/label.tsx
Normal file
24
packages/ui/src/components/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
Loading…
Reference in New Issue
Block a user