From 93b86ae3cc136d971d119c08db55b1d88fc1957b Mon Sep 17 00:00:00 2001 From: Zikil Date: Sun, 24 Nov 2024 22:33:34 +0700 Subject: [PATCH] add category, city, country, --- @types/prisma.ts | 3 + app/(root)/[category]/page.tsx | 92 +++ app/(root)/adt/[id]/page.tsx | 67 +- app/(root)/adt/create/page.tsx | 4 +- app/(root)/page.tsx | 2 +- app/(root)/profile/page.tsx | 2 +- app/actions.ts | 30 - app/api/adt/route.ts | 71 +- components/Categories.tsx | 54 +- .../shared/adt-create/adt-create-form.tsx | 168 +++- components/shared/adt-create/schemas.ts | 17 +- components/shared/block-adts.tsx | 17 +- components/shared/breadcrumbs-category.tsx | 36 + components/ui/breadcrumb.tsx | 115 +++ components/ui/card.tsx | 76 ++ components/ui/form.tsx | 178 ++++ components/ui/label.tsx | 26 + components/ui/select.tsx | 159 ++++ components/ui/toast.tsx | 129 +++ components/ui/toaster.tsx | 35 + hooks/use-toast.ts | 194 +++++ package-lock.json | 245 ++++++ package.json | 3 + prisma/constant.ts | 767 ++++++++++++------ prisma/schema.prisma | 192 ++++- prisma/seed.ts | 41 +- 26 files changed, 2264 insertions(+), 459 deletions(-) create mode 100644 @types/prisma.ts create mode 100644 app/(root)/[category]/page.tsx create mode 100644 components/shared/breadcrumbs-category.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 hooks/use-toast.ts diff --git a/@types/prisma.ts b/@types/prisma.ts new file mode 100644 index 0000000..0aeef84 --- /dev/null +++ b/@types/prisma.ts @@ -0,0 +1,3 @@ +import { Adt, Category, City, User } from "@prisma/client"; + +export type AdtWithRelations = Adt & { category: Category; city: City; user: User }; \ No newline at end of file diff --git a/app/(root)/[category]/page.tsx b/app/(root)/[category]/page.tsx new file mode 100644 index 0000000..cf98744 --- /dev/null +++ b/app/(root)/[category]/page.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { notFound } from 'next/navigation'; +import { BreadcrumbsCategory } from '@/components/shared/breadcrumbs-category'; +import { BlockAdts } from '@/components/shared/block-adts'; +import { prisma } from '@/prisma/prisma-client'; + + +type Params = Promise<{ category: string }> + +export default async function CategoryPage(props: { params: Params }) { + const params = await props.params; + const categorySlug = params.category; + + const category = await prisma.category.findFirst({ + where: { + slug: categorySlug + }, + include: { + adts: true, + children: { + include: { + adts: true + } + }, + parent: true + } + }); + + if (!category) { + return notFound(); + } + + // Объединяем объявления текущей категории и всех подкатегорий + const allAdts = [ + ...category.adts, + ...category.children.flatMap(child => child.adts) + ]; + +// const aadts = await fetch(`/api/adt?category=${categorySlug}`).then(res => res.json()); + + + return ( +
+
+

{category.nameEn}

+ {category.parentId && ( + + )} +
+ + {category.children.length > 0 && ( +
+

Подкатегории

+
+ {category.children.map((subcat) => ( + + {subcat.nameEn} + + ))} +
+
+ )} + +
+

Объявления

+ + {/* {allAdts.map((adt) => ( +
+ {adt.image && ( + {adt.title} + )} +
+

{adt.title}

+

{adt.description}

+

+ {adt.price ? `${adt.price} ₽` : 'Цена не указана'} +

+
+
+ ))} */} +
+
+ ); +} diff --git a/app/(root)/adt/[id]/page.tsx b/app/(root)/adt/[id]/page.tsx index e61dbb5..82ecce8 100644 --- a/app/(root)/adt/[id]/page.tsx +++ b/app/(root)/adt/[id]/page.tsx @@ -2,25 +2,32 @@ import React from 'react'; // import { useParams } from 'next/navigation'; -import { MapPin, Calendar, Phone, MessageCircle, Share2, Flag, Heart } from 'lucide-react'; +import { MapPin, Calendar, Phone, MessageCircle, Share2, Flag, Heart, Tag, Building, ChevronRight } from 'lucide-react'; import { prisma } from '@/prisma/prisma-client'; import { notFound } from 'next/navigation'; import { ShowNumberModal } from '@/components/shared/modals/show-number'; import { getUserSession } from '@/lib/get-user-session'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; +import { BreadcrumbsCategory } from '@/components/shared/breadcrumbs-category'; type Params = Promise<{ id: string }> export default async function AdtPage(props: { params: Params }) { const params = await props.params; - const session = await getUserSession(); const adt = await prisma.adt.findFirst({ where: { id: Number(params.id), }, include: { - user: true + user: true, + category: { + include: { + parent: true + } + }, + city: true } }) @@ -30,47 +37,50 @@ export default async function AdtPage(props: { params: Params }) { const user = adt.user - // const { id } = params(); - // const adt = adts.find(l => l.id === id) || adts[0]; - return ( <> -
+
+
+

