ui popup lead
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { SearchCardProps } from '@/app/types'
|
import { SearchCardProps } from '@/app/types'
|
||||||
import noPhoto from '../../../../public/images/noPhoto.png'
|
import noPhoto from '../../../../public/images/noPhoto.png'
|
||||||
|
import LeadPopup from '@/components/popups/LeadPopup'
|
||||||
|
import useUserStore from '@/app/store/userStore'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
const formatDateTime = (dateTimeString: string): string => {
|
const formatDateTime = (dateTimeString: string): string => {
|
||||||
const date = new Date(dateTimeString)
|
const date = new Date(dateTimeString)
|
||||||
@@ -41,6 +46,18 @@ const SearchCard = ({
|
|||||||
country_from_icon,
|
country_from_icon,
|
||||||
country_to_icon,
|
country_to_icon,
|
||||||
}: SearchCardProps) => {
|
}: SearchCardProps) => {
|
||||||
|
const [isLeadPopupOpen, setIsLeadPopupOpen] = useState(false)
|
||||||
|
const { isAuthenticated } = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleLeadClick = () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsLeadPopupOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
const getUserRequestStyles = () => {
|
const getUserRequestStyles = () => {
|
||||||
if (owner_type === 'customer') {
|
if (owner_type === 'customer') {
|
||||||
return 'text-[#065bff]'
|
return 'text-[#065bff]'
|
||||||
@@ -88,6 +105,7 @@ const SearchCard = ({
|
|||||||
<Button
|
<Button
|
||||||
text="Откликнуться"
|
text="Откликнуться"
|
||||||
className="bg-orange hover:bg-orange/80 cursor-pointer px-10 py-3 text-base font-semibold text-white transition-colors"
|
className="bg-orange hover:bg-orange/80 cursor-pointer px-10 py-3 text-base font-semibold text-white transition-colors"
|
||||||
|
onClick={handleLeadClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,7 +222,7 @@ const SearchCard = ({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className={`text-sm font-semibold ${getUserRequestStyles()}`}>{userRequest}</div>
|
<div className={`text-sm font-semibold ${getUserRequestStyles()}`}>{userRequest}</div>
|
||||||
<div className="text-sm font-semibold">
|
<div className="text-sm font-semibold">
|
||||||
Тип посылки: <span className="text-orange">{formatted_cargo_type}</span>
|
<span className="text-orange">{formatted_cargo_type}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 mb-2 flex flex-row items-center justify-between gap-3">
|
<div className="mt-5 mb-2 flex flex-row items-center justify-between gap-3">
|
||||||
@@ -243,7 +261,7 @@ const SearchCard = ({
|
|||||||
<div className="flex flex-1 flex-col justify-between">
|
<div className="flex flex-1 flex-col justify-between">
|
||||||
<div className="-mt-[14px] flex items-center">
|
<div className="-mt-[14px] flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src={`/images/flags/${from_country_name.toLowerCase()}.png`}
|
src={country_to_icon}
|
||||||
width={26}
|
width={26}
|
||||||
height={13}
|
height={13}
|
||||||
alt={from_country_name.substring(0, 3)}
|
alt={from_country_name.substring(0, 3)}
|
||||||
@@ -267,8 +285,8 @@ const SearchCard = ({
|
|||||||
className="h-[15px] w-[15px] object-contain"
|
className="h-[15px] w-[15px] object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2 h-[2px] w-[165px] bg-gray-200" />
|
<div className="my-2 h-[2px] w-[228px] bg-gray-200" />
|
||||||
<div className="text-sm">Дата доставки: {formatted_arrival}</div>
|
<div className="text-sm">Дата доставки: {formatDateTime(formatted_arrival)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="-mb-[14px] flex flex-col">
|
<div className="-mb-[14px] flex flex-col">
|
||||||
@@ -280,7 +298,7 @@ const SearchCard = ({
|
|||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Image
|
<Image
|
||||||
src={`/images/flags/${to_country_name.toLowerCase()}.png`}
|
src={country_to_icon}
|
||||||
width={26}
|
width={26}
|
||||||
height={13}
|
height={13}
|
||||||
alt={to_country_name.substring(0, 3)}
|
alt={to_country_name.substring(0, 3)}
|
||||||
@@ -301,8 +319,24 @@ const SearchCard = ({
|
|||||||
<span className="text-sm font-semibold">{formatDateTime(formatted_arrival)}</span>
|
<span className="text-sm font-semibold">{formatDateTime(formatted_arrival)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="mt-8">
|
||||||
|
<Button
|
||||||
|
text="Откликнуться"
|
||||||
|
className="bg-orange hover:bg-orange/80 w-full cursor-pointer py-3 text-base font-semibold text-white transition-colors"
|
||||||
|
onClick={handleLeadClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LeadPopup
|
||||||
|
id={id}
|
||||||
|
isOpen={isLeadPopupOpen}
|
||||||
|
onClose={() => setIsLeadPopupOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsLeadPopupOpen(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import AddressSelector from '@/components/AddressSelector'
|
|||||||
import SearchCard from '@/app/(urls)/search/components/SearchCard'
|
import SearchCard from '@/app/(urls)/search/components/SearchCard'
|
||||||
import FAQ from '@/components/FAQ'
|
import FAQ from '@/components/FAQ'
|
||||||
import { routes } from '@/app/constants'
|
import { routes } from '@/app/constants'
|
||||||
import Button from '@/components/ui/Button'
|
|
||||||
import News from '@/components/News'
|
import News from '@/components/News'
|
||||||
import { getFAQs } from '@/lib/main/fetchFAQ'
|
import { getFAQs } from '@/lib/main/fetchFAQ'
|
||||||
import { getNews } from '@/lib/main/fetchNews'
|
import { getNews } from '@/lib/main/fetchNews'
|
||||||
@@ -71,7 +70,6 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* первые пять серч карточек -- бекенд??? */}
|
|
||||||
<div className="mx-auto w-full max-w-[1250px]">
|
<div className="mx-auto w-full max-w-[1250px]">
|
||||||
<div className="grid w-full grid-cols-1 gap-4">
|
<div className="grid w-full grid-cols-1 gap-4">
|
||||||
{Array.isArray(latestRoutes) && latestRoutes.length > 0 ? (
|
{Array.isArray(latestRoutes) && latestRoutes.length > 0 ? (
|
||||||
@@ -81,10 +79,12 @@ export default async function Home() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center py-4">
|
<div className="flex justify-center py-4">
|
||||||
<Button
|
<Link
|
||||||
text="Разместить объявление"
|
href="/account/create-as-sender"
|
||||||
className="bg-orange rounded-lg px-6 py-3 text-xl font-semibold text-white"
|
className="bg-orange rounded-lg px-6 py-3 text-xl font-semibold text-white"
|
||||||
/>
|
>
|
||||||
|
Разместить объявление
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,10 +154,12 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center pt-8 sm:pt-10">
|
<div className="flex justify-center pt-8 sm:pt-10">
|
||||||
<Button
|
<Link
|
||||||
text="Отправить посылку"
|
href="/account/create-as-sender"
|
||||||
className="bg-orange hover:bg-orange/80 rounded-lg px-8 py-2.5 text-base text-white sm:px-12 sm:py-3 sm:text-lg"
|
className="bg-orange hover:bg-orange/80 rounded-lg px-8 py-2.5 text-base text-white sm:px-12 sm:py-3 sm:text-lg"
|
||||||
/>
|
>
|
||||||
|
Отправить посылку
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
139
frontend/components/popups/LeadPopup.tsx
Normal file
139
frontend/components/popups/LeadPopup.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import Modal from '../ui/Modal'
|
||||||
|
import Button from '../ui/Button'
|
||||||
|
import { useForm } from '@/app/hooks/useForm'
|
||||||
|
import showToast from '../ui/Toast'
|
||||||
|
import TextInput from '../ui/TextInput'
|
||||||
|
import TextAreaInput from '../ui/TextAreaInput'
|
||||||
|
import PhoneInput from '../ui/PhoneInput'
|
||||||
|
import useUserStore from '@/app/store/userStore'
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
name: { required: true },
|
||||||
|
phone_number: { required: true },
|
||||||
|
email: { required: true },
|
||||||
|
price: { required: false },
|
||||||
|
deliveryTime: { required: false },
|
||||||
|
comment: { required: false },
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeadPopupProps {
|
||||||
|
id: number
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeadPopup = ({ id, isOpen, onClose, onSuccess }: LeadPopupProps) => {
|
||||||
|
const { user } = useUserStore()
|
||||||
|
|
||||||
|
const { values, handleChange, handleSubmit, setValues } = useForm(
|
||||||
|
{
|
||||||
|
name: user?.name || '',
|
||||||
|
phone_number: user?.phone_number || '',
|
||||||
|
email: user?.email || '',
|
||||||
|
price: '',
|
||||||
|
deliveryTime: '',
|
||||||
|
comment: '',
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
validationRules,
|
||||||
|
async values => {
|
||||||
|
try {
|
||||||
|
// await sendLead(values)
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Сообщение отправлено!',
|
||||||
|
})
|
||||||
|
onSuccess()
|
||||||
|
} catch {
|
||||||
|
showToast({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Упс, что то пошло не так...',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
//форма инициализируется до того, как данные пользователя загружаются из стора, поэтому нужно обновлять значения формы при загрузке данных пользователя
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
name: user.name,
|
||||||
|
phone_number: user.phone_number || '',
|
||||||
|
email: user.email || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="Откликнуться на заявку" isOpen={isOpen} onClose={onClose}>
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TextInput
|
||||||
|
name="name"
|
||||||
|
value={values.name}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="ФИО"
|
||||||
|
placeholder="Введите ваше полное имя"
|
||||||
|
style="register"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PhoneInput
|
||||||
|
value={values.phone_number}
|
||||||
|
handleChange={handleChange}
|
||||||
|
operatorsInfo={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
name="email"
|
||||||
|
value={values.email}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Email"
|
||||||
|
placeholder="example@mail.com"
|
||||||
|
style="register"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
name="price"
|
||||||
|
value={values.price}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Предлагаемая цена"
|
||||||
|
placeholder="Укажите стоимость перевозки"
|
||||||
|
style="register"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
name="deliveryTime"
|
||||||
|
value={values.deliveryTime}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Срок доставки"
|
||||||
|
placeholder="Укажите предполагаемый срок доставки"
|
||||||
|
style="register"
|
||||||
|
/>
|
||||||
|
<TextAreaInput
|
||||||
|
name="comment"
|
||||||
|
value={values.comment}
|
||||||
|
handleChange={handleChange}
|
||||||
|
label="Комментарий (необязательно)"
|
||||||
|
placeholder="Опишите условия перевозки, детали и другую важную информацию"
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
text="Отправить заявку"
|
||||||
|
className="bg-orange hover:bg-opacity-90 flex w-full justify-center rounded-xl py-4 text-base font-semibold text-white transition-colors"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeadPopup
|
||||||
67
frontend/components/ui/Modal.tsx
Normal file
67
frontend/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { IoClose } from 'react-icons/io5'
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
title: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('keydown', handleEscape)
|
||||||
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
||||||
|
document.body.style.paddingRight = `${scrollbarWidth}px`
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEscape)
|
||||||
|
document.body.style.paddingRight = '0px'
|
||||||
|
document.body.style.overflow = 'unset'
|
||||||
|
}
|
||||||
|
}, [isOpen, onClose])
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[9999] overflow-y-auto">
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 bg-black transition-all duration-500 ease-in-out ${isOpen ? 'opacity-50 backdrop-blur-sm' : 'opacity-0 backdrop-blur-none'} `}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="z-[10000] flex min-h-full items-center justify-center">
|
||||||
|
<div
|
||||||
|
className={`relative min-h-screen w-full transform overflow-hidden bg-white text-left shadow-xl transition-all duration-500 ease-in-out sm:m-4 sm:min-h-0 sm:max-w-3xl sm:rounded-2xl ${
|
||||||
|
isOpen ? 'translate-y-0 scale-100 opacity-100' : 'translate-y-8 scale-95 opacity-0'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-100 p-4 sm:p-6">
|
||||||
|
<h2 className="text-xl font-bold sm:text-2xl">{title}</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-xl bg-gray-500 p-2 text-white transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-400 hover:text-black"
|
||||||
|
>
|
||||||
|
<IoClose size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal
|
||||||
Reference in New Issue
Block a user