fix: update form
This commit is contained in:
BIN
public/images/license-dtr-eac.png
Normal file
BIN
public/images/license-dtr-eac.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
BIN
public/images/license-mcs.jpg
Normal file
BIN
public/images/license-mcs.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
0
src/core/providers/index.ts
Normal file
0
src/core/providers/index.ts
Normal file
41
src/core/providers/modal-provider.tsx
Normal file
41
src/core/providers/modal-provider.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
} from 'react';
|
||||||
|
import { Modal } from '@shared/ui/modal';
|
||||||
|
|
||||||
|
const ModalContext = createContext({
|
||||||
|
hideModal: () => {},
|
||||||
|
showModal: (content: ReactNode) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const useModal = () => useContext(ModalContext);
|
||||||
|
|
||||||
|
const ModalProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [modalContent, setModalContent] = useState<ReactNode>(null);
|
||||||
|
|
||||||
|
const showModal = useCallback((content: ReactNode) => {
|
||||||
|
setModalContent(content);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideModal = useCallback(() => {
|
||||||
|
setModalContent(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ hideModal, showModal }}>
|
||||||
|
{children}
|
||||||
|
{/* Ваш Modal компонент здесь */}
|
||||||
|
<Modal isOpen={modalContent !== null} onClose={hideModal}>
|
||||||
|
{modalContent}
|
||||||
|
</Modal>
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useModal, ModalProvider };
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
//frontend breakpoint
|
//frontend breakpoint
|
||||||
$mobile: 360px;
|
$mobile: 360px;
|
||||||
$tablet: 768px;
|
$tablet: 768px;
|
||||||
@@ -24,5 +23,6 @@ $color-darkgray: #999999;
|
|||||||
$color-text: #333333;
|
$color-text: #333333;
|
||||||
$color-text-light: #222222;
|
$color-text-light: #222222;
|
||||||
$color-mark: #E96526;
|
$color-mark: #E96526;
|
||||||
$color-error: #FF0000;
|
$color-error: #ff0000;
|
||||||
|
$color-error-light: #ff9191;
|
||||||
$color-gray-border: #555555;
|
$color-gray-border: #555555;
|
||||||
1
src/shared/lib/clickOutside/index.ts
Normal file
1
src/shared/lib/clickOutside/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { useClickOutside } from './useClickOutside';
|
||||||
28
src/shared/lib/clickOutside/useClickOutside.tsx
Normal file
28
src/shared/lib/clickOutside/useClickOutside.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useClickOutside(
|
||||||
|
ref: RefObject<HTMLDivElement | null>,
|
||||||
|
fn: (event: Event) => void,
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: Event) => {
|
||||||
|
const el = ref?.current;
|
||||||
|
if (event instanceof MouseEvent && window !== undefined) {
|
||||||
|
const x = event?.offsetX || 0;
|
||||||
|
const width = window?.innerWidth - 18;
|
||||||
|
if (x >= width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!el || el.contains((event?.target as Node) || null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [ref, fn]);
|
||||||
|
}
|
||||||
15
src/shared/lib/detectIOS/detectIOS.ts
Normal file
15
src/shared/lib/detectIOS/detectIOS.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
function detectIOS() {
|
||||||
|
const iosQuirkPresent = () => {
|
||||||
|
const audio = new Audio();
|
||||||
|
audio.volume = 0.5;
|
||||||
|
return audio.volume === 1; // volume cannot be changed from "1" on iOS 12 and below
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
const isAppleDevice = navigator.userAgent.includes('Macintosh');
|
||||||
|
const isTouchScreen = navigator.maxTouchPoints >= 1; // true for iOS 13 (and hopefully beyond)
|
||||||
|
|
||||||
|
return isIOS || (isAppleDevice && (isTouchScreen || iosQuirkPresent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { detectIOS };
|
||||||
1
src/shared/lib/detectIOS/index.ts
Normal file
1
src/shared/lib/detectIOS/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { detectIOS } from './detectIOS';
|
||||||
2
src/shared/lib/index.ts
Normal file
2
src/shared/lib/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { useClickOutside } from './clickOutside';
|
||||||
|
export { detectIOS } from './detectIOS';
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import s from './advancedPhoneInput.module.scss';
|
|
||||||
import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, Ref } from 'react';
|
|
||||||
import { clsx } from 'clsx';
|
|
||||||
import { Button, Input } from '@shared/ui';
|
|
||||||
|
|
||||||
type AdvancedPhoneInputProps = {
|
|
||||||
containerClassName?: string;
|
|
||||||
inputClassName?: string;
|
|
||||||
buttonClassName?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
text: string;
|
|
||||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
|
||||||
|
|
||||||
const AdvancedPhoneInput = forwardRef(function AdvancedPhoneInput(
|
|
||||||
{
|
|
||||||
containerClassName,
|
|
||||||
inputClassName,
|
|
||||||
buttonClassName,
|
|
||||||
onClick,
|
|
||||||
text,
|
|
||||||
...props
|
|
||||||
}: AdvancedPhoneInputProps,
|
|
||||||
ref: Ref<HTMLInputElement>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={clsx(containerClassName, s.Container)}>
|
|
||||||
<Input {...props} ref={ref} className={clsx(inputClassName, s.Phone)} />
|
|
||||||
<Button
|
|
||||||
className={clsx(buttonClassName, s.Button)}
|
|
||||||
onClick={onClick}
|
|
||||||
variant='orange'
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default AdvancedPhoneInput;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export { Button } from './button';
|
export { Button } from './button';
|
||||||
export { Mark } from './mark';
|
export { Mark } from './mark';
|
||||||
export { Input } from './input';
|
export { Input } from './input';
|
||||||
export { AdvancedPhoneInput } from './advanced-phone-input';
|
|
||||||
export { TextArea } from './text-area';
|
export { TextArea } from './text-area';
|
||||||
export { PhoneInput } from './phone-input';
|
export { PhoneInput } from './phone-input';
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
.Container {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.Input {
|
.Input {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
@@ -13,16 +18,16 @@
|
|||||||
color: $color-text;
|
color: $color-text;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|
||||||
@include iftablet{
|
@include iftablet {
|
||||||
font-size: rem(18px);
|
font-size: rem(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include iflaptop{
|
@include iflaptop {
|
||||||
font-size: rem(20px);
|
font-size: rem(20px);
|
||||||
padding: rem(10px) rem(20px);
|
padding: rem(10px) rem(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include ifdesktop{
|
@include ifdesktop {
|
||||||
font-size: rem(24px);
|
font-size: rem(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@
|
|||||||
border-color: $color-error;
|
border-color: $color-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_fullWidth{
|
&_fullWidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,4 +73,16 @@
|
|||||||
transition: border-color ease .5s;
|
transition: border-color ease .5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Error {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
left: rem(8px);
|
||||||
|
bottom: rem(-16px);
|
||||||
|
font-family: $font-open-sans;
|
||||||
|
font-weight: $font-light;
|
||||||
|
font-size: rem(12px);
|
||||||
|
line-height: 100%;
|
||||||
|
color: $color-error;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ type InputProps = {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
variant?: 'default' | 'ghost';
|
variant?: 'default' | 'ghost';
|
||||||
error?: string | boolean;
|
error?: string | boolean;
|
||||||
|
errorTextColor?: string;
|
||||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||||
|
|
||||||
const Input = forwardRef(function Input(
|
const Input = forwardRef(function Input(
|
||||||
@@ -19,22 +20,30 @@ const Input = forwardRef(function Input(
|
|||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
error = false,
|
error = false,
|
||||||
|
errorTextColor,
|
||||||
...props
|
...props
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref: Ref<HTMLInputElement>,
|
ref: Ref<HTMLInputElement>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<input
|
<div className={s.Container}>
|
||||||
{...props}
|
<input
|
||||||
ref={ref}
|
{...props}
|
||||||
className={clsx(
|
ref={ref}
|
||||||
s.Input,
|
className={clsx(
|
||||||
s['Input_' + variant],
|
s.Input,
|
||||||
fullWidth && s.Input_fullWidth,
|
s['Input_' + variant],
|
||||||
error && s.Input_error,
|
fullWidth && s.Input_fullWidth,
|
||||||
className,
|
error && s.Input_error,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<span className={s.Error} style={{ color: errorTextColor }}>
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1
src/shared/ui/modal/index.ts
Normal file
1
src/shared/ui/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Modal, ModalContent } from './modal';
|
||||||
144
src/shared/ui/modal/modal.module.scss
Normal file
144
src/shared/ui/modal/modal.module.scss
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
.ModalBackdrop {
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@include iftablet {
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0 rem(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include iflaptop {
|
||||||
|
padding: 0 rem(48px);
|
||||||
|
}
|
||||||
|
animation: fadeIn ease 0.3s;
|
||||||
|
|
||||||
|
&_open {
|
||||||
|
animation: fadeOut ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_isIOS {
|
||||||
|
height: 150vh !important;
|
||||||
|
|
||||||
|
@include iftablet {
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModalContent {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
//max-height: 90%;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
//border-radius: 8px;
|
||||||
|
animation: fadeIn ease 0.3s;
|
||||||
|
|
||||||
|
@include iftablet {
|
||||||
|
position: relative;
|
||||||
|
top: unset;
|
||||||
|
bottom: unset;
|
||||||
|
left: unset;
|
||||||
|
right: unset;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: rem(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_open {
|
||||||
|
animation: fadeOut ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_isIOS {
|
||||||
|
height: 100vh !important;
|
||||||
|
|
||||||
|
@include iftablet {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Inner {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Modal {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__Close {
|
||||||
|
position: absolute;
|
||||||
|
top: rem(5px);
|
||||||
|
right: rem(5px);
|
||||||
|
width: rem(36px);
|
||||||
|
height: rem(36px);
|
||||||
|
padding: rem(10px);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: rem(4px);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
@include iftablet {
|
||||||
|
top: rem(35px);
|
||||||
|
right: rem(35px);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include iflaptop {
|
||||||
|
top: rem(50px);
|
||||||
|
right: rem(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: rem(17px);
|
||||||
|
height: rem(17px);
|
||||||
|
}
|
||||||
|
|
||||||
|
//svg {
|
||||||
|
// transform: rotate(0deg);
|
||||||
|
// transition: transform 0.3s;
|
||||||
|
// &:hover,
|
||||||
|
// &:active {
|
||||||
|
// transform: rotate(45deg);
|
||||||
|
// transition: transform 0.3s;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/shared/ui/modal/modal.tsx
Normal file
109
src/shared/ui/modal/modal.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import s from './modal.module.scss';
|
||||||
|
import { ReactNode, useEffect, useRef } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useModal } from '@core/providers/modal-provider';
|
||||||
|
import { detectIOS, useClickOutside } from '@shared/lib';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = ({ children, isOpen, onClose }: ModalProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const html = document.documentElement;
|
||||||
|
if (isOpen) {
|
||||||
|
html.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
html.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const isIOS = detectIOS();
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
s.ModalBackdrop,
|
||||||
|
isOpen && s.ModalBackdrop__open,
|
||||||
|
isIOS && s.ModalBackdrop_isIOS,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ModalContent(props: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
innerClassName?: string;
|
||||||
|
closeByClickOutside?: boolean;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
innerClassName,
|
||||||
|
closeByClickOutside = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
const hideModal = () => modal.hideModal();
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
if (closeByClickOutside) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useClickOutside(modalRef, handleClickOutside);
|
||||||
|
|
||||||
|
const isIOS = detectIOS();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(className, s.ModalContent, isIOS && s.ModalContent_isIOS)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
ref={modalRef}
|
||||||
|
>
|
||||||
|
<div className={clsx(s.ModalContent__Inner, innerClassName)}>
|
||||||
|
<div className={s.Modal__Close} onClick={hideModal}>
|
||||||
|
<CloseIcon />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Modal, ModalContent };
|
||||||
|
|
||||||
|
const CloseIcon = () => (
|
||||||
|
<svg
|
||||||
|
width='17'
|
||||||
|
height='17'
|
||||||
|
viewBox='0 0 17 17'
|
||||||
|
fill='none'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d='M2.5 15L15.5 2M2.5 2L15.5 15'
|
||||||
|
stroke='#053635'
|
||||||
|
strokeOpacity='0.3'
|
||||||
|
strokeWidth='2'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
//import s from './phone-input.module.scss';
|
|
||||||
import { Input } from '@shared/ui';
|
import { Input } from '@shared/ui';
|
||||||
import { useMaskito } from '@maskito/react';
|
import { useMaskito } from '@maskito/react';
|
||||||
|
|
||||||
import { maskitoPhoneOptionsGenerator } from '@maskito/phone';
|
import { maskitoPhoneOptionsGenerator } from '@maskito/phone';
|
||||||
import metadata from 'libphonenumber-js/min/metadata';
|
import metadata from 'libphonenumber-js/min/metadata';
|
||||||
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
|
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
|
||||||
|
|
||||||
type PhoneInput = {
|
type PhoneInput = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'default' | 'ghost';
|
||||||
|
error?: string | boolean;
|
||||||
|
errorTextColor?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||||
|
|
||||||
export default function PhoneInput({ ...props }: PhoneInput) {
|
export default function PhoneInput({ ...props }: PhoneInput) {
|
||||||
@@ -17,9 +19,5 @@ export default function PhoneInput({ ...props }: PhoneInput) {
|
|||||||
});
|
});
|
||||||
const maskedInputRef = useMaskito({ options });
|
const maskedInputRef = useMaskito({ options });
|
||||||
|
|
||||||
return (
|
return <Input {...props} ref={maskedInputRef} type='tel' />;
|
||||||
<>
|
|
||||||
<Input {...props} ref={maskedInputRef} type='tel' />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.Container {
|
.Container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -6,7 +7,7 @@
|
|||||||
/* Allows the `resize` property to work on the div */
|
/* Allows the `resize` property to work on the div */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&_fullWidth{
|
&_fullWidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,16 +30,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
@include iftablet{
|
@include iftablet {
|
||||||
font-size: rem(18px);
|
font-size: rem(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include iflaptop{
|
@include iflaptop {
|
||||||
font-size: rem(20px);
|
font-size: rem(20px);
|
||||||
padding: rem(10px) rem(20px);
|
padding: rem(10px) rem(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include ifdesktop{
|
@include ifdesktop {
|
||||||
font-size: rem(24px);
|
font-size: rem(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,27 +48,33 @@
|
|||||||
/* Arrow mouse cursor over the scrollbar */
|
/* Arrow mouse cursor over the scrollbar */
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: rem(12px);
|
width: rem(12px);
|
||||||
height: rem(12px);
|
height: rem(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track,
|
&::-webkit-scrollbar-track,
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
border: rem(4px) solid transparent;
|
border: rem(4px) solid transparent;
|
||||||
border-radius: rem(12px);
|
border-radius: rem(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background-color: #333; // цвет дорожки
|
background-color: #333; // цвет дорожки
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: #999; // цвет указателя
|
background-color: #999; // цвет указателя
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
:where(&:not(:disabled))::-webkit-scrollbar-thumb:hover {
|
:where(&:not(:disabled))::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: #999;
|
background-color: #999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:where(:autofill) {
|
&:where(:autofill) {
|
||||||
/* Reliably removes native autofill colors */
|
/* Reliably removes native autofill colors */
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -113,4 +120,18 @@
|
|||||||
transition: border-color ease .5s;
|
transition: border-color ease .5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_error {
|
||||||
|
border: 1px solid $color-error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Error {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: $font-open-sans;
|
||||||
|
font-weight: $font-light;
|
||||||
|
font-size: rem(12px);
|
||||||
|
line-height: 100%;
|
||||||
|
color: $color-error;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,8 @@ type TextAreaProps = {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
variant?: 'default' | 'ghost';
|
variant?: 'default' | 'ghost';
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
error?: string | boolean;
|
||||||
|
errorTextColor?: string;
|
||||||
} & DetailedHTMLProps<
|
} & DetailedHTMLProps<
|
||||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
HTMLTextAreaElement
|
HTMLTextAreaElement
|
||||||
@@ -24,6 +26,8 @@ const TextArea = forwardRef(function TextArea(
|
|||||||
children,
|
children,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
|
error = false,
|
||||||
|
errorTextColor,
|
||||||
...props
|
...props
|
||||||
}: TextAreaProps,
|
}: TextAreaProps,
|
||||||
ref: Ref<HTMLTextAreaElement>,
|
ref: Ref<HTMLTextAreaElement>,
|
||||||
@@ -33,10 +37,20 @@ const TextArea = forwardRef(function TextArea(
|
|||||||
<textarea
|
<textarea
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(className, s.Area, s['Area_' + variant])}
|
className={clsx(
|
||||||
|
className,
|
||||||
|
s.Area,
|
||||||
|
s['Area_' + variant],
|
||||||
|
error && s.Area_error,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</textarea>
|
</textarea>
|
||||||
|
{error && (
|
||||||
|
<span className={s.Error} style={{ color: errorTextColor }}>
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
84
src/widgets/advanced-phone-input/advanced-phone-input.tsx
Normal file
84
src/widgets/advanced-phone-input/advanced-phone-input.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import s from './advanced-phone-input.module.scss';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { Button, PhoneInput } from '@shared/ui';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { sendFormFn } from '@shared/api/api.service';
|
||||||
|
import { isValidPhoneNumber } from 'libphonenumber-js/min';
|
||||||
|
|
||||||
|
type AdvancedPhoneInputProps = {
|
||||||
|
containerClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
phone: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdvancedPhoneInput({
|
||||||
|
containerClassName,
|
||||||
|
}: AdvancedPhoneInputProps) {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
clearErrors,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: TForm) => {
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
form: 'offer-request-form-desktop',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendFormFn(payload);
|
||||||
|
toast.success('Заявка на консультацию принята');
|
||||||
|
reset(defaultValues);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('Ошибка при отправке заявки...', {
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className={clsx(containerClassName, s.Container)}
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={'phone'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<PhoneInput
|
||||||
|
{...field}
|
||||||
|
className={s.Phone}
|
||||||
|
placeholder={'+7 (999) 123-45-67'}
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button className={s.Button} variant='orange'>
|
||||||
|
Отправить заявку
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import s from './styles.module.scss';
|
import s from './styles.module.scss';
|
||||||
import { Button, Input } from '@shared/ui';
|
import { Button, Input, PhoneInput } from '@shared/ui';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
@@ -10,10 +10,16 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import bgForm from '@public/images/bg-form.jpg';
|
import bgForm from '@public/images/bg-form.jpg';
|
||||||
import { sendFormFn } from '@shared/api/api.service';
|
import { sendFormFn } from '@shared/api/api.service';
|
||||||
|
import { isValidPhoneNumber } from 'libphonenumber-js/min';
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
name: z.string().min(3),
|
name: z
|
||||||
phone: z.string(),
|
.string()
|
||||||
|
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
|
||||||
|
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
|
||||||
|
message: 'Поле содержит некорректные символы',
|
||||||
|
}),
|
||||||
|
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
|
||||||
});
|
});
|
||||||
type TForm = z.infer<typeof FormSchema>;
|
type TForm = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
@@ -27,6 +33,7 @@ export default function ContactsForm() {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@@ -75,18 +82,33 @@ export default function ContactsForm() {
|
|||||||
control={control}
|
control={control}
|
||||||
name={'name'}
|
name={'name'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} type='text' placeholder='Ваше имя' fullWidth />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={'Ваше имя'}
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('name');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.name?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={'phone'}
|
name={'phone'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<PhoneInput
|
||||||
{...field}
|
{...field}
|
||||||
type='text'
|
placeholder={'+7 (999) 123-45-67'}
|
||||||
placeholder='+7 (999) 123 45 67'
|
|
||||||
fullWidth
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import s from './styles.module.scss';
|
import s from './styles.module.scss';
|
||||||
import { Button, Input, Mark, TextArea } from '@shared/ui';
|
import { Button, Input, Mark, PhoneInput, TextArea } from '@shared/ui';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -9,11 +9,19 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import man from '@public/images/footer-man.png';
|
import man from '@public/images/footer-man.png';
|
||||||
import { sendFormFn } from '@shared/api/api.service';
|
import { sendFormFn } from '@shared/api/api.service';
|
||||||
|
import { isValidPhoneNumber } from 'libphonenumber-js/min';
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
name: z.string().min(3),
|
name: z
|
||||||
phone: z.string(),
|
.string()
|
||||||
message: z.string(),
|
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
|
||||||
|
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
|
||||||
|
message: 'Поле содержит некорректные символы',
|
||||||
|
}),
|
||||||
|
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.min(21, { message: 'Оставьте сообщение мин. 20 символов' }),
|
||||||
});
|
});
|
||||||
type TForm = z.infer<typeof FormSchema>;
|
type TForm = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
@@ -28,6 +36,7 @@ export default function FooterForm() {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@@ -67,6 +76,12 @@ export default function FooterForm() {
|
|||||||
variant='ghost'
|
variant='ghost'
|
||||||
placeholder={'Ваше имя'}
|
placeholder={'Ваше имя'}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('name');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.name?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -74,11 +89,17 @@ export default function FooterForm() {
|
|||||||
control={control}
|
control={control}
|
||||||
name={'phone'}
|
name={'phone'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<PhoneInput
|
||||||
{...field}
|
{...field}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
placeholder={'+7 999 1234567'}
|
placeholder={'+7 (999) 123-45-67'}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -94,6 +115,8 @@ export default function FooterForm() {
|
|||||||
id='story'
|
id='story'
|
||||||
name='story'
|
name='story'
|
||||||
rows={6}
|
rows={6}
|
||||||
|
error={errors && errors.message?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export { LicenseForm } from './license-form';
|
|||||||
export { LicenseSlider } from './license-slider';
|
export { LicenseSlider } from './license-slider';
|
||||||
export { OfferForm } from './offer-form';
|
export { OfferForm } from './offer-form';
|
||||||
export { OfferRequestForm } from './offer-request';
|
export { OfferRequestForm } from './offer-request';
|
||||||
|
export { AdvancedPhoneInput } from './advanced-phone-input';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import s from './styles.module.scss';
|
import s from './styles.module.scss';
|
||||||
import { Button, Input } from '@shared/ui';
|
import { Button, Input, PhoneInput } from '@shared/ui';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -10,11 +10,18 @@ import { z } from 'zod';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { sendFormFn } from '@shared/api/api.service';
|
import { sendFormFn } from '@shared/api/api.service';
|
||||||
|
import { isValidPhoneNumber } from 'libphonenumber-js/min';
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
name: z.string().min(3),
|
name: z
|
||||||
phone: z.string(),
|
.string()
|
||||||
|
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
|
||||||
|
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
|
||||||
|
message: 'Поле содержит некорректные символы',
|
||||||
|
}),
|
||||||
|
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
|
||||||
});
|
});
|
||||||
|
|
||||||
type TForm = z.infer<typeof FormSchema>;
|
type TForm = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@@ -27,6 +34,7 @@ export default function LicenseForm() {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@@ -77,14 +85,34 @@ export default function LicenseForm() {
|
|||||||
control={control}
|
control={control}
|
||||||
name={'name'}
|
name={'name'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} placeholder={'Ваше имя'} fullWidth />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={'Ваше имя'}
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('name');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.name?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={'phone'}
|
name={'phone'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} placeholder={'+7 (999) 123 45 67'} fullWidth />
|
<PhoneInput
|
||||||
|
{...field}
|
||||||
|
placeholder={'+7 (999) 123-45-67'}
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button variant='orange' fullWidth>
|
<Button variant='orange' fullWidth>
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export default function OfferForm() {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
mode: 'onSubmit',
|
|
||||||
reValidateMode: 'onBlur',
|
|
||||||
resolver: zodResolver(FormSchema),
|
resolver: zodResolver(FormSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
@@ -66,6 +65,11 @@ export default function OfferForm() {
|
|||||||
className={s.Unit}
|
className={s.Unit}
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='Ваше имя'
|
placeholder='Ваше имя'
|
||||||
|
error={errors && errors.name?.message}
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('name');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -78,6 +82,11 @@ export default function OfferForm() {
|
|||||||
className={s.Unit}
|
className={s.Unit}
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='+7 999 123-45-67'
|
placeholder='+7 999 123-45-67'
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: rem(12px);
|
gap: rem(20px);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
max-width: rem(400px);
|
max-width: rem(400px);
|
||||||
@include iftablet{
|
@include iftablet{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import s from './styles.module.scss';
|
import s from './styles.module.scss';
|
||||||
import { AdvancedPhoneInput, Button, Input } from '@shared/ui';
|
import { Button, Input, PhoneInput } from '@shared/ui';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -9,12 +9,18 @@ import { z } from 'zod';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import bgForm from '@public/images/bg-form.jpg';
|
import bgForm from '@public/images/bg-form.jpg';
|
||||||
import { useState } from 'react';
|
import { AdvancedPhoneInput } from '@/widgets';
|
||||||
import { sendFormFn } from '@shared/api/api.service';
|
import { sendFormFn } from '@shared/api/api.service';
|
||||||
|
import { isValidPhoneNumber } from 'libphonenumber-js/min';
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
name: z.string().min(3),
|
name: z
|
||||||
phone: z.string(),
|
.string()
|
||||||
|
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
|
||||||
|
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
|
||||||
|
message: 'Поле содержит некорректные символы',
|
||||||
|
}),
|
||||||
|
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
|
||||||
});
|
});
|
||||||
type TForm = z.infer<typeof FormSchema>;
|
type TForm = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
@@ -28,6 +34,7 @@ export default function OfferRequest() {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
@@ -36,12 +43,10 @@ export default function OfferRequest() {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [inputPhone, setInputPhone] = useState('');
|
const onSubmit = async (data: TForm) => {
|
||||||
|
|
||||||
const onSubmitForm = async (data: TForm) => {
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...data,
|
...data,
|
||||||
form: 'offer-request-form',
|
form: 'offer-request-form-mobile',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -55,23 +60,6 @@ export default function OfferRequest() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmitPhone = async (phone: string) => {
|
|
||||||
const payload = {
|
|
||||||
phone: phone,
|
|
||||||
form: 'offer-request-form',
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendFormFn(payload);
|
|
||||||
toast.success('Заявка на консультацию принята');
|
|
||||||
setInputPhone('');
|
|
||||||
} catch (e) {
|
|
||||||
toast.error('Ошибка при отправке заявки...', {
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.Form}>
|
<div className={s.Form}>
|
||||||
<Image
|
<Image
|
||||||
@@ -88,28 +76,40 @@ export default function OfferRequest() {
|
|||||||
<h3 className={s.Title}>Оставьте заявку на бесплатную консультацию</h3>
|
<h3 className={s.Title}>Оставьте заявку на бесплатную консультацию</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.PanelRight}>
|
<div className={s.PanelRight}>
|
||||||
<AdvancedPhoneInput
|
<AdvancedPhoneInput containerClassName={s.AdvPhoneInput} />
|
||||||
value={inputPhone}
|
|
||||||
onChange={(e) => setInputPhone(e.target.value)}
|
|
||||||
onClick={() => onSubmitPhone(inputPhone)}
|
|
||||||
containerClassName={s.AdvPhoneInput}
|
|
||||||
text='Отправить заявку'
|
|
||||||
placeholder={'+7 (999) 123 45 67'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form className={s.MobileBtns} onSubmit={handleSubmit(onSubmitForm)}>
|
<form className={s.MobileBtns} onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={'name'}
|
name={'name'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} placeholder='Ваше имя' fullWidth />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder='Ваше имя'
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('name');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.name?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={'phone'}
|
name={'phone'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} placeholder='+7 999 123 45 67' fullWidth />
|
<PhoneInput
|
||||||
|
{...field}
|
||||||
|
placeholder='+7 (999) 123-45-67'
|
||||||
|
onChange={(e) => {
|
||||||
|
clearErrors('phone');
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
error={errors && errors.phone?.message}
|
||||||
|
errorTextColor={'#ff9191'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button variant='orange' fullWidth>
|
<Button variant='orange' fullWidth>
|
||||||
|
|||||||
Reference in New Issue
Block a user