add bd, pages: adt, create, profile
This commit is contained in:
parent
a2d2ff4e01
commit
7b40ce2c1a
109
app/(root)/adt/[id]/page.tsx
Normal file
109
app/(root)/adt/[id]/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
app/(root)/adt/create/page.tsx
Normal file
105
app/(root)/adt/create/page.tsx
Normal 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
25
app/(root)/layout.tsx
Normal 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
39
app/(root)/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
app/(root)/profile/page.tsx
Normal file
66
app/(root)/profile/page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
8
app/api/category/route.ts
Normal file
8
app/api/category/route.ts
Normal 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.
@ -2,20 +2,12 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--background);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,21 +2,9 @@ import type { Metadata } from "next";
|
|||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import "./globals.css";
|
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({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@ -26,7 +14,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
101
app/page.tsx
101
app/page.tsx
@ -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
21
components.json
Normal 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
42
components/Categories.tsx
Normal 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
53
components/Header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
components/ListingCard.tsx
Normal file
51
components/ListingCard.tsx
Normal 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
57
components/ui/button.tsx
Normal 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
74
data/adt.ts
Normal 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
6
lib/utils.ts
Normal 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
868
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -6,21 +6,38 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"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": {
|
"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": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-dom": "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": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "15.0.3",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"eslint": "^8",
|
"typescript": "^5"
|
||||||
"eslint-config-next": "15.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
prisma/constant.ts
Normal file
101
prisma/constant.ts
Normal 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
13
prisma/prisma-client.ts
Normal 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
62
prisma/schema.prisma
Normal 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
67
prisma/seed.ts
Normal 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
9
services/adt.ts
Normal 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
7
services/api-client.ts
Normal 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
9
services/category.ts
Normal 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
5
services/constant.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum ApiRoutes {
|
||||||
|
SEARCH_PRODUCTS = 'products/search',
|
||||||
|
CATEGORY = 'category',
|
||||||
|
ADT = 'adt',
|
||||||
|
}
|
||||||
5
services/instance.ts
Normal file
5
services/instance.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const axiosInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||||
|
});
|
||||||
@ -1,18 +1,24 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: 'var(--background)',
|
||||||
foreground: "var(--foreground)",
|
foreground: 'var(--foreground)'
|
||||||
},
|
},
|
||||||
},
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwindcss-animate")],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user