From 53920d41ef82f670ff141bc1145bd14472bd3dd3 Mon Sep 17 00:00:00 2001 From: Zikil Date: Sat, 23 Nov 2024 20:43:10 +0700 Subject: [PATCH] toast, en, show number etc --- app/(root)/adt/[id]/page.tsx | 10 +- app/(root)/page.tsx | 36 +--- app/layout.tsx | 2 + components/Header.tsx | 16 +- components/ListingCard.tsx | 4 +- components/shared/add-button.tsx | 38 +++++ .../shared/adt-create/adt-create-form.tsx | 8 +- components/shared/adt-create/schemas.ts | 4 +- components/shared/block-adts.tsx | 158 ++++++++++++++++++ .../shared/modals/auth-modal/auth-modal.tsx | 11 +- .../modals/auth-modal/forms/login-form.tsx | 9 +- .../modals/auth-modal/forms/register-form.tsx | 13 +- components/shared/modals/show-number.tsx | 67 ++++---- components/shared/profile-form.tsx | 13 +- package-lock.json | 27 ++- package.json | 1 + prisma/constant.ts | 149 ++++++++++++++++- 17 files changed, 461 insertions(+), 105 deletions(-) create mode 100644 components/shared/add-button.tsx create mode 100644 components/shared/block-adts.tsx diff --git a/app/(root)/adt/[id]/page.tsx b/app/(root)/adt/[id]/page.tsx index 0b709e0..e61dbb5 100644 --- a/app/(root)/adt/[id]/page.tsx +++ b/app/(root)/adt/[id]/page.tsx @@ -6,13 +6,15 @@ import { MapPin, Calendar, Phone, MessageCircle, Share2, Flag, Heart } from 'luc 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'; type Params = Promise<{ id: string }> export default async function AdtPage(props: { params: Params }) { - // const [ openShowNumberModal, setOpenShowNumberModal ] = React.useState(false) const params = await props.params; + + const session = await getUserSession(); const adt = await prisma.adt.findFirst({ where: { id: Number(params.id), @@ -33,7 +35,6 @@ export default async function AdtPage(props: { params: Params }) { return ( <> - {/* setOpenShowNumberModal(false)} /> */}
@@ -91,13 +92,14 @@ export default async function AdtPage(props: { params: Params }) {
- + */} - - - + setOpenAuthModal(true)} />
{/* Desktop Navigation */}
- - - + + {/* + */}
diff --git a/components/shared/add-button.tsx b/components/shared/add-button.tsx new file mode 100644 index 0000000..92171b0 --- /dev/null +++ b/components/shared/add-button.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useState } from 'react'; +import { PlusCircle } from 'lucide-react'; +import Link from 'next/link'; +import { useSession } from 'next-auth/react'; +import { AuthModal } from './modals/auth-modal/auth-modal'; +import toast from 'react-hot-toast'; + +export default function AddButton() { + const [openAuthModal, setOpenAuthModal] = useState(false); + const { data: session } = useSession(); + + const handleClick = () => { + if (!session) { + toast.error("Authorization required") + setOpenAuthModal(true); + return; + } + }; + + return ( + <> + + + + + setOpenAuthModal(false)} + /> + + ); +} diff --git a/components/shared/adt-create/adt-create-form.tsx b/components/shared/adt-create/adt-create-form.tsx index 4dd0300..bdacded 100644 --- a/components/shared/adt-create/adt-create-form.tsx +++ b/components/shared/adt-create/adt-create-form.tsx @@ -7,6 +7,7 @@ import { formAdtCreateSchema, type TFormAdtCreateValues } from './schemas'; import Image from 'next/image'; import { Category } from '@prisma/client'; import { useRouter } from 'next/navigation'; +import toast from 'react-hot-toast'; interface CreateAdtFormProps { categories: Category[]; @@ -59,7 +60,9 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { }); if (!response.ok) { - throw new Error('Ошибка при создании объявления'); + const err = await response.text() + toast.error("Error creating adt: " + err) + throw new Error('Error creating adt'); } reset(); @@ -67,6 +70,8 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { if (fileInputRef.current) { fileInputRef.current.value = ''; } + + toast.success("Adt created successfully") // Здесь можно добавить уведомление об успешном создании const data_f = await response.json(); // Редирект на страницу созданного объявления @@ -76,6 +81,7 @@ export default function AdtCreateForm({ categories }: CreateAdtFormProps) { } catch (error) { console.error('Error:', error); + // toast.error("Error creating adt: " + error) // Здесь можно добавить обработку ошибок } finally { setIsSubmitting(false); diff --git a/components/shared/adt-create/schemas.ts b/components/shared/adt-create/schemas.ts index 0b593c7..e00cd27 100644 --- a/components/shared/adt-create/schemas.ts +++ b/components/shared/adt-create/schemas.ts @@ -2,10 +2,10 @@ import {z} from 'zod' export const formAdtCreateSchema = z.object({ title: z.string() - .min(5, 'Заголовок должен содержать минимум 5 символов') + .min(2, 'Заголовок должен содержать минимум 5 символов') .max(100, 'Заголовок не может быть длиннее 100 символов'), description: z.string() - .min(20, 'Описание должно содержать минимум 20 символов') + .min(10, 'Описание должно содержать минимум 20 символов') .max(1000, 'Описание не может быть длиннее 1000 символов') .nullable(), price: z.string() diff --git a/components/shared/block-adts.tsx b/components/shared/block-adts.tsx new file mode 100644 index 0000000..aa068f3 --- /dev/null +++ b/components/shared/block-adts.tsx @@ -0,0 +1,158 @@ +"use client" + +// Импортируем необходимые компоненты и типы +import { FC, useEffect, useState, useRef, useCallback } from 'react' +import ListingCard from '../ListingCard' +import { Adt } from '@prisma/client' +import toast from 'react-hot-toast' + +interface BlockAdtsProps {} + +export const BlockAdts: FC = () => { + + // Состояния для хранения объявлений и управления их отображением + const [adts, setAdts] = useState([]) // Массив объявлений + const [sortBy, setSortBy] = useState('new') // Тип сортировки + const [isLoading, setIsLoading] = useState(true) // Флаг загрузки + const [isLoadingMore, setIsLoadingMore] = useState(false) // Флаг загрузки дополнительных объявлений + const [page, setPage] = useState(1) // Текущая страница + const [hasMore, setHasMore] = useState(true) // Флаг наличия дополнительных объявлений + + // Создаем наблюдатель для бесконечной прокрутки + const observer = useRef() + const lastAdtElementRef = useCallback((node: HTMLDivElement) => { + if (isLoadingMore) return + if (observer.current) observer.current.disconnect() + observer.current = new IntersectionObserver(entries => { + // Если последний элемент виден и есть еще объявления - загружаем следующую порцию + if (entries[0].isIntersecting && hasMore) { + loadMore() + } + }) + if (node) observer.current.observe(node) + }, [isLoadingMore, hasMore]) + + // Функция загрузки объявлений + const loadAdts = async (pageNum: number, isLoadMore = false) => { + try { + // Устанавливаем соответствующий флаг загрузки + if (isLoadMore) { + setIsLoadingMore(true) + } else { + setIsLoading(true) + } + + // Запрашиваем данные с сервера + const response = await fetch(`/api/adt?page=${pageNum}&sort=${sortBy}`) + const { data: newAdts, meta } = await response.json() + + // Обновляем список объявлений + if (pageNum === 1) { + setAdts(newAdts) + } else { + setAdts(prev => [...prev, ...newAdts]) + } + + // Проверяем, есть ли еще объявления для загрузки + setHasMore(newAdts.length > 0 && pageNum < meta.totalPages) + } catch (error) { + console.error('Ошибка загрузки объявлений:', error) + } finally { + setIsLoading(false) + setIsLoadingMore(false) + } + } + + // Загружаем объявления при изменении способа сортировки + useEffect(() => { + loadAdts(1) + }, [sortBy]) + + // Обработчик изменения сортировки + const handleSort = (event: React.ChangeEvent) => { + setSortBy(event.target.value) + setAdts([]) + setPage(1) + setHasMore(true) + } + + // Функция загрузки дополнительных объявлений + const loadMore = () => { + if (!isLoadingMore && hasMore) { + const nextPage = page + 1 + setPage(nextPage) + loadAdts(nextPage, true) + } + } + + // Компонент-заглушка для отображения во время загрузки + const SkeletonCard = () => ( +
+
+
+
+
+
+ ) + + return ( +
+ {/* Заголовок и селектор сортировки */} +
+

Listings

+
+ +
+
+ + {/* Сетка объявлений */} +
+ {isLoading ? ( + // Отображаем заглушки во время начальной загрузки + <> + + + + + + + + ) : ( + // Отображаем список объявлений + adts.map((adt, index) => ( +
+ +
+ )) + )} +
+ + {/* Отображаем заглушки при загрузке дополнительных объявлений */} + {isLoadingMore && ( +
+ + + +
+ )} +
+ ) +} \ No newline at end of file diff --git a/components/shared/modals/auth-modal/auth-modal.tsx b/components/shared/modals/auth-modal/auth-modal.tsx index f52f8ea..6f16a50 100644 --- a/components/shared/modals/auth-modal/auth-modal.tsx +++ b/components/shared/modals/auth-modal/auth-modal.tsx @@ -19,6 +19,7 @@ export const AuthModal: React.FC = ({ open, onClose}) => { const handleClose = () => { onClose() + setType('login') } return ( @@ -31,7 +32,7 @@ export const AuthModal: React.FC = ({ open, onClose}) => {
- + */} - + */}
diff --git a/components/shared/modals/auth-modal/forms/login-form.tsx b/components/shared/modals/auth-modal/forms/login-form.tsx index 116df32..ad8ae5b 100644 --- a/components/shared/modals/auth-modal/forms/login-form.tsx +++ b/components/shared/modals/auth-modal/forms/login-form.tsx @@ -6,6 +6,7 @@ import { Title } from "@/components/shared/title"; import { FormInput } from "@/components/shared/form"; import { Button } from "@/components/ui/button"; import { signIn } from "next-auth/react"; +import toast from "react-hot-toast"; interface Props { onClose?: VoidFunction; @@ -28,11 +29,13 @@ export const LoginForm: React.FC = ({onClose}) => { }) if (!resp?.ok) { + toast.error("Incorrect login or password") throw Error(); } onClose?.() } catch (error) { + toast.error("Error login") console.error('Error [LOGIN]', error) } } @@ -42,8 +45,8 @@ export const LoginForm: React.FC = ({onClose}) => {
- - <p className="text-gray-400">Введите свою почту, чтобы войти</p> + <Title text="Login" size='md' className="font-bold" /> + <p className="text-gray-400">Enter your email to login</p> </div> {/* <img src="..." /> */} </div> @@ -52,7 +55,7 @@ export const LoginForm: React.FC<Props> = ({onClose}) => { <Button loading={form.formState.isSubmitting} className="h-12 text-base" type='submit' > { - 'Войти' + 'Login' } </Button> </form> diff --git a/components/shared/modals/auth-modal/forms/register-form.tsx b/components/shared/modals/auth-modal/forms/register-form.tsx index 5bd67d4..74840e9 100644 --- a/components/shared/modals/auth-modal/forms/register-form.tsx +++ b/components/shared/modals/auth-modal/forms/register-form.tsx @@ -7,6 +7,7 @@ import { FormInput } from "@/components/shared/form"; import { Button } from "@/components/ui/button"; import { signIn } from "next-auth/react"; import { registerUser } from "@/app/actions"; +import toast from "react-hot-toast"; interface Props { onClose?: VoidFunction; @@ -35,22 +36,24 @@ export const RegisterForm: React.FC<Props> = ({onClose}) => { // throw Error(); // } + toast.success("User registered successfully. Login") onClose?.() } catch (error) { - console.error('Error [LOGIN]', error) + toast.error("Error register") + console.error('Error [REGISTER]', error) } } return ( <FormProvider {...form}> <form className="flex flex-col gap-5" onSubmit={form.handleSubmit(onSubmit)}> - {/* <div className="fles justify-between items-center"> + <div className="fles justify-between items-center"> <div className="mr-2"> - <Title text="Вход в аккаунт" size='md' className="font-bold" /> - <p className="text-gray-400">Введите свою почту, чтобы войти</p> + <Title text="Sign Up" size='md' className="font-bold" /> + <p className="text-gray-400">Enter your name and email to sign up</p> </div> - </div> */} + </div> <FormInput name='name' label='Name' required /> <FormInput name='email' label='E-Mail' required /> <FormInput name='password' label='Password' required /> diff --git a/components/shared/modals/show-number.tsx b/components/shared/modals/show-number.tsx index 09c8cbf..060bc35 100644 --- a/components/shared/modals/show-number.tsx +++ b/components/shared/modals/show-number.tsx @@ -1,45 +1,46 @@ -import { Dialog } from "@/components/ui/dialog"; +'use client' + +import { Dialog, DialogContent } from "@/components/ui/dialog"; import { getUserSession } from "@/lib/get-user-session"; import React from "react"; +import { Phone } from "lucide-react"; interface Props { - open: boolean; - onClose: () => void; + phoneNumber: string; + session: any; } -export const ShowNumberModal: React.FC<Props> = ({ open, onClose}) => { - - // const [ session, setSession ] = React.useState(null) +export const ShowNumberModal: React.FC<Props> = ({ phoneNumber, session }) => { + const [open, setOpen] = React.useState(false); + // const [session, setSession] = React.useState<any>(null); // React.useEffect(() => { // const getSession = async () => { - // const userSession = await getUserSession() - // setSession(userSession) - // } - // getSession() - // }, []) - // const session = await getUserSession() - const handleClose = () => { - onClose() - } + // const userSession = await getUserSession(); + // setSession(userSession); + // }; + // getSession(); + // }, []); return ( - <Dialog open={open} onOpenChange={handleClose}> - {/* <DialogContent className="w-[450px] bg-white p-10"> */} - {/* { - // session && ( - // <h1 className="text-2xl font-bold mb-4">892348924823</h1> - // ) - } */} + <> + <button + onClick={() => setOpen(true)} + className="w-full flex items-center justify-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700" + > + <Phone className="h-5 w-5" /> + <span>Show Phone Number</span> + </button> - {/* <h1 className="text-2xl font-bold mb-4">892348924823</h1> */} - <hr /> - <div className="flex gap-2"> - - - </div> - {/* </DialogContent> */} - - </Dialog> - ) -} \ No newline at end of file + <Dialog open={open} onOpenChange={setOpen}> + <DialogContent className="w-[450px] bg-white p-10"> + {session ? ( + <h1 className="text-2xl font-bold mb-4">{phoneNumber}</h1> + ) : ( + <h2 className="text-xl text-center">Please login to see the phone number</h2> + )} + </DialogContent> + </Dialog> + </> + ); +}; \ No newline at end of file diff --git a/components/shared/profile-form.tsx b/components/shared/profile-form.tsx index eee7f76..39097b5 100644 --- a/components/shared/profile-form.tsx +++ b/components/shared/profile-form.tsx @@ -11,6 +11,7 @@ import { Title } from "./title"; import { FormInput } from "./form/form-input"; import { Button } from "../ui/button"; import { updateUserInfo } from "@/app/actions"; +import toast from "react-hot-toast"; interface Props { data: User @@ -35,13 +36,13 @@ export const ProfileForm: React.FC<Props> = ({ data }) => { password: formData.password, }); - // toast.error('Данные обновлены 📝', { - // icon: '✅', - // }); + toast.success('Data updated 📝', { + icon: '✅', + }); } catch (error) { - // return toast.error('Ошибка при обновлении данных', { - // icon: '❌', - // }); + return toast.error('Error updating data', { + icon: '❌', + }); console.log('error update', error) } }; diff --git a/package-lock.json b/package-lock.json index eb13f5e..dbc0483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "^7.53.2", + "react-hot-toast": "^2.4.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -2492,7 +2493,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3795,6 +3795,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5659,6 +5668,22 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "license": "MIT", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index faa8eba..4e109fd 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "^7.53.2", + "react-hot-toast": "^2.4.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" diff --git a/prisma/constant.ts b/prisma/constant.ts index 0438d89..e107f7c 100644 --- a/prisma/constant.ts +++ b/prisma/constant.ts @@ -97,5 +97,152 @@ export const categories = [ image: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=800", // date: "Posted 7 days ago" userId: 1 + }, + { + title: "2020 Tesla Model 3 Long Range", + price: "$41,999", + location: "San Francisco, CA", + image: "https://images.unsplash.com/photo-1560958089-b8a1929cea89?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Винтажный велосипед", + price: "$450", + location: "Portland, OR", + image: "https://images.unsplash.com/photo-1485965120184-e220f721d03e?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Профессиональный набор для рисования", + price: "$299", + location: "Seattle, WA", + image: "https://images.unsplash.com/photo-1513364776144-60967b0f800f?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Кожаный диван", + price: "$1,299", + location: "Austin, TX", + image: "https://images.unsplash.com/photo-1493663284031-b7e3aefcae8e?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Коллекционные виниловые пластинки", + price: "$599", + location: "Nashville, TN", + image: "https://images.unsplash.com/photo-1539375665275-f9de415ef9ac?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Беговая дорожка NordicTrack", + price: "$899", + location: "Phoenix, AZ", + image: "https://images.unsplash.com/photo-1540497077202-7c8a3999166f?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Набор кухонной посуды", + price: "$399", + location: "Boston, MA", + image: "https://images.unsplash.com/photo-1556911220-bff31c812dba?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Горный велосипед Trek", + price: "$789", + location: "Denver, CO", + image: "https://images.unsplash.com/photo-1576435728678-68d0fbf94e91?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Игровая приставка PS5", + price: "$499", + location: "Las Vegas, NV", + image: "https://images.unsplash.com/photo-1606144042614-b2417e99c4e3?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Антикварный комод", + price: "$850", + location: "Charleston, SC", + image: "https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Дрон DJI Mavic Air 2", + price: "$799", + location: "San Diego, CA", + image: "https://images.unsplash.com/photo-1579829366248-204fe8413f31?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Электрогитара Fender", + price: "$1,199", + location: "Atlanta, GA", + image: "https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Кофемашина Breville", + price: "$599", + location: "Seattle, WA", + image: "https://images.unsplash.com/photo-1517080317843-0b926a6ce350?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Умные часы Apple Watch", + price: "$349", + location: "Houston, TX", + image: "https://images.unsplash.com/photo-1546868871-7041f2a55e12?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Винтажная печатная машинка", + price: "$299", + location: "Portland, ME", + image: "https://images.unsplash.com/photo-1558522195-e1201b090344?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Телескоп Celestron", + price: "$899", + location: "Tucson, AZ", + image: "https://images.unsplash.com/photo-1566004100631-35d015d6a491?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Набор для йоги", + price: "$89", + location: "Santa Monica, CA", + image: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Винный холодильник", + price: "$599", + location: "Napa Valley, CA", + image: "https://images.unsplash.com/photo-1585703900468-13c7a978ad86?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Электросамокат Xiaomi", + price: "$399", + location: "Chicago, IL", + image: "https://images.unsplash.com/photo-1589999562311-56254d5d4325?auto=format&fit=crop&w=800", + userId: 1 + }, + { + title: "Коллекционные монеты", + price: "$2,999", + location: "Washington, DC", + image: "https://images.unsplash.com/photo-1566321343730-237ec35e53f3?auto=format&fit=crop&w=800", + userId: 2 + }, + { + title: "Профессиональный микрофон", + price: "$299", + location: "Nashville, TN", + image: "https://images.unsplash.com/photo-1590602847861-f357a9332bbc?auto=format&fit=crop&w=800", + userId: 1 } - ]; \ No newline at end of file + ];