add create adt. login
This commit is contained in:
parent
2a90b6f2b0
commit
792956adbd
@ -1,13 +1,18 @@
|
|||||||
// 'use client'
|
// 'use client'
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ImagePlus, X } from 'lucide-react';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { AdtCreateForm } from '@/components/shared/adt-create/adt-create-form';
|
import AdtCreateForm from '@/components/shared/adt-create/adt-create-form';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default async function CreateListing() {
|
||||||
|
const categories = await prisma.category.findMany();
|
||||||
|
|
||||||
|
|
||||||
export default function CreateListing() {
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<AdtCreateForm />
|
<AdtCreateForm categories={categories} />
|
||||||
|
|
||||||
|
|
||||||
// <main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
// <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 }) {
|
session({ session, token }) {
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
session.user.id = token.id,
|
session.user.id = token.id,
|
||||||
|
session.user.email = token.email,
|
||||||
session.user.role = token.role
|
session.user.role = token.role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,133 +1,217 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import { useState, useRef } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { formAdtCreateSchema, TFormAdtCreateValues } from "./schemas";
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Title } from "@/components/shared/title";
|
import { formAdtCreateSchema, type TFormAdtCreateValues } from './schemas';
|
||||||
import { FormInput } from "@/components/shared/form";
|
import Image from 'next/image';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Category } from '@prisma/client';
|
||||||
import { createAdt } from "@/app/actions";
|
import { useRouter } from 'next/navigation';
|
||||||
import { prisma } from "@/prisma/prisma-client";
|
|
||||||
import { getUserSession } from "@/lib/get-user-session";
|
|
||||||
import { Category } from "@prisma/client";
|
|
||||||
|
|
||||||
interface Props {
|
interface CreateAdtFormProps {
|
||||||
onClose?: VoidFunction;
|
categories: Category[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdtCreateForm: React.FC<Props> = ({onClose}) => {
|
export default function AdtCreateForm({ categories }: CreateAdtFormProps) {
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
const fetchCategories = async () => {
|
register,
|
||||||
const categoriesData = await fetch('/api/category').then(res => res.json());
|
handleSubmit,
|
||||||
setCategories(categoriesData);
|
formState: { errors },
|
||||||
};
|
reset,
|
||||||
|
setValue,
|
||||||
fetchCategories();
|
watch
|
||||||
}, []);
|
} = useForm<TFormAdtCreateValues>({
|
||||||
|
resolver: zodResolver(formAdtCreateSchema),
|
||||||
// const currentUser = await getUserSession();
|
defaultValues: {
|
||||||
|
categoryIds: [],
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const selectedCategories = watch('categoryIds');
|
||||||
<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>
|
|
||||||
|
|
||||||
<FormInput name='title' label='Заголовок' required />
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
const onSubmit = async (data: TFormAdtCreateValues) => {
|
||||||
<label className="text-sm font-medium">Категории</label>
|
try {
|
||||||
<select
|
setIsSubmitting(true);
|
||||||
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>
|
|
||||||
|
|
||||||
<FormInput
|
const response = await fetch('/api/adt', {
|
||||||
name='price'
|
method: 'POST',
|
||||||
label='Цена'
|
headers: {
|
||||||
type="number"
|
'Content-Type': 'application/json',
|
||||||
required
|
},
|
||||||
/>
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
if (!response.ok) {
|
||||||
<label className="text-sm font-medium">Описание</label>
|
throw new Error('Ошибка при создании объявления');
|
||||||
<textarea
|
}
|
||||||
{...form.register('description')}
|
|
||||||
className="w-full px-4 py-2 rounded-lg border border-gray-200"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormInput
|
reset();
|
||||||
name='image'
|
setPreview(null);
|
||||||
label='Ссылка на изображение'
|
if (fileInputRef.current) {
|
||||||
required
|
fileInputRef.current.value = '';
|
||||||
/>
|
}
|
||||||
|
// Здесь можно добавить уведомление об успешном создании
|
||||||
|
const data_f = await response.json();
|
||||||
|
// Редирект на страницу созданного объявления
|
||||||
|
router.push(`/adt/${data_f.id}`);
|
||||||
|
// Обновляем кэш Next.js
|
||||||
|
// router.refresh();
|
||||||
|
|
||||||
<FormInput
|
} catch (error) {
|
||||||
name='location'
|
console.error('Error:', error);
|
||||||
label='Местоположение'
|
// Здесь можно добавить обработку ошибок
|
||||||
required
|
} finally {
|
||||||
/>
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<Button
|
const handleCategoryChange = (categoryId: number) => {
|
||||||
loading={form.formState.isSubmitting}
|
const currentCategories = watch('categoryIds') || [];
|
||||||
className="h-12 text-base"
|
const updatedCategories = currentCategories.includes(categoryId)
|
||||||
type='submit'
|
? currentCategories.filter(id => id !== categoryId)
|
||||||
>
|
: [...currentCategories, categoryId];
|
||||||
Создать объявление
|
setValue('categoryIds', updatedCategories);
|
||||||
</Button>
|
};
|
||||||
</form>
|
|
||||||
</FormProvider>
|
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'
|
import {z} from 'zod'
|
||||||
|
|
||||||
export const formAdtCreateSchema = z.object({
|
export const formAdtCreateSchema = z.object({
|
||||||
title: z.string().min(1, {message: 'заголовок обязателен'}),
|
title: z.string()
|
||||||
categories: z.array(z.object({
|
.min(5, 'Заголовок должен содержать минимум 5 символов')
|
||||||
id: z.number(),
|
.max(100, 'Заголовок не может быть длиннее 100 символов'),
|
||||||
name: z.string()
|
description: z.string()
|
||||||
})).min(1, {message: 'выберите хотя бы одну категорию'}),
|
.min(20, 'Описание должно содержать минимум 20 символов')
|
||||||
price: z.string().min(0, {message: 'цена должна быть положительным числом'}),
|
.max(1000, 'Описание не может быть длиннее 1000 символов')
|
||||||
description: z.string().min(1, {message: 'описание обязательно'}),
|
.nullable(),
|
||||||
image: z.string().url({message: 'неверный формат URL'}),
|
price: z.string()
|
||||||
location: z.string().min(1, {message: 'местоположение обязательно'}),
|
.min(1, 'Укажите цену')
|
||||||
// createdAt: z.date().default(new Date()),
|
.nullable(),
|
||||||
// userId: z.string().uuid({message: 'неверный формат UUID'}),
|
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>
|
export type TFormAdtCreateValues = z.infer<typeof formAdtCreateSchema>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user