add create adt. login

This commit is contained in:
Zikil 2024-11-22 00:15:56 +07:00
parent 2a90b6f2b0
commit 792956adbd
5 changed files with 295 additions and 136 deletions

View File

@ -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
View 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 }
);
}
}

View File

@ -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
} }

View File

@ -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();
const form = useForm<TFormAdtCreateValues>({
// resolver: zodResolver(formAdtCreateSchema),
defaultValues: { defaultValues: {
title: '', categoryIds: [],
categories: categories,
price: '0',
description: '',
image: '',
location: '',
} }
}) });
const selectedCategories = watch('categoryIds');
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);
}
};
const onSubmit = async (data: TFormAdtCreateValues) => { const onSubmit = async (data: TFormAdtCreateValues) => {
try { try {
await createAdt({ setIsSubmitting(true);
title: data.title,
price: data.price.toString(),
description: data.description,
image: data.image,
location: data.location,
// categories: data.categories,
}, categories)
// if (!resp?.ok) { const response = await fetch('/api/adt', {
// throw Error(); 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();
onClose?.()
} catch (error) { } catch (error) {
console.error('Error [LOGIN]', error) console.error('Error:', 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);
};
return ( return (
<FormProvider {...form}> <form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl mx-auto p-6 space-y-6">
<form className="flex flex-col gap-5" onSubmit={form.handleSubmit(onSubmit)}> <div>
<div className="flex justify-between items-center"> <label htmlFor="title" className="block text-sm font-medium text-gray-700">
<div className="mr-2"> Заголовок*
<Title text="Создать объявление" size='md' className="font-bold" /> </label>
<p className="text-gray-400">Заполните все поля для создания объявления</p> <input
</div> type="text"
</div> id="title"
{...register('title')}
<FormInput name='title' label='Заголовок' required /> className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
<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>
<FormInput
name='price'
label='Цена'
type="number"
required
/> />
{errors.title && (
<p className="mt-1 text-sm text-red-600">{errors.title.message}</p>
)}
</div>
<div className="flex flex-col gap-2"> <div>
<label className="text-sm font-medium">Описание</label> <label htmlFor="description" className="block text-sm font-medium text-gray-700">
Описание
</label>
<textarea <textarea
{...form.register('description')} id="description"
className="w-full px-4 py-2 rounded-lg border border-gray-200"
rows={4} 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>
<FormInput <div>
name='image' <label htmlFor="price" className="block text-sm font-medium text-gray-700">
label='Ссылка на изображение' Цена
required </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>
<FormInput <div>
name='location' <label className="block text-sm font-medium text-gray-700">
label='Местоположение' Категории*
required </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>
<Button <div>
loading={form.formState.isSubmitting} <label htmlFor="location" className="block text-sm font-medium text-gray-700">
className="h-12 text-base" Местоположение
type='submit' </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> </button>
</div>
</form> </form>
</FormProvider> );
)
} }

View File

@ -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>