diff --git a/app/(root)/adt/create/page.tsx b/app/(root)/adt/create/page.tsx index db0da10..f05ca20 100644 --- a/app/(root)/adt/create/page.tsx +++ b/app/(root)/adt/create/page.tsx @@ -1,13 +1,18 @@ // 'use client' import React from 'react'; -import { ImagePlus, X } from 'lucide-react'; -import { AdtCreateForm } from '@/components/shared/adt-create/adt-create-form'; +import { PrismaClient } from '@prisma/client'; +import AdtCreateForm from '@/components/shared/adt-create/adt-create-form'; -export default function CreateListing() { +const prisma = new PrismaClient(); + +export default async function CreateListing() { + const categories = await prisma.category.findMany(); + + return ( - + // diff --git a/app/api/adt/route.ts b/app/api/adt/route.ts new file mode 100644 index 0000000..875e805 --- /dev/null +++ b/app/api/adt/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from 'next/server'; +import { PrismaClient } from '@prisma/client'; +import { formAdtCreateSchema } from '@/components/shared/adt-create/schemas'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '../auth/[...nextauth]/route'; +import { getUserSession } from '@/lib/get-user-session'; + +const prisma = new PrismaClient(); + +export async function POST(request: Request) { + try { + const session = await getUserSession(); + + if (!session) { + return NextResponse.json( + { error: 'Необходима авторизация' }, + { status: 401 } + ); + } + + const body = await request.json(); + + // Валидация данных + const validatedData = formAdtCreateSchema.parse(body); + + // Получаем пользователя + const user = await prisma.user.findUnique({ + where: { id: Number(session.id) } + }); + + if (!user) { + return NextResponse.json( + { error: 'Пользователь не найден' }, + { status: 404 } + ); + } + + // Создание объявления + const adt = await prisma.adt.create({ + data: { + title: validatedData.title, + description: validatedData.description, + price: validatedData.price, + location: validatedData.location, + image: validatedData.image, + userId: user.id, + categories: { + connect: validatedData.categoryIds.map(id => ({ id })) + } + }, + include: { + categories: true + } + }); + + return NextResponse.json(adt, { status: 201 }); + } catch (error) { + console.error('Error creating adt:', error); + return NextResponse.json( + { error: 'Ошибка при создании объявления' }, + { status: 400 } + ); + } +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 7108fb1..e9f0441 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -140,6 +140,7 @@ export const authOptions: AuthOptions = { session({ session, token }) { if (session?.user) { session.user.id = token.id, + session.user.email = token.email, session.user.role = token.role } diff --git a/components/shared/adt-create/adt-create-form.tsx b/components/shared/adt-create/adt-create-form.tsx index e7ffb87..4dd0300 100644 --- a/components/shared/adt-create/adt-create-form.tsx +++ b/components/shared/adt-create/adt-create-form.tsx @@ -1,133 +1,217 @@ -'use client' +'use client'; -import React, { useEffect, useState } from "react"; -import { FormProvider, useForm } from 'react-hook-form'; -import { formAdtCreateSchema, TFormAdtCreateValues } from "./schemas"; +import { useState, useRef } from 'react'; +import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Title } from "@/components/shared/title"; -import { FormInput } from "@/components/shared/form"; -import { Button } from "@/components/ui/button"; -import { createAdt } from "@/app/actions"; -import { prisma } from "@/prisma/prisma-client"; -import { getUserSession } from "@/lib/get-user-session"; -import { Category } from "@prisma/client"; +import { formAdtCreateSchema, type TFormAdtCreateValues } from './schemas'; +import Image from 'next/image'; +import { Category } from '@prisma/client'; +import { useRouter } from 'next/navigation'; -interface Props { - onClose?: VoidFunction; +interface CreateAdtFormProps { + categories: Category[]; } -export const AdtCreateForm: React.FC = ({onClose}) => { - const [categories, setCategories] = useState([]); +export default function AdtCreateForm({ categories }: CreateAdtFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [preview, setPreview] = useState(null); + const fileInputRef = useRef(null); + const router = useRouter(); - useEffect(() => { - const fetchCategories = async () => { - const categoriesData = await fetch('/api/category').then(res => res.json()); - setCategories(categoriesData); - }; - - fetchCategories(); - }, []); - - // const currentUser = await getUserSession(); - - const form = useForm({ - // resolver: zodResolver(formAdtCreateSchema), - defaultValues: { - title: '', - categories: categories, - price: '0', - description: '', - image: '', - location: '', - } - }) - - const onSubmit = async (data: TFormAdtCreateValues) => { - try { - await createAdt({ - title: data.title, - price: data.price.toString(), - description: data.description, - image: data.image, - location: data.location, - // categories: data.categories, - }, categories) - - // if (!resp?.ok) { - // throw Error(); - // } - - - - onClose?.() - } catch (error) { - console.error('Error [LOGIN]', error) - } + const { + register, + handleSubmit, + formState: { errors }, + reset, + setValue, + watch + } = useForm({ + resolver: zodResolver(formAdtCreateSchema), + defaultValues: { + categoryIds: [], } + }); - return ( - - - - - - Заполните все поля для создания объявления - - + const selectedCategories = watch('categoryIds'); - - - - Категории - - {categories.map((category) => ( - - {category.name} - - ))} - - + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result as string); + setValue('image', reader.result as string); + }; + reader.readAsDataURL(file); + } + }; - + const onSubmit = async (data: TFormAdtCreateValues) => { + try { + setIsSubmitting(true); + + const response = await fetch('/api/adt', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); - - Описание - - + if (!response.ok) { + throw new Error('Ошибка при создании объявления'); + } - + reset(); + setPreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + // Здесь можно добавить уведомление об успешном создании + const data_f = await response.json(); + // Редирект на страницу созданного объявления + router.push(`/adt/${data_f.id}`); + // Обновляем кэш Next.js + // router.refresh(); + + } catch (error) { + console.error('Error:', error); + // Здесь можно добавить обработку ошибок + } finally { + setIsSubmitting(false); + } + }; - - - - Создать объявление - - - - ) -} \ No newline at end of file + const handleCategoryChange = (categoryId: number) => { + const currentCategories = watch('categoryIds') || []; + const updatedCategories = currentCategories.includes(categoryId) + ? currentCategories.filter(id => id !== categoryId) + : [...currentCategories, categoryId]; + setValue('categoryIds', updatedCategories); + }; + + return ( + + + + Заголовок* + + + {errors.title && ( + {errors.title.message} + )} + + + + + Описание + + + {errors.description && ( + {errors.description.message} + )} + + + + + Цена + + + {errors.price && ( + {errors.price.message} + )} + + + + + Категории* + + + {categories.map((category) => ( + + handleCategoryChange(category.id)} + checked={selectedCategories?.includes(category.id)} + /> + {category.name} + + ))} + + {errors.categoryIds && ( + {errors.categoryIds.message} + )} + + + + + Местоположение + + + {errors.location && ( + {errors.location.message} + )} + + + + + Изображение + + + {preview && ( + + + + )} + + + + + {isSubmitting ? 'Создание...' : 'Создать объявление'} + + + + ); +} diff --git a/components/shared/adt-create/schemas.ts b/components/shared/adt-create/schemas.ts index 0204aa4..0b593c7 100644 --- a/components/shared/adt-create/schemas.ts +++ b/components/shared/adt-create/schemas.ts @@ -1,17 +1,22 @@ import {z} from 'zod' export const formAdtCreateSchema = z.object({ - title: z.string().min(1, {message: 'заголовок обязателен'}), - categories: z.array(z.object({ - id: z.number(), - name: z.string() - })).min(1, {message: 'выберите хотя бы одну категорию'}), - price: z.string().min(0, {message: 'цена должна быть положительным числом'}), - description: z.string().min(1, {message: 'описание обязательно'}), - image: z.string().url({message: 'неверный формат URL'}), - location: z.string().min(1, {message: 'местоположение обязательно'}), - // createdAt: z.date().default(new Date()), - // userId: z.string().uuid({message: 'неверный формат UUID'}), -}); + title: z.string() + .min(5, 'Заголовок должен содержать минимум 5 символов') + .max(100, 'Заголовок не может быть длиннее 100 символов'), + description: z.string() + .min(20, 'Описание должно содержать минимум 20 символов') + .max(1000, 'Описание не может быть длиннее 1000 символов') + .nullable(), + price: z.string() + .min(1, 'Укажите цену') + .nullable(), + location: z.string() + .min(2, 'Укажите местоположение') + .max(100, 'Слишком длинное название местоположения') + .nullable(), + image: z.string().nullable().optional(), + categoryIds: z.array(z.number()).min(1, 'Выберите хотя бы одну категорию'), + }); export type TFormAdtCreateValues = z.infer
Заполните все поля для создания объявления
{errors.title.message}
{errors.description.message}
{errors.price.message}
{errors.categoryIds.message}
{errors.location.message}