add bd, pages: adt, create, profile

This commit is contained in:
Zikil 2024-11-20 18:58:35 +07:00
parent a2d2ff4e01
commit 7b40ce2c1a
30 changed files with 1714 additions and 276 deletions

View File

@ -0,0 +1,109 @@
import React from 'react';
// import { useParams } from 'next/navigation';
import { MapPin, Calendar, Phone, MessageCircle, Share2, Flag, Heart } from 'lucide-react';
import { adts } from '@/data/adt';
import { prisma } from '@/prisma/prisma-client';
import Header from '@/components/Header';
import { notFound } from 'next/navigation';
export default async function AdtPage({params: { id } }: { params: { id: string } }) {
const adt = await prisma.adt.findUnique({
where: {
id: Number(id),
},
include: {
user: true
}
})
if (!adt) {
return notFound();
}
const user = adt.user
// const { id } = params();
// const adt = adts.find(l => l.id === id) || adts[0];
return (
<>
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<img src={adt.image} alt={adt.title} className="w-full h-[400px] object-cover" />
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<h1 className="text-2xl font-semibold">{adt.title}</h1>
<span className="text-2xl font-bold text-indigo-600">{adt.price}</span>
</div>
<div className="flex items-center gap-4 text-gray-500 mb-6">
<div className="flex items-center gap-1">
<MapPin className="h-5 w-5" />
<span>{adt.location}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="h-5 w-5" />
<span>{String(adt.createdAt)}</span>
</div>
</div>
<h2 className="font-semibold text-lg mb-3">Description</h2>
<p className="text-gray-600 mb-6">
This is a detailed description of the listing. It includes all the important information
about the item, its condition, and any special features or considerations.
</p>
<div className="flex gap-3">
<button className="flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-200 hover:bg-gray-50">
<Share2 className="h-5 w-5" />
<span>Share</span>
</button>
<button className="flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-200 hover:bg-gray-50">
<Flag className="h-5 w-5" />
<span>Report</span>
</button>
</div>
</div>
</div>
</div>
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-sm p-6 sticky top-24">
<div className="flex items-center gap-4 mb-6">
<img
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=100"
alt="Seller"
className="w-12 h-12 rounded-full"
/>
<div>
<h3 className="font-semibold">{user.name}</h3>
<p className="text-sm text-gray-500">Member {String(user.createdAt)}</p>
</div>
</div>
<div className="space-y-3">
{/* TODO всплывающее окно для показа номера */}
<button className="w-full flex items-center justify-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700">
<Phone className="h-5 w-5" />
<span>Show Phone Number</span>
</button>
<button className="w-full flex items-center justify-center gap-2 bg-white border border-indigo-600 text-indigo-600 px-4 py-2 rounded-lg hover:bg-indigo-50">
<MessageCircle className="h-5 w-5" />
<span>Send Message (In development)</span>
</button>
<button className="w-full flex items-center justify-center gap-2 bg-white border border-gray-200 px-4 py-2 rounded-lg hover:bg-gray-50">
<Heart className="h-5 w-5" />
<span>Save to Favorites (In development)</span>
</button>
</div>
</div>
</div>
</div>
</main>
</>
);
}

View File

@ -0,0 +1,105 @@
import React from 'react';
import { ImagePlus, X } from 'lucide-react';
export default function CreateListing() {
return (
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-xl shadow-sm p-6">
<h1 className="text-2xl font-semibold mb-6">Create New Listing</h1>
<form className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Title
</label>
<input
type="text"
className="w-full px-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
placeholder="Enter listing title"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Category
</label>
<select className="w-full px-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500">
<option>Select a category</option>
<option>Vehicles</option>
<option>Real Estate</option>
<option>Electronics</option>
<option>Fashion</option>
<option>Jobs</option>
<option>Sports</option>
<option>Art</option>
<option>Books</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Price
</label>
<div className="relative">
<span className="absolute left-4 top-2 text-gray-500">$</span>
<input
type="number"
className="w-full pl-8 pr-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
placeholder="0.00"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
rows={4}
className="w-full px-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
placeholder="Describe your item..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Photos
</label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<button className="aspect-square rounded-lg border-2 border-dashed border-gray-300 flex flex-col items-center justify-center hover:border-indigo-500 hover:bg-indigo-50">
<ImagePlus className="h-8 w-8 text-gray-400" />
<span className="mt-2 text-sm text-gray-500">Add Photo</span>
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Location
</label>
<input
type="text"
className="w-full px-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
placeholder="Enter location"
/>
</div>
<div className="flex gap-3">
<button
type="submit"
className="flex-1 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700"
>
Create Listing
</button>
<button
type="button"
className="px-4 py-2 rounded-lg border border-gray-200 hover:bg-gray-50"
>
Cancel
</button>
</div>
</form>
</div>
</main>
);
}