{adt.title}

+ +
+
{adt.title}
-
-

{adt.title}

- {adt.price} -
- - {adt.location} + + {adt.address || 'Адрес не указан'}
- - {String(adt.createdAt)} + + {adt.city?.nameEn}
+
+ + {new Date(adt.createdAt).toLocaleDateString()} +
+ {adt.price ? `${adt.price} ₽` : 'Цена не указана'}
-

Description

+

Описание

- {adt.description} + {adt.description || 'Описание отсутствует'}

@@ -86,27 +96,20 @@ export default async function AdtPage(props: { params: Params }) { className="w-12 h-12 rounded-full" />
-

{user?.name}

-

Member {String(user?.createdAt)}

+

{user?.name || 'Пользователь'}

+

На сайте с {new Date(user?.createdAt || '').toLocaleDateString()}

- {/* */}
diff --git a/app/(root)/adt/create/page.tsx b/app/(root)/adt/create/page.tsx index f05ca20..3ffd295 100644 --- a/app/(root)/adt/create/page.tsx +++ b/app/(root)/adt/create/page.tsx @@ -8,11 +8,13 @@ const prisma = new PrismaClient(); export default async function CreateListing() { const categories = await prisma.category.findMany(); + const countries = await prisma.country.findMany(); + const cities = await prisma.city.findMany(); return ( - + //
diff --git a/app/(root)/page.tsx b/app/(root)/page.tsx index 1a3de5a..63a1dfd 100644 --- a/app/(root)/page.tsx +++ b/app/(root)/page.tsx @@ -8,7 +8,7 @@ import { prisma } from "@/prisma/prisma-client"; import toast from "react-hot-toast"; export default async function Home() { - const adts = await prisma.adt.findMany() + // const adts = await prisma.adt.findMany() // const session = await getUserSession() // // console.log(user) // console.log("session",session) diff --git a/app/(root)/profile/page.tsx b/app/(root)/profile/page.tsx index 39f53c7..359c583 100644 --- a/app/(root)/profile/page.tsx +++ b/app/(root)/profile/page.tsx @@ -74,7 +74,7 @@ export default async function Profile() {
{user?.adts.map((adt) => ( - + ))}
diff --git a/app/actions.ts b/app/actions.ts index 83174c8..3232d64 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -65,33 +65,3 @@ export async function registerUser(body: Prisma.UserCreateInput) { } -export async function createAdt(body: Prisma.AdtCreateInput, categories: Category[]) { - try { - - const currentUser = await getUserSession(); - - if (!currentUser) { - throw new Error('Not found user') - } - - - await prisma.adt.create({ - data: { - title: body.title, - categories: { - connect: categories?.map((category) => ({ - id: category.id, - })), - }, - price: body.price, - description: body.description, - image: body.image, - location: body.location, - userId: Number(currentUser.id), - } - }) - } catch (error) { - console.log('Error [create adt]', error); - throw error; - } -} \ No newline at end of file diff --git a/app/api/adt/route.ts b/app/api/adt/route.ts index 1f580c8..a743e14 100644 --- a/app/api/adt/route.ts +++ b/app/api/adt/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { PrismaClient } from '@prisma/client'; +import { Prisma, PrismaClient } from '@prisma/client'; import { formAdtCreateSchema } from '@/components/shared/adt-create/schemas'; import { getUserSession } from '@/lib/get-user-session'; @@ -9,6 +9,7 @@ const prisma = new PrismaClient(); // GET функция для получения списка объявлений с пагинацией export async function GET(request: Request) { try { + console.log("request",request) // Получаем параметры из URL const { searchParams } = new URL(request.url); @@ -17,22 +18,69 @@ export async function GET(request: Request) { const limit = Number(searchParams.get('limit')) || 10; const skip = (page - 1) * limit; - // Получаем общее количество объявлений для пагинации - const total = await prisma.adt.count(); + // Получаем параметры фильтрации + const category = searchParams.get('category'); + const cityId = searchParams.get('cityId'); - // Получаем объявления с учетом пагинации + + // Формируем условия фильтрации + const where: Prisma.AdtWhereInput = {}; + + if (category) { + // Сначала находим категорию и ее дочерние категории + const categoryWithChildren = await prisma.category.findFirst({ + where: { slug: category }, + include: { + children: true + } + }); + + if (categoryWithChildren) { + // Получаем ID текущей категории и всех дочерних категорий + const categoryIds = [ + categoryWithChildren.id, + ...categoryWithChildren.children.map(child => child.id) + ]; + + // Используем IN для поиска объявлений во всех категориях + where.categoryId = { + in: categoryIds + }; + } else { + where.category = { + slug: category + }; + } + } + if (cityId) { + where.cityId = cityId; + } + + + // Получаем общее количество объявлений для пагинации с учетом фильтров + const total = await prisma.adt.count({ where }); + + // Получаем объявления с учетом пагинации и фильтров const adts = await prisma.adt.findMany({ + where, skip, take: limit, include: { - categories: true, + category: true, user: { select: { id: true, name: true, email: true } - } + }, + city: { + select: { + nameEn: true, + nameAr: true + } + }, + country: true }, orderBy: { createdAt: 'desc' // Сортировка по дате создания (новые первыми) @@ -89,22 +137,21 @@ export async function POST(request: Request) { { status: 404 } ); } - // Создание объявления const adt = await prisma.adt.create({ data: { title: validatedData.title, description: validatedData.description, price: validatedData.price, - location: validatedData.location, + address: validatedData.address, image: validatedData.image, userId: user.id, - categories: { - connect: validatedData.categoryIds.map(id => ({ id })) - } + categoryId: validatedData.categoryId, + countryId: validatedData.countryId, + cityId: validatedData.cityId }, include: { - categories: true + category: true } }); diff --git a/components/Categories.tsx b/components/Categories.tsx index 2cb096b..20ce834 100644 --- a/components/Categories.tsx +++ b/components/Categories.tsx @@ -1,40 +1,38 @@ // "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(); + const categories = await prisma.category.findMany({ + where: { + parentId: null, + } + }); return ( -
+
-

Browse Categories

-
- {categories.map((category) => { - // const Icon = category.icon; - return ( - - ); - })} +

Categories

+
+ {categories.map((category) => ( + +
+ {category.icon && ( + + {category.icon} + + )} + + {category.nameEn} + +
+ + ))}
diff --git a/components/shared/adt-create/adt-create-form.tsx b/components/shared/adt-create/adt-create-form.tsx index bdacded..bcb9df0 100644 --- a/components/shared/adt-create/adt-create-form.tsx +++ b/components/shared/adt-create/adt-create-form.tsx @@ -1,19 +1,21 @@ 'use client'; -import { useState, useRef } from 'react'; +import { useState, useRef, useMemo, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { formAdtCreateSchema, type TFormAdtCreateValues } from './schemas'; import Image from 'next/image'; -import { Category } from '@prisma/client'; +import { Category, Country, City } from '@prisma/client'; import { useRouter } from 'next/navigation'; import toast from 'react-hot-toast'; interface CreateAdtFormProps { categories: Category[]; + countries: Country[]; + cities: City[]; } -export default function AdtCreateForm({ categories }: CreateAdtFormProps) { +export default function AdtCreateForm({ categories, countries, cities }: CreateAdtFormProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [preview, setPreview] = useState(null); const fileInputRef = useRef(null); @@ -29,11 +31,35 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { } = useForm({ resolver: zodResolver(formAdtCreateSchema), defaultValues: { - categoryIds: [], + categoryId: '', } }); - const selectedCategories = watch('categoryIds'); + const selectedCategory = watch('categoryId'); + const [selectedCountry, setSelectedCountry] = useState(null); + const [filteredCities, setFilteredCities] = useState([]); + const [selectedParentCategory, setSelectedParentCategory] = useState(null); + const [filteredSubCategories, setFilteredSubCategories] = useState([]); + + const parentCategories = useMemo(() => { + return categories.filter(cat => !cat.parentId); + }, [categories]); + + useEffect(() => { + if (selectedCountry) { + const countryCities = cities.filter(city => city.countryId === selectedCountry); + setFilteredCities(countryCities); + setValue('cityId', ''); + } + }, [selectedCountry, cities]); + + useEffect(() => { + if (selectedParentCategory) { + const subCategories = categories.filter(cat => cat.parentId === selectedParentCategory); + setFilteredSubCategories(subCategories); + setValue('categoryId', ''); + } + }, [selectedParentCategory, categories]); const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -72,28 +98,23 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { } toast.success("Adt created successfully") - // Здесь можно добавить уведомление об успешном создании const data_f = await response.json(); - // Редирект на страницу созданного объявления router.push(`/adt/${data_f.id}`); - // Обновляем кэш Next.js - // router.refresh(); } catch (error) { console.error('Error:', error); - // toast.error("Error creating adt: " + error) - // Здесь можно добавить обработку ошибок } finally { setIsSubmitting(false); } }; - const handleCategoryChange = (categoryId: number) => { - const currentCategories = watch('categoryIds') || []; - const updatedCategories = currentCategories.includes(categoryId) - ? currentCategories.filter(id => id !== categoryId) - : [...currentCategories, categoryId]; - setValue('categoryIds', updatedCategories); + const handleCountryChange = (countryId: string) => { + setSelectedCountry(countryId); + setValue('countryId', countryId); + }; + + const handleCategoryParentChange = (categoryId: string) => { + setSelectedParentCategory(categoryId); }; return ( @@ -144,40 +165,105 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { )}
-
- -
- {categories.map((category) => ( - - ))} +
+
+ +
- {errors.categoryIds && ( -

{errors.categoryIds.message}

+ + {selectedParentCategory && ( +
+ + + {errors.categoryId && ( +

{errors.categoryId.message}

+ )} +
+ )} +
+ +
+
+ + + {errors.countryId && ( +

{errors.countryId.message}

+ )} +
+ + {selectedCountry && ( +
+ + + {errors.cityId && ( +

{errors.cityId.message}

+ )} +
)}
-
diff --git a/components/shared/adt-create/schemas.ts b/components/shared/adt-create/schemas.ts index e00cd27..633b28b 100644 --- a/components/shared/adt-create/schemas.ts +++ b/components/shared/adt-create/schemas.ts @@ -9,14 +9,17 @@ export const formAdtCreateSchema = z.object({ .max(1000, 'Описание не может быть длиннее 1000 символов') .nullable(), price: z.string() - .min(1, 'Укажите цену') - .nullable(), - location: z.string() - .min(2, 'Укажите местоположение') - .max(100, 'Слишком длинное название местоположения') + // .transform((val) => (val ? parseFloat(val) : null)) .nullable(), + countryId: z.string().min(1, 'Выберите страну'), + cityId: z.string().min(1, 'Выберите город'), + address: z.string().nullable().optional(), + latitude: z.number().nullable().optional(), + longitude: z.number().nullable().optional(), + contactPhone: z.string().nullable().optional(), image: z.string().nullable().optional(), - categoryIds: z.array(z.number()).min(1, 'Выберите хотя бы одну категорию'), - }); + images: z.array(z.string()).optional(), + categoryId: z.string().min(1, 'Выберите категорию'), +}); export type TFormAdtCreateValues = z.infer diff --git a/components/shared/block-adts.tsx b/components/shared/block-adts.tsx index 4c0af76..c280376 100644 --- a/components/shared/block-adts.tsx +++ b/components/shared/block-adts.tsx @@ -3,15 +3,16 @@ // Импортируем необходимые компоненты и типы import { FC, useEffect, useState, useRef, useCallback } from 'react' import ListingCard from '../ListingCard' -import { Adt } from '@prisma/client' -import toast from 'react-hot-toast' +import { AdtWithRelations } from '@/@types/prisma' -// interface BlockAdtsProps {} +interface BlockAdtsProps { + category?: string +} -export const BlockAdts: FC = () => { +export const BlockAdts: FC = ({ category }) => { // Состояния для хранения объявлений и управления их отображением - const [adts, setAdts] = useState([]) // Массив объявлений + const [adts, setAdts] = useState([]) // Массив объявлений const [sortBy, setSortBy] = useState('new') // Тип сортировки const [isLoading, setIsLoading] = useState(true) // Флаг загрузки const [isLoadingMore, setIsLoadingMore] = useState(false) // Флаг загрузки дополнительных объявлений @@ -42,8 +43,10 @@ export const BlockAdts: FC = () => { setIsLoading(true) } + const categoryParam = category ? `&category=${category}` : ''; + // Запрашиваем данные с сервера - const response = await fetch(`/api/adt?page=${pageNum}&sort=${sortBy}`) + const response = await fetch(`/api/adt?page=${pageNum}&sort=${sortBy}${categoryParam}`) const { data: newAdts, meta } = await response.json() // Обновляем список объявлений @@ -136,7 +139,7 @@ export const BlockAdts: FC = () => { title={adt.title} image={String(adt.image)} price={String(adt.price)} - location={String(adt.location)} + location={String(adt.city.nameEn)} date={String(adt.createdAt)} id={String(adt.id)} /> diff --git a/components/shared/breadcrumbs-category.tsx b/components/shared/breadcrumbs-category.tsx new file mode 100644 index 0000000..c01c6e6 --- /dev/null +++ b/components/shared/breadcrumbs-category.tsx @@ -0,0 +1,36 @@ +import { Tag } from "lucide-react"; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; +import { Category } from "@prisma/client"; +import { prisma } from "@/prisma/prisma-client"; + +interface BreadcrumbsCategoryProps { + category?: Category +} + + +export const BreadcrumbsCategory = async ({ category }: BreadcrumbsCategoryProps) => { + const categ = await prisma.category.findFirst({ + where: { + id: String(category?.id) + }, + include: { + parent: true + } + }) + return ( +
+ + + + + {categ?.parent?.nameEn} + + + + {categ?.nameEn} + + + +
+ ); +} \ No newline at end of file diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..3bd4a81 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>