add create adt. login
This commit is contained in:
parent
2a90b6f2b0
commit
792956adbd
@ -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 (
|
||||
|
||||
<AdtCreateForm />
|
||||
<AdtCreateForm categories={categories} />
|
||||
|
||||
|
||||
// <main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
64
app/api/adt/route.ts
Normal file
64
app/api/adt/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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<Props> = ({onClose}) => {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
export default function AdtCreateForm({ categories }: CreateAdtFormProps) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(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<TFormAdtCreateValues>({
|
||||
// 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<TFormAdtCreateValues>({
|
||||
resolver: zodResolver(formAdtCreateSchema),
|
||||
defaultValues: {
|
||||
categoryIds: [],
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form className="flex flex-col gap-5" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="mr-2">
|
||||
<Title text="Создать объявление" size='md' className="font-bold" />
|
||||
<p className="text-gray-400">Заполните все поля для создания объявления</p>
|
||||
</div>
|
||||
</div>
|
||||
const selectedCategories = watch('categoryIds');
|
||||
|
||||
<FormInput name='title' label='Заголовок' required />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Категории</label>
|
||||
<select
|
||||
multiple
|
||||
{...form.register('categories')}
|
||||
className="w-full px-4 py-2 rounded-lg border border-gray-200"
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
<FormInput
|
||||
name='price'
|
||||
label='Цена'
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
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),
|
||||
});
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Описание</label>
|
||||
<textarea
|
||||
{...form.register('description')}
|
||||
className="w-full px-4 py-2 rounded-lg border border-gray-200"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при создании объявления');
|
||||
}
|
||||
|
||||
<FormInput
|
||||
name='image'
|
||||
label='Ссылка на изображение'
|
||||
required
|
||||
/>
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
<FormInput
|
||||
name='location'
|
||||
label='Местоположение'
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
loading={form.formState.isSubmitting}
|
||||
className="h-12 text-base"
|
||||
type='submit'
|
||||
>
|
||||
Создать объявление
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
const handleCategoryChange = (categoryId: number) => {
|
||||
const currentCategories = watch('categoryIds') || [];
|
||||
const updatedCategories = currentCategories.includes(categoryId)
|
||||
? currentCategories.filter(id => id !== categoryId)
|
||||
: [...currentCategories, categoryId];
|
||||
setValue('categoryIds', updatedCategories);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl mx-auto p-6 space-y-6">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
|
||||
Заголовок*
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
{...register('title')}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||
Описание
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
rows={4}
|
||||
{...register('description')}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="price" className="block text-sm font-medium text-gray-700">
|
||||
Цена
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="price"
|
||||
{...register('price')}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
placeholder="Например: 1000 ₽"
|
||||
/>
|
||||
{errors.price && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.price.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Категории*
|
||||
</label>
|
||||
<div className="mt-2 space-y-2">
|
||||
{categories.map((category) => (
|
||||
<label key={category.id} className="inline-flex items-center mr-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox h-4 w-4 text-indigo-600"
|
||||
onChange={() => handleCategoryChange(category.id)}
|
||||
checked={selectedCategories?.includes(category.id)}
|
||||
/>
|
||||
<span className="ml-2">{category.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{errors.categoryIds && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.categoryIds.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="location" className="block text-sm font-medium text-gray-700">
|
||||
Местоположение
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
{...register('location')}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
/>
|
||||
{errors.location && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.location.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Изображение
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
ref={fileInputRef}
|
||||
className="mt-1 block w-full text-sm text-gray-500
|
||||
file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-indigo-50 file:text-indigo-700
|
||||
hover:file:bg-indigo-100"
|
||||
/>
|
||||
{preview && (
|
||||
<div className="mt-2 relative h-48 w-48">
|
||||
<Image
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
fill
|
||||
className="object-cover rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400"
|
||||
>
|
||||
{isSubmitting ? 'Создание...' : 'Создать объявление'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<typeof formAdtCreateSchema>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user