25
app/(root)/layout.tsx Normal file
View File

@ -0,0 +1,25 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "../globals.css";
export const metadata: Metadata = {
title: "Bazar",
description: "Bazar",
};
export default function HomeLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

39
app/(root)/page.tsx Normal file
View File

@ -0,0 +1,39 @@
// "use client"
import Categories from "@/components/Categories";
import Header from "@/components/Header";
import ListingCard from "@/components/ListingCard";
import { adts } from "@/data/adt";
import { prisma } from "@/prisma/prisma-client";
import Image from "next/image";
export default async function Home() {
const adts = await prisma.adt.findMany()
return (
<>
<div className="min-h-screen bg-gray-100">
<Header />
<Categories />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold">Featured Listings</h2>
<div className="flex gap-2">
<select className="px-4 py-2 rounded-lg border border-gray-200 bg-white">
<option>Most Recent</option>
<option>Price: Low to High</option>
<option>Price: High to Low</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{adts.map((adt) => (
<ListingCard key={adt.id} title={adt.title} image={adt.image} price={adt.price} location={adt.location} date={String(adt.createdAt)} id={adt.id} />
))}
</div>
</main>
</div>
</>
);
}

View File

@ -0,0 +1,66 @@
import React from 'react';
import { Settings, Package, Heart, Bell } from 'lucide-react';
import ListingCard from '@/components/ListingCard';
import { adts } from '@/data/adt';
import Header from '@/components/Header';
export default function Profile() {
return (
<>
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-xl shadow-sm mb-8">
<div className="relative h-48 rounded-t-xl bg-gradient-to-r from-indigo-500 to-purple-600">
<img
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&w=150"
alt="Profile"
className="absolute -bottom-12 left-8 w-24 h-24 rounded-full border-4 border-white"
/>
</div>
<div className="pt-16 pb-8 px-8">
<div className="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold">John Doe</h1>
<p className="text-gray-500">San Francisco, CA</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-200 hover:bg-gray-50">
<Settings className="h-5 w-5" />
<span>Edit Profile</span>
</button>
</div>
</div>
<div className="border-t">
<nav className="flex divide-x">
<button className="flex-1 px-4 py-3 text-indigo-600 border-b-2 border-indigo-600">
<div className="flex items-center justify-center gap-2">
<Package className="h-5 w-5" />
<span>My Listings</span>
</div>
</button>
<button className="flex-1 px-4 py-3 text-gray-500 hover:text-gray-700">
<div className="flex items-center justify-center gap-2">
<Heart className="h-5 w-5" />
<span>Favorites</span>
</div>
</button>
<button className="flex-1 px-4 py-3 text-gray-500 hover:text-gray-700">
<div className="flex items-center justify-center gap-2">
<Bell className="h-5 w-5" />
<span>Notifications</span>
</div>
</button>
</nav>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{adts.slice(0, 3).map((adt) => (
<ListingCard key={adt.id} {...adt} />
))}
</div>
</main>
</>
);
}

View File

@ -0,0 +1,8 @@
import { prisma } from "@/prisma/prisma-client"
import { NextResponse } from "next/server"
export async function GET() {
const ingredients = await prisma.category.findMany()
return NextResponse.json(ingredients)
}

Binary file not shown.

Binary file not shown.

View File

@ -2,20 +2,12 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
:root {
--radius: 0.5rem;
}
}

View File

@ -2,21 +2,9 @@ import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
@ -26,7 +14,7 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>

View File

@ -1,101 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

42
components/Categories.tsx Normal file
View File

@ -0,0 +1,42 @@
// "use client"
import React from 'react';
import { Car, Home, Laptop, Shirt, Briefcase, Dumbbell, Palette, Book } from 'lucide-react';
import Link from 'next/link';
import { prisma } from '@/prisma/prisma-client';
const categories = [
{ name: 'Vehicles', icon: Car },
{ name: 'Real Estate', icon: Home },
{ name: 'Electronics', icon: Laptop },
{ name: 'Fashion', icon: Shirt },
{ name: 'Jobs', icon: Briefcase },
{ name: 'Sports', icon: Dumbbell },
{ name: 'Art', icon: Palette },
{ name: 'Books', icon: Book },
];
export default async function Categories() {
const categories = await prisma.category.findMany();
return (
<div className="py-8 bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-xl font-semibold mb-6">Browse Categories</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-4">
{categories.map((category) => {
// const Icon = category.icon;
return (
<button
key={category.name}
className="flex flex-col items-center p-4 bg-white rounded-xl hover:shadow-md transition-shadow"
>
{/* <Icon className="h-8 w-8 text-indigo-600 mb-2" /> */}
<span className="text-sm text-gray-700">{category.name}</span>
</button>
);
})}
</div>
</div>
</div>
);
}

53
components/Header.tsx Normal file
View File

@ -0,0 +1,53 @@
"use client"
import React from 'react';
import { Search, PlusCircle, Bell, User } from 'lucide-react';
import Link from 'next/link';
export default function Header() {
return (
<header className="sticky top-0 z-50 bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<Link href="/" className="text-2xl font-bold text-indigo-600">
MarketSpot
</Link>
</div>
<div className="flex-1 max-w-2xl mx-8">
<div className="relative">
<input
type="text"
placeholder="Search listings..."
className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
/>
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
</div>
</div>
<div className="flex items-center gap-4">
<Link
href="/adt/create"
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors"
>
<PlusCircle className="h-5 w-5" />
<span>Post Ad</span>
</Link>
<button className="relative p-2 hover:bg-gray-100 rounded-full">
<Bell className="h-6 w-6 text-gray-600" />
<span className="absolute top-0 right-0 h-4 w-4 bg-red-500 rounded-full text-xs text-white flex items-center justify-center">
2
</span>
</button>
<Link
href="/profile"
className="p-2 hover:bg-gray-100 rounded-full"
>
<User className="h-6 w-6 text-gray-600" />
</Link>
</div>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,51 @@
"use client"
import React from 'react';
import { Heart, MapPin } from 'lucide-react';
import Link from 'next/link';
// import { Link } from 'next/navigation';
interface ListingCardProps {
id: string;
title: string;
price?: string;
location?: string;
image?: string;
date: string;
}
export default function ListingCard({ id, title, price, location, image, date }: ListingCardProps) {
return (
<Link href={`/adt/${id}`} className="block">
<div className="bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="relative aspect-[4/3]">
<img
src={image}
alt={title}
className="w-full h-full object-cover rounded-t-xl"
/>
<button
className="absolute top-3 right-3 p-2 bg-white/90 rounded-full hover:bg-white"
onClick={(e) => {
e.preventDefault();
// Handle favorite toggle
}}
>
<Heart className="h-5 w-5 text-gray-600" />
</button>
</div>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-medium text-gray-900 line-clamp-2">{title}</h3>
<span className="text-lg font-semibold text-indigo-600">{price}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<MapPin className="h-4 w-4" />
<span>{location}</span>
</div>
<div className="mt-2 text-sm text-gray-400">{date}</div>
</div>
</div>
</Link>
);
}

57
components/ui/button.tsx Normal file
View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300",
{
variants: {
variant: {
default:
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
destructive:
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
outline:
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
secondary:
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

74
data/adt.ts Normal file
View File

@ -0,0 +1,74 @@
export const adts = [
{
id: '1',
title: "2020 Tesla Model 3 Long Range",
price: "$41,999",
location: "San Francisco, CA",
image: "https://images.unsplash.com/photo-1560958089-b8a1929cea89?auto=format&fit=crop&w=800",
date: "Posted 2 hours ago"
},
{
id: '2',
title: "Modern Studio Apartment in Downtown",
price: "$2,200/mo",
location: "Seattle, WA",
image: "https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=800",
date: "Posted 5 hours ago"
},
{
id: '3',
title: "MacBook Pro M2 16-inch",
price: "$2,499",
location: "Austin, TX",
image: "https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=800",
date: "Posted 1 day ago"
},
{
id: '4',
title: "Vintage Leather Jacket",
price: "$299",
location: "Portland, OR",
image: "https://images.unsplash.com/photo-1551028719-00167b16eac5?auto=format&fit=crop&w=800",
date: "Posted 2 days ago"
},
{
id: '5',
title: "Professional DSLR Camera Kit",
price: "$1,899",
location: "New York, NY",
image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800",
date: "Posted 3 days ago"
},
{
id: '6',
title: "Handcrafted Wooden Dining Table",
price: "$899",
location: "Denver, CO",
image: "https://images.unsplash.com/photo-1577140917170-285929fb55b7?auto=format&fit=crop&w=800",
date: "Posted 4 days ago"
},
{
id: '7',
title: "Smart Bluetooth Speaker",
price: "$99",
location: "Los Angeles, CA",
image: "https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=800",
date: "Posted 5 days ago"
},
{
id: '8',
title: "Luxury Designer Watch",
price: "$1,299",
location: "Chicago, IL",
image: "https://images.unsplash.com/photo-1551028719-00167b16eac5?auto=format&fit=crop&w=800",
date: "Posted 6 days ago"
},
{
id: '9',
title: "Gaming Laptop",
price: "$1,499",
location: "Miami, FL",
image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800",
date: "Posted 7 days ago"
}
];

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,21 +6,38 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prisma:push": "prisma db push",
"prisma:studio": "prisma studio",
"prisma:seed": "prisma db seed"
},
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@radix-ui/react-slot": "^1.1.0",
"@types/bcrypt": "^5.0.2",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.460.0",
"next": "15.0.3",
"prisma": "^5.22.0",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"next": "15.0.3"
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.3"
"typescript": "^5"
}
}

101
prisma/constant.ts Normal file
View File

@ -0,0 +1,101 @@
export const categories = [
{
name: "Vehicles",
},
{
name: "Real Estate",
},
{
name: "Electronics",
},
{
name: "Fashion",
},
{
name: "Sports",
},
{
name: "Art",
},
{
name: "Books",
},
{
name: "etc",
},
]
export const adts = [
{
title: "2020 Tesla Model 3 Long Range",
price: "$41,999",
location: "San Francisco, CA",
image: "https://images.unsplash.com/photo-1560958089-b8a1929cea89?auto=format&fit=crop&w=800",
// date: "Posted 2 hours ago"
userId: 1
},
{
title: "Modern Studio Apartment in Downtown",
price: "$2,200/mo",
location: "Seattle, WA",
image: "https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=800",
// date: "Posted 5 hours ago"
userId: 1
},
{
title: "MacBook Pro M2 16-inch",
price: "$2,499",
location: "Austin, TX",
image: "https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=800",
// date: "Posted 1 day ago"
userId: 1
},
{
title: "Vintage Leather Jacket",
price: "$299",
location: "Portland, OR",
image: "https://images.unsplash.com/photo-1551028719-00167b16eac5?auto=format&fit=crop&w=800",
// date: "Posted 2 days ago"
userId: 2
},
{
title: "Professional DSLR Camera Kit",
price: "$1,899",
location: "New York, NY",
image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800",
// date: "Posted 3 days ago"
userId: 1
},
{
title: "Handcrafted Wooden Dining Table",
price: "$899",
location: "Denver, CO",
image: "https://images.unsplash.com/photo-1577140917170-285929fb55b7?auto=format&fit=crop&w=800",
// date: "Posted 4 days ago"
userId: 2
},
{
title: "Smart Bluetooth Speaker",
price: "$99",
location: "Los Angeles, CA",
image: "https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=800",
// date: "Posted 5 days ago"
userId: 1
},
{
title: "Luxury Designer Watch",
price: "$1,299",
location: "Chicago, IL",
image: "https://images.unsplash.com/photo-1551028719-00167b16eac5?auto=format&fit=crop&w=800",
// date: "Posted 6 days ago"
userId: 1
},
{
title: "Gaming Laptop",
price: "$1,499",
location: "Miami, FL",
image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800",
// date: "Posted 7 days ago"
userId: 1
}
];

13
prisma/prisma-client.ts Normal file
View File

@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma

62
prisma/schema.prisma Normal file
View File

@ -0,0 +1,62 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
role Role @default(USER)
adts Adt[]
// favoriteAdts Adt[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Adt {
id Int @id @default(autoincrement())
title String
description String?
price String?
location String?
image String?
status Status @default(CHECKING)
user User @relation(fields: [userId], references: [id])
userId Int
categories Category[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Category {
id Int @id @default(autoincrement())
name String
adts Adt[]
}
enum Role {
USER
ADMIN
}
enum Status {
CHECKING
PUBLISHED
CLOSED
}

67
prisma/seed.ts Normal file
View File

@ -0,0 +1,67 @@
import { adts, categories } from "./constant";
import { prisma } from "./prisma-client";
import { hashSync } from "bcrypt";
async function up() {
await prisma.user.createMany({
data: [
{
name: "user",
email: "j@j.com",
password: hashSync("123456", 10),
// verified: new Date(),
role: "USER",
},
{
name: "user2",
email: "da@j.com",
password: hashSync("123456", 10),
// verified: new Date(),
role: "USER",
},
{
name: "admin",
email: "d@j.com",
password: hashSync("123456", 10),
// verified: new Date(),
role: "ADMIN",
}
]
});
await prisma.category.createMany({
data: categories
});
await prisma.adt.createMany({
data: adts
});
}
async function down() {
await prisma.$executeRaw`TRUNCATE TABLE "User" RESTART IDENTITY CASCADE`;
await prisma.$executeRaw`TRUNCATE TABLE "Category" RESTART IDENTITY CASCADE`;
await prisma.$executeRaw`TRUNCATE TABLE "Adt" RESTART IDENTITY CASCADE`;
}
async function main() {
try {
await down();
await up();
} catch (e) {
console.error(e);
}
}
main()
.then(async() => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect()
process.exit(1)
})

9
services/adt.ts Normal file
View File

@ -0,0 +1,9 @@
import { Adt } from "@prisma/client"
import { axiosInstance } from "./instance"
import { ApiRoutes } from "./constant";
export const search = async (query: string): Promise<Adt[]> => {
const {data} = await axiosInstance.get<Adt[]>(ApiRoutes.ADT, {params: {query}});
return data;
}

7
services/api-client.ts Normal file
View File

@ -0,0 +1,7 @@
import * as adts from './adt';
import * as categorys from './category';
export const Api = {
adts,
categorys
}

9
services/category.ts Normal file
View File

@ -0,0 +1,9 @@
import { Category } from "@prisma/client"
import { axiosInstance } from "./instance"
import { ApiRoutes } from "./constant";
export const getAll = async (): Promise<Category[]> => {
const {data} = await axiosInstance.get<Category[]>(ApiRoutes.CATEGORY);
return data;
}

5
services/constant.tsx Normal file
View File

@ -0,0 +1,5 @@
export enum ApiRoutes {
SEARCH_PRODUCTS = 'products/search',
CATEGORY = 'category',
ADT = 'adt',
}

5
services/instance.ts Normal file
View File

@ -0,0 +1,5 @@
import axios from "axios";
export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});

View File

@ -1,18 +1,24 @@
import type { Config } from "tailwindcss";
export default {
content: [
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
extend: {
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [],
plugins: [require("tailwindcss-animate")],
} satisfies Config;