This commit is contained in:
2024-03-18 13:38:16 +05:00
parent c9399c9408
commit 58df0badf7
114 changed files with 11334 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
icon?: JSX.Element;
color?: "primary" | "secondary";
width?: "fit" | "full";
disabled?: boolean;
className?: string;
onClick?: () => void;
}
function Button({
children,
color = "primary",
icon,
width = "fit",
disabled = false,
className,
onClick,
}: ButtonProps) {
return (
<button
disabled={disabled}
onClick={onClick}
className={`group relative px-6 py-2 rounded-full min-w-fit ${
(color === "primary" ? "bg-gradient" : "") ||
(color === "secondary" ? "outline outline-1 outline-[#3D425C]" : "")
} ${
icon ? "pr-4" : ""
} flex justify-between gap-1 items-center overflow-hidden w-${width} ${className}`}
>
<span className="group-hover:opacity-10 opacity-0 bg-black transition-opacity absolute top-0 left-0 w-full h-full"></span>
<span className="relative font-gilroy font-medium">{children}</span>
<span className="relative ">{icon}</span>
</button>
);
}
export default Button;
+437
View File
@@ -0,0 +1,437 @@
/* eslint-disable react-hooks/exhaustive-deps */
import Slider from "react-rangeslider";
import Button from "./Button";
import CalcSelect from "./CalcSelect";
import ArrowRightIcon from "./icons/ArrowRightIcon";
import regionsData from "../assets/regionsData.json";
import { useEffect, useState } from "react";
import CloseIcon from "./icons/CloseIcon";
import api from "../utils/api";
interface Region {
id: number;
name: string;
areaInComplex: number;
areaApartment: number;
costPerSquare: number;
}
function Calc() {
const [selectedRegion, setSelectedRegion] = useState<Region>();
const [consultations, setConsultations] = useState<number>(100);
const [implementationPeriod, setImplementationPeriod] = useState<number>(
null!
);
const [oldImplementationPeriod, setOldImplementationPeriod] =
useState<number>();
const [monthlyIncome, setMonthlyIncome] = useState<number>();
const [oldMonthlyIncome, setOldMonthlyIncome] = useState<number>();
const [isToolEnabled, setIsToolEnabled] = useState<boolean>(false);
const [reservation, setReservation] = useState<number>();
const [oldReservation, setOldReservation] = useState<number>();
const [sales, setSales] = useState<number>();
const [oldSales, setOldSales] = useState<number>();
const [diffImplementationPeriod, setDiffImplementationPeriod] =
useState<number>();
const [diffMonthlyIncome, setDiffMonthlyIncome] = useState<number>();
const [implementationPeriodEnding, setImplementationPeriodEnding] =
useState<string>();
const [oldImplementationPeriodEnding, setOldImplementationPeriodEnding] =
useState<string>();
const [diffImplementationPeriodEnding, setDiffImplementationPeriodEnding] =
useState<string>();
async function getRegionName() {
const result: any = await api.get("getRegionName").json();
if (result.error) {
setSelectedRegion(regionsData.find((region) => region.id === 11));
return;
}
const foundRegion =
regionsData.find((region) => region.name === result.regionName) ||
regionsData.find((region) => region.id === 11);
setSelectedRegion(foundRegion);
}
useEffect(() => {
getRegionName();
}, []);
useEffect(() => {
if (!consultations || !selectedRegion || !sales || !oldSales) return;
setOldImplementationPeriod(
Math.round(
selectedRegion.areaInComplex / selectedRegion.areaApartment / oldSales
)
);
setImplementationPeriod(
Math.round(
selectedRegion.areaInComplex / selectedRegion.areaApartment / sales
)
);
setOldMonthlyIncome(
Math.round(
(selectedRegion.areaApartment *
selectedRegion.costPerSquare *
oldSales) /
1000
)
);
setMonthlyIncome(
Math.round(
(selectedRegion.areaApartment * selectedRegion.costPerSquare * sales) /
1000
)
);
}, [consultations, selectedRegion, isToolEnabled, sales]);
useEffect(() => {
setOldReservation(Math.round((30 * consultations) / 100));
setReservation(Math.round((48 * consultations) / 100));
setOldSales(Math.round((((30 * consultations) / 100) * 30) / 100));
setSales(Math.round((((48 * consultations) / 100) * 42) / 100));
}, [consultations]);
useEffect(() => {
if (!implementationPeriod || !oldImplementationPeriod) return;
setDiffImplementationPeriod(oldImplementationPeriod - implementationPeriod);
}, [implementationPeriod, oldImplementationPeriod]);
useEffect(() => {
if (!monthlyIncome || !oldMonthlyIncome) return;
setDiffMonthlyIncome(monthlyIncome - oldMonthlyIncome);
}, [monthlyIncome, oldMonthlyIncome]);
useEffect(() => {
if (implementationPeriod > 10 && implementationPeriod < 15) {
setOldImplementationPeriodEnding("месяцев");
return;
}
if (implementationPeriod % 10 === 1) {
setImplementationPeriodEnding("месяц");
} else if (
implementationPeriod % 10 === 2 ||
implementationPeriod % 10 === 3 ||
implementationPeriod % 10 === 4
) {
setImplementationPeriodEnding("месяца");
} else {
setImplementationPeriodEnding("месяцев");
}
}, [implementationPeriod]);
useEffect(() => {
if (!oldImplementationPeriod) return;
if (oldImplementationPeriod > 10 && oldImplementationPeriod < 15) {
setOldImplementationPeriodEnding("месяцев");
return;
}
if (oldImplementationPeriod % 10 === 1) {
setOldImplementationPeriodEnding("месяц");
} else if (
oldImplementationPeriod % 10 === 2 ||
oldImplementationPeriod % 10 === 3 ||
oldImplementationPeriod % 10 === 4
) {
setOldImplementationPeriodEnding("месяца");
} else {
setOldImplementationPeriodEnding("месяцев");
}
}, [oldImplementationPeriod]);
useEffect(() => {
if (!diffImplementationPeriod) return;
if (diffImplementationPeriod > 10 && diffImplementationPeriod < 15) {
setDiffImplementationPeriodEnding("месяцев");
return;
}
if (diffImplementationPeriod % 10 === 1) {
setDiffImplementationPeriodEnding("месяц");
} else if (
diffImplementationPeriod % 10 === 2 ||
diffImplementationPeriod % 10 === 3 ||
diffImplementationPeriod % 10 === 4
) {
setDiffImplementationPeriodEnding("месяца");
} else {
setDiffImplementationPeriodEnding("месяцев");
}
}, [diffImplementationPeriod]);
return (
<div className="relative flex flex-col sm:gap-16 gap-8">
<div className="grid xl:grid-cols-4 sm:grid-cols-3">
<div className="xl:col-auto sm:col-span-full xl:block sm:grid grid-cols-2">
{selectedRegion && (
<CalcSelect
label="Регион"
placeholder="Выберите регион"
defaultOption={selectedRegion.name}
options={regionsData.map((regionItem) => regionItem.name)}
handleSelect={(option) => {
const foundRegion = regionsData.find(
(region) => region.name === option
);
if (foundRegion) {
setSelectedRegion(foundRegion);
}
}}
/>
)}
<div className="border xl:border-t-0 xl:border-l sm:border-l-0 sm:border-t border-t-0 border-[#3D425C] 2xl:p-6 p-4 flex items-center">
<p className="text-[#52587A] text-xs leading-[120%]">
Установлены усредненные показатели по региону.
<br className="2xl:block xl:hidden sm:block hidden" /> Источник:{" "}
<a
href="https://наш.дом.рф"
target="_blank"
className="text-[#798FFF]"
>
наш.дом.рф
</a>
</p>
</div>
</div>
<div className="2xl:px-8 2xl:py-6 px-6 py-4 flex flex-col justify-between gap-6 border xl:border-l-0 xl:border-t border-t-0 border-[#3D425C]">
<p className="text-sm">
Средняя площадь
<br />
жилья в комплексе, м²
</p>
<p className="2xl:text-5xl text-[32px] font-gilroy font-medium leading-none">
{(selectedRegion?.areaInComplex || 1500).toLocaleString()}
</p>
</div>
<div className="2xl:px-8 2xl:py-6 px-6 py-4 flex flex-col justify-between gap-6 border sm:border-l-0 border-l xl:border-t border-t-0 border-[#3D425C]">
<p className="text-sm">
Средняя площадь
<br />
квартиры, м²
</p>
<p className="2xl:text-5xl text-[32px] font-gilroy font-medium leading-none">
{(selectedRegion?.areaApartment || 100).toLocaleString()}
</p>
</div>
<div className="2xl:px-8 2xl:py-6 px-6 py-4 flex flex-col justify-between gap-6 border sm:border-l-0 border-l xl:border-t border-t-0 border-[#3D425C]">
<p className="text-sm">
Средняя стоимость
<br />
одного м², тыс. руб.
</p>
<p className="2xl:text-5xl text-[32px] font-gilroy font-medium leading-none">
{(selectedRegion?.costPerSquare || 100).toLocaleString()}
</p>
</div>
</div>
<div className="grid xl:grid-cols-4 grid-cols-2">
<div className="xl:flex flex-col xl:gap-6 xl:mr-8 xl:border-b border-[#3D425C] xl:col-auto col-span-full sm:grid grid-cols-2 gap-12 xl:mb-0 sm:mb-6 mb-8">
<div className="flex flex-col gap-6">
<div className="flex justify-between">
<p className="font-gilroy font-medium 2xl:text-base text-sm leading-none">
Очных консультаций в месяц
</p>
<p className="font-gilroy font-medium text-[#798FFF] 2xl:text-base text-sm leading-none">
{consultations}
</p>
</div>
<div className="py-[9px]">
<Slider
min={10}
max={1000}
step={10}
value={consultations}
onChange={(value) => setConsultations(value)}
tooltip={false}
/>
</div>
</div>
<Button
icon={
isToolEnabled ? (
<CloseIcon className="2xl:w-8 w-6 2xl:h-8 h-6" />
) : (
<ArrowRightIcon className="2xl:w-8 2xl:h-8" />
)
}
color={isToolEnabled ? "secondary" : "primary"}
onClick={() => setIsToolEnabled((prev) => !prev)}
className="px-6 py-4 w-full h-fit self-center sm:flex hidden"
>
{isToolEnabled ? "Отключить" : "Включить"} инструмент
</Button>
</div>
<div className="col-span-3 grid xl:grid-cols-2">
<div className="xl:block sm:grid grid-cols-2">
<div className="2xl:px-8 2xl:py-6 p-4 border border-[#3D425C] grid grid-cols-2 gap-4 items-center">
<div className="flex flex-col gap-4">
<p className="text-sm">Срок реализации</p>
<p
className={`2xl:text-5xl text-[32px] font-gilroy font-medium flex items-end gap-1.5 w-fit ${
isToolEnabled ? "text-gradient" : ""
}`}
>
<span className="2xl:leading-none leading-[115%]">
{isToolEnabled
? implementationPeriod
: oldImplementationPeriod}
</span>
<span className="2xl:text-2xl xl:text-xl text-base">
{isToolEnabled
? implementationPeriodEnding
: oldImplementationPeriodEnding}
</span>
</p>
</div>
<p
className={`text-xs transition-opacity ${
isToolEnabled ? `opacity-100` : `opacity-0`
}`}
>
На{" "}
<span className="text-[#798FFF]">
{diffImplementationPeriod} {diffImplementationPeriodEnding}
</span>{" "}
вы сократили
<br />
срок реализации проекта
</p>
</div>
<div className="2xl:px-8 2xl:py-6 p-4 border xl:border-t-0 sm:border-t border-t-0 xl:border-l sm:border-l-0 border-l border-[#3D425C] grid grid-cols-2 gap-4 items-center">
<div className="flex flex-col gap-4">
<p className="text-sm">Месячный доход</p>
<p
className={`2xl:text-5xl text-[32px] font-gilroy font-medium flex items-end gap-1.5 w-fit ${
isToolEnabled ? "text-gradient" : ""
}`}
>
<span className="2xl:leading-none leading-[115%]">
{isToolEnabled ? monthlyIncome : oldMonthlyIncome}
</span>
<span className="2xl:text-2xl xl:text-xl text-base">
млн руб.
</span>
</p>
</div>
<p
className={`text-xs transition-opacity ${
isToolEnabled ? `opacity-100` : `opacity-0`
}`}
>
На{" "}
<span className="text-[#798FFF]">
{diffMonthlyIncome} млн руб.
</span>{" "}
в месяц
<br />
вы заработали больше
</p>
</div>
</div>
<div className="2xl:px-8 2xl:py-6 sm:px-6 sm:py-4 p-4 border xl:border-l-0 xl:border-t border-t-0 border-[#3D425C] flex flex-col gap-4">
<p className="text-sm">Статистика продаж</p>
<div className="flex flex-col 2xl:gap-3 sm:gap-1.5 gap-2">
<div className="flex sm:gap-4 gap-2">
<div className="flex flex-col 2xl:gap-3 sm:gap-1.5 gap-2 justify-around">
<p className="2xl:text-sm text-xs">100%</p>
<p className="2xl:text-sm text-xs">
{isToolEnabled ? 48 : 30}%
</p>
<p className="2xl:text-sm text-xs">
{isToolEnabled ? 42 : 30}%
</p>
</div>
<div className="w-full flex flex-col 2xl:gap-3 sm:gap-1.5 gap-2 justify-around">
<div className="bg-[#212431] rounded-full flex justify-center">
<div
className={`2xl:py-3.5 py-2 rounded-full w-full transition-all ${
isToolEnabled ? "bg-gradient" : "bg-[#52587A]"
}`}
>
<p className="text-center">{consultations}</p>
</div>
</div>
<div className="bg-[#212431] rounded-full flex justify-center">
<div
className={`2xl:py-3.5 py-2 rounded-full transition-all ${
isToolEnabled
? "w-[60%] bg-gradient"
: "w-[50%] bg-[#52587A]"
}`}
>
<p className="text-center">
{isToolEnabled ? reservation : oldReservation}
</p>
</div>
</div>
<div className="bg-[#212431] rounded-full flex justify-center">
<div
className={`2xl:py-3.5 py-2 rounded-full transition-all ${
isToolEnabled
? "w-[32%] bg-gradient"
: "w-[22%] bg-[#52587A]"
}`}
>
<p className="text-center">
{isToolEnabled ? sales : oldSales}
</p>
</div>
</div>
</div>
<div className="flex flex-col 2xl:gap-3 sm:gap-1.5 gap-2 justify-around">
<p className="2xl:text-sm text-xs text-[#8088A7] whitespace-nowrap">
<span className="sm:inline hidden">
Консультаций в офисе
</span>
<span className="sm:hidden inline">Консультации</span>
</p>
<p className="2xl:text-sm text-xs text-[#8088A7] whitespace-nowrap">
Бронь<span className="sm:inline hidden"> квартиры</span>
</p>
<p className="2xl:text-sm text-xs text-[#8088A7] whitespace-nowrap">
Продажа
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<Button
icon={
isToolEnabled ? (
<CloseIcon className="2xl:w-8 w-6 2xl:h-8 h-6" />
) : (
<ArrowRightIcon className="2xl:w-8 2xl:h-8" />
)
}
color={isToolEnabled ? "secondary" : "primary"}
onClick={() => setIsToolEnabled((prev) => !prev)}
className="px-6 py-4 w-full h-fit self-center sm:hidden flex"
>
{isToolEnabled ? "Отключить" : "Включить"} инструмент
</Button>
</div>
);
}
export default Calc;
+17
View File
@@ -0,0 +1,17 @@
.range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #798fff;
cursor: pointer;
}
.range-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #798fff;
cursor: pointer;
}
+26
View File
@@ -0,0 +1,26 @@
import { useState } from "react";
import "./CalcRangeSlider.css";
interface CalcRangeSliderProps {
defaultValue: number;
min: number;
max: number;
handleChange?: (value: number) => void;
}
function CalcRangeSlider({ defaultValue, min, max }: CalcRangeSliderProps) {
const [value, setvalue] = useState<number>(defaultValue);
return (
<input
type="range"
min={min}
max={max}
className="transition-all w-full h-0.5 bg-[#798FFF] range-slider"
value={value}
onChange={(e) => setvalue(+e.target.value)}
/>
);
}
export default CalcRangeSlider;
+76
View File
@@ -0,0 +1,76 @@
import { useRef, useState } from "react";
import ChevronDown from "./icons/ChevronDown";
import CheckIcon from "./icons/CheckIcon";
import { useOnClickOutside } from "usehooks-ts";
interface CalcSelectProps {
label: string;
placeholder: string;
defaultOption: string;
options: string[];
handleSelect: (option: string) => void;
}
function CalcSelect({
label,
placeholder,
defaultOption = "",
options,
handleSelect,
}: CalcSelectProps) {
const [selectedOption, setSelectedOption] = useState<string>(defaultOption);
const [isShow, setIsShow] = useState<boolean>(false);
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setIsShow(false));
function handleClick(option: string) {
setSelectedOption(option);
setIsShow(false);
handleSelect(option);
}
return (
<div ref={ref} className="relative z-10">
<div
className={`border ${
isShow ? "border-[#798FFF]" : "border-[#3D425C]"
} 2xl:p-6 2xl:pr-8 p-4 pr-6 flex flex-col justify-center h-24 cursor-pointer transition-colors`}
onClick={() => setIsShow((prev) => !prev)}
>
<div className="relative h-full cursor-pointer">
<div className="flex flex-col justify-between h-full ">
<label className="2xl:text-base text-sm opacity-50 font-gilroy font-medium leading-none cursor-pointer">
{label}
</label>
<input
readOnly
type="text"
placeholder={placeholder}
className="2xl:text-base text-sm w-full bg-transparent outline-none cursor-pointer"
value={selectedOption}
/>
</div>
</div>
<span className="absolute self-end">
<ChevronDown />
</span>
</div>
{isShow && (
<div className="absolute max-h-[411.5px] overflow-y-auto flex flex-col border border-[#3D425C] w-full py-2 backdrop-blur-[14px] bg-[#14161F] bg-opacity-50">
{options.map((option, index) => (
<button
key={index}
className="2xl:pl-6 2xl:pr-8 2xl:py-3 pl-4 pr-6 py-2 2xl:text-base text-sm hover:bg-white hover:bg-opacity-10 transition-colors w-full text-left flex justify-between"
onClick={() => handleClick(option)}
>
<span>{option}</span>
{option === selectedOption && <CheckIcon />}
</button>
))}
</div>
)}
</div>
);
}
export default CalcSelect;
+20
View File
@@ -0,0 +1,20 @@
.contacts-field:focus ~ .contacts-placeholder {
top: 0;
}
.contacts-field:focus ~ .contacts-placeholder-2 {
opacity: 0;
}
.contacts-field:valid ~ .contacts-placeholder {
top: 0;
}
.contacts-field:valid ~ .contacts-placeholder-2 {
opacity: 0;
}
.contacts-field::placeholder {
font-weight: 600;
color: #77787d;
}
+212
View File
@@ -0,0 +1,212 @@
import { ChangeEvent, FormEvent, useState } from "react";
import AsteriskIcon from "./icons/AsteriskIcon";
import InputMask from "react-input-mask";
import "./ContactsForm.css";
import Button from "./Button";
import SendIcon from "./icons/SendIcon";
import LoaderIcon from "./icons/LoaderIcon";
import api from "../utils/api";
import CheckGradientIcon from "./icons/CheckGradientIcon";
import useModalStore from "../stores/useModalStore";
import Close2Icon from "./icons/Close2Icon";
function ContactsForm() {
const [name, setName] = useState<string>("");
const [phone, setPhone] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSend, setIsSend] = useState<boolean>(false);
const [setModal] = useModalStore((state) => [state.setModal]);
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
sendMail();
}
async function sendMail() {
setIsLoading(true);
try {
await api
.post("mail", {
json: {
fullname: name,
phone,
email,
request: description,
},
})
.json();
setIsSend(true);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
if (error instanceof Error) {
alert(error.message);
}
}
}
return (
<>
{!isSend ? (
<div className="flex flex-col gap-5">
<div className="flex justify-between items-center">
<p className="font-gilroy text-gradient sm:text-2xl text-xl w-fit font-semibold">
Свяжитесь с нами
</p>
<button
onClick={() => setModal(null)}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Close2Icon />
</button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div>
<div className="relative">
<input
required
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="feedback-field bg-transparent border border-[#3D425C] rounded-none sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Имя</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<InputMask
required
type="tel"
mask={"+999999999999999"}
maskChar={null}
value={phone}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPhone(e.target.value)
}
className={[
"feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full",
].join(" ")}
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Телефон</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<input
required
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute sm:pt-4 sm:pb-4 sm:px-4 sm:top-4 pt-3 pb-3 px-3 top-3 w-full opacity-50 transition-all pointer-events-none flex justify-between items-center">
<span>Email</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<textarea
placeholder="Опишите вашу задачу"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="feedback-field bg-transparent resize-none border rounded-none border-t-0 border-[#3D425C] p-4 sm:min-h-[192px] min-h-[128px] outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
></textarea>
</div>
<div
className="border border-t-0 border-[#3D425C] 2xl:p-6 p-4 sm:mt-0 flex items-center"
style={{ marginTop: "-6px" }}
>
<div className="text-xs leading-tight">
Нажимая кнопку отправить, вы принимаете{" "}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
условия использования
</a>{" "}
и{" "}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
политику конфиденциальности
</a>
</div>
</div>
<div className="border border-t-0 border-[#3D425C] 2xl:p-6 p-4 sm:mt-0 text-xs flex items-center gap-2">
<div className="flex gap-2">
<div className="">
<AsteriskIcon />
</div>
<p></p>
<p>
Звездочкой отмечены обязательные
<br />
для заполнения поля
</p>
</div>
</div>
</div>
<Button
width="full"
disabled={isLoading}
icon={
isLoading ? (
<LoaderIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6 animate-spin" />
) : (
<SendIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6" />
)
}
className="py-4"
>
Отправить
</Button>
</form>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<p className="font-gilroy text-gradient sm:text-2xl text-xl w-fit font-semibold flex items-center gap-2">
<span>Заявка отправлена</span>
<CheckGradientIcon className="lg:w-8 lg:h-8 w-6 h-6" />
</p>
<button
onClick={() => setModal(null)}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Close2Icon />
</button>
</div>
<div className="flex flex-col gap-2">
<p className="font-gilroy leading-snug lg:text-2xl text-xl font-semibold">
Спасибо за подачу заявки!
</p>
<p className="lg:text-base text-sm">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся
с вами для уточнения деталей проекта.
</p>
</div>
</div>
)}
</>
);
}
export default ContactsForm;
+34
View File
@@ -0,0 +1,34 @@
import { motion } from "framer-motion";
interface ExampleCardProps {
title: string;
company: string;
image: string;
}
function ExampleCard({ title, company, image }: ExampleCardProps) {
return (
<div className="flex flex-col gap-4 xl:mb-16 sm:mb-10 mb-8">
<motion.div
initial={{ opacity: 0, translateY: 64 }}
whileInView={{ opacity: 1, translateY: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 1, ease: [0.58, 0.12, 0.27, 0.98] }}
className="aspect-video bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: `url('${image}')` }}
></motion.div>
<motion.div
initial={{ opacity: 0, translateY: 64 }}
whileInView={{ opacity: 1, translateY: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 1, ease: [0.58, 0.12, 0.27, 0.98] }}
className="flex flex-col gap-1"
>
<p className="font-medium xl:text-xl text-base font-gilroy">{title}</p>
<p className="xl:text-sm text-xs">{company}</p>
</motion.div>
</div>
);
}
export default ExampleCard;
+47
View File
@@ -0,0 +1,47 @@
/* eslint-disable @next/next/no-img-element */
import ArrowIcon from "./icons/ArrowIcon";
interface FeatureItemProps {
title: string;
desc: string;
video: string;
handleHoverStart?: (video: string) => void;
handleHoverEnd?: () => void;
}
function FeatureItem({
title,
desc,
video,
handleHoverStart,
handleHoverEnd,
}: FeatureItemProps) {
return (
<div
className="group relative"
onMouseEnter={() => handleHoverStart && handleHoverStart(video)}
onMouseLeave={() => handleHoverEnd && handleHoverEnd()}
>
<div className="relative border-b border-[#3D425C] py-16 flex justify-between items-center h-36 cursor-default overflow-hidden ">
<div className="overflow-hidden py-2">
<p className="group-hover:opacity-0 group-hover:-translate-y-[125%] transition-all 2xl:text-[32px] xl:text-2xl text-xl font-gilroy font-medium leading-none duration-300">
{title}
</p>
</div>
<p className="group-hover:opacity-100 group-hover:translate-y-0 opacity-0 translate-y-[100%] absolute transition-all w-4/5 duration-300 2xl:text-base text-sm">
{desc}
</p>
<div className="group-hover:opacity-100 opacity-0 blur-[10px] absolute -top-[100%] left-0 transition-opacity duration-300">
<img src="/images/blendings/8.svg" alt="" />
</div>
</div>
<div className="group-hover:opacity-0 group-hover:translate-x-[100%] transition-all absolute top-[50%] right-0 -translate-y-[50%] mt-0.5 duration-300">
<ArrowIcon />
</div>
</div>
);
}
export default FeatureItem;
+36
View File
@@ -0,0 +1,36 @@
import { useEffect, useRef } from "react";
interface FeatureVideoViewBoxProps {
video: string;
}
function FeatureVideoViewBox({ video }: FeatureVideoViewBoxProps) {
// const videoRef = useRef<HTMLVideoElement>(null);
const videoContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!video || !videoContainerRef.current) return;
const videoEl = document.createElement("video");
videoEl.src = video;
videoEl.muted = true;
videoEl.autoplay = true;
videoEl.loop = true;
videoEl.playsInline = true;
videoEl.preload = "metadata";
videoEl.classList.add("absolute", "h-fit");
// videoEl.onloadeddata = () => console.log("onloadeddata");
videoContainerRef.current.appendChild(videoEl);
if (videoContainerRef.current.childElementCount > 1) {
setTimeout(() => {
videoContainerRef.current?.firstElementChild?.remove();
}, 5000);
}
}, [video]);
return <div ref={videoContainerRef} className="relative h-fit"></div>;
}
export default FeatureVideoViewBox;
+182
View File
@@ -0,0 +1,182 @@
import { ChangeEvent, FormEvent, useState } from "react";
import InputMask from "react-input-mask";
import AsteriskIcon from "./icons/AsteriskIcon";
import SendIcon from "./icons/SendIcon";
import CheckGradientIcon from "./icons/CheckGradientIcon";
import LoaderIcon from "./icons/LoaderIcon";
import Button from "./Button";
import api from "../utils/api";
function FeedbackForm() {
const [name, setName] = useState<string>("");
const [phone, setPhone] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [isSend, setIsSend] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
async function sendMail(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setIsLoading(true);
try {
await api
.post("mail", {
json: {
fullname: name,
phone,
email,
request: description,
},
})
.json();
setIsSend(true);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
if (error instanceof Error) {
alert(error.message);
}
}
}
return (
<form
className="grid lg:grid-cols-3 sm:grid-cols-2 relative"
onSubmit={(e) => void sendMail(e)}
>
<div className="relative col-span-1">
<input
required
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="feedback-field bg-transparent border border-[#3D425C] rounded-none lg:p-6 lg:pt-14 p-4 pt-12 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute lg:top-4 top-5 left-0 w-full lg:p-6 p-4 opacity-50 transition-all pointer-events-none flex justify-between">
<span>Имя</span>
<AsteriskIcon />
</p>
</div>
<div className="relative">
<InputMask
required
type="tel"
mask={"+999999999999999"}
maskChar={null}
value={phone}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPhone(e.target.value)
}
className={[
"feedback-field bg-transparent border rounded-none sm:border-l-0 sm:border-t border-t-0 border-l border-[#3D425C] lg:p-6 lg:pt-14 p-4 pt-12 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full",
].join(" ")}
/>
<p className="feedback-placeholder lg:text-base text-sm absolute lg:top-4 top-5 left-0 w-full lg:p-6 p-4 opacity-50 transition-all pointer-events-none flex justify-between">
<span>Телефон</span>
<AsteriskIcon />
</p>
</div>
<div className="relative lg:col-span-1 sm:col-span-2 col-span-1">
<input
required
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="feedback-field bg-transparent border rounded-none lg:border-l-0 lg:border-t border-t-0 border-[#3D425C] lg:p-6 lg:pt-14 p-4 pt-12 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
/>
<p className="feedback-placeholder lg:text-base text-sm absolute lg:top-4 top-5 left-0 w-full lg:p-6 p-4 opacity-50 transition-all pointer-events-none flex justify-between">
<span>Email</span>
<AsteriskIcon />
</p>
</div>
<div className="relative lg:col-span-3 sm:col-span-2 h-[194px]">
<textarea
placeholder="Опишите вашу задачу"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="feedback-field bg-transparent resize-none border rounded-none border-t-0 border-[#3D425C] lg:p-6 p-4 h-full outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full"
></textarea>
</div>
<div className="2xl:pt-6 2xl:pr-6 pt-4 sm:pr-4 lg:order-none order-last flex items-center">
<Button
width="full"
disabled={isLoading}
icon={
isLoading ? (
<LoaderIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6 animate-spin" />
) : (
<SendIcon className="relative 2xl:w-8 2xl:h-8 w-6 h-6" />
)
}
className="py-4"
>
Отправить
</Button>
</div>
<div className="border sm:border-t-0 border-t border-[#3D425C] 2xl:p-6 p-4 sm:mt-0 mt-6 flex items-center">
<div className="text-xs leading-tight">
Нажимая кнопку отправить, вы принимаете{" "}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
условия использования
</a>{" "}
и{" "}
<a
href="https://graff.tech/privacypolicy"
target="_blank"
className="text-[#798FFF] cursor-pointer opacity-95 hover:opacity-100 transition-all"
>
политику конфиденциальности
</a>
</div>
</div>
<div className="border border-t-0 sm:border-l-0 border-[#3D425C] 2xl:p-6 p-4 text-xs flex items-center gap-2">
<div className="flex gap-2">
<div className="">
<AsteriskIcon />
</div>
<p></p>
<p className="leading-tight">
Звездочкой отмечены обязательные
<br />
для заполнения поля
</p>
</div>
</div>
{isSend && (
<div className="absolute top-0 left-0 w-full h-full bg-[#14161F] border border-[#3D425C] p-6 flex flex-col justify-between">
<p className="text-gradient text-xl font-gilroy leading-tight font-semibold flex items-center gap-2">
<span>Заявка отправлена</span>
<CheckGradientIcon className="lg:w-8 lg:h-8 w-6 h-6" />
</p>
<div className="flex flex-col gap-2">
<p className="font-gilroy leading-snug lg:text-2xl text-xl font-semibold">
Спасибо за подачу заявки!
</p>
<p className="lg:w-1/2 sm:w-2/3 lg:text-base text-sm">
Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся
с вами для уточнения деталей проекта.
</p>
</div>
</div>
)}
</form>
);
}
export default FeedbackForm;
+18
View File
@@ -0,0 +1,18 @@
import { ReactNode } from "react";
interface Heading2Props {
children: ReactNode;
className?: string;
}
function Heading2({ children, className = "" }: Heading2Props) {
return (
<h2
className={`2xl:text-[64px] xl:text-5xl text-[40px] text-gradient font-gilroy font-medium leading-none w-fit ${className}`}
>
{children}
</h2>
);
}
export default Heading2;
+20
View File
@@ -0,0 +1,20 @@
import useModalStore from "../stores/useModalStore";
function ModalContainer() {
const [modal] = useModalStore((state) => [state.modal]);
if (modal) {
return (
<div
// onClick={() => setModal(null)}
className={`fixed p-8 top-0 left-0 z-10 w-full h-full flex justify-center items-center bg-black bg-opacity-80 overflow-auto transition-opacity`}
>
<div onClick={(e) => e.stopPropagation()} className="cursor-default">
{modal}
</div>
</div>
);
}
}
export default ModalContainer;
+24
View File
@@ -0,0 +1,24 @@
import { motion } from "framer-motion";
import PlusIcon from "./icons/PlusIcon";
interface MoreProjectButtonProps {
onClick?: () => void;
}
function MoreProjectButton({ onClick }: MoreProjectButtonProps) {
return (
<motion.button
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, ease: [0.58, 0.12, 0.27, 0.98], delay: 0.2 }}
className="sm:aspect-[4/3] border border-[#3D425C] rounded-[48px] px-6 py-4 flex sm:flex-col sm:justify-center justify-between items-center gap-2"
onClick={onClick}
>
<p className="font-gilroy font-medium leading-none">Показать еще</p>
<PlusIcon />
</motion.button>
);
}
export default MoreProjectButton;
+22
View File
@@ -0,0 +1,22 @@
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
interface ProgressPieProps {
value: number;
}
function ProgressPie({ value }: ProgressPieProps) {
return (
<CircularProgressbar
value={value}
strokeWidth={50}
styles={buildStyles({
trailColor: "#3D425C",
pathColor: "#798FFF",
strokeLinecap: "butt",
})}
className="w-5 h-5"
/>
);
}
export default ProgressPie;
+92
View File
@@ -0,0 +1,92 @@
import { motion } from "framer-motion";
import ProgressPie from "./ProgressPie";
import TouchScreenIcon from "./icons/TouchScreenIcon";
import VRIcon from "./icons/VRIcon";
import MobileIcon from "./icons/MobileIcon";
import { IProject } from "../types/IProject";
import { format } from "date-fns";
function ProjectCard({
name,
company,
city,
image,
stage = 6,
releaseDate = format(new Date(), "yyyy-MM-dd"),
devices = [],
}: IProject) {
const stagePercentage = Math.round((100 / 6) * stage);
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, ease: [0.58, 0.12, 0.27, 0.98], delay: 0.2 }}
className="group relative aspect-[4/3] p-4 flex items-end overflow-hidden"
>
<div
className="group-hover:scale-110 transition-transform duration-500 absolute top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${process.env.VITE_API_URL}/upload/${image})`,
}}
></div>
<div className="absolute top-0 left-0 w-full h-full bg-gradient-card"></div>
<div className="relative flex flex-col gap-4">
<div>
<p className="2xl:text-2xl text-xl font-gilroy font-medium">{name}</p>
<p className="2xl:text-sm text-xs">
{company !== "-" && `${company},`} {city}
</p>
</div>
<div className="flex gap-2">
{stage < 6 ? (
<div className="bg-[#14161F] px-3 py-2 rounded-full w-fit flex items-center gap-1">
<p className="font-gilroy font-medium leading-none">
{stagePercentage}%
</p>
<ProgressPie value={stagePercentage} />
</div>
) : (
<div className="bg-gradient py-2.5 px-4 rounded-full">
<p className="font-gilroy font-medium leading-none">
{new Date(releaseDate).getFullYear()}
</p>
</div>
)}
{devices.length > 0 && (
<>
{devices.includes("stream") && (
<div className="bg-[#14161F] px-3 py-2 rounded-full w-fit flex items-center gap-2">
<div className="w-2 h-2 bg-gradient rounded-full"></div>
<p className="font-gilroy font-semibold leading-none text-gradient">
Stream
</p>
</div>
)}
{devices.includes("touch") && (
<div className="bg-[#14161F] p-2 rounded-full w-fit flex items-center gap-2">
<TouchScreenIcon />
</div>
)}
{devices.includes("mobile") && (
<div className="bg-[#14161F] p-2 rounded-full w-fit flex items-center gap-2">
<MobileIcon />
</div>
)}
{devices.includes("vr") && (
<div className="bg-[#14161F] p-2 rounded-full w-fit flex items-center gap-2">
<VRIcon />
</div>
)}
</>
)}
</div>
</div>
</motion.div>
);
}
export default ProjectCard;
+39
View File
@@ -0,0 +1,39 @@
.rangeslider {
margin: 10px 0;
box-shadow: none !important;
height: 2px !important;
background-color: #3d425c !important;
}
.rangeslider__fill {
box-shadow: none !important;
background-color: #798fff !important;
}
.rangeslider__handle {
box-shadow: none !important;
background-color: #798fff !important;
width: 24px !important;
height: 24px !important;
border: none !important;
border-radius: 50% !important;
}
.rangeslider__handle::after {
opacity: 0;
background-color: rgba(121, 143, 255, 0.5) !important;
width: 24px !important;
height: 24px !important;
top: 0 !important;
left: 0 !important;
box-shadow: none !important;
transition: all 0.2s;
}
.rangeslider__handle:hover.rangeslider__handle::after {
opacity: 1;
width: 40px !important;
height: 40px !important;
top: calc(-50% + 4px) !important;
left: calc(-50% + 4px) !important;
}
+24
View File
@@ -0,0 +1,24 @@
import Slider from "react-rangeslider";
import "react-rangeslider/lib/index.css";
import "./RangeSlider.css";
import { useState } from "react";
interface RangeSliderProps {
defaultValue: number;
}
function RangeSlider({ defaultValue }: RangeSliderProps) {
const [value, setValue] = useState<number>(defaultValue);
return (
<Slider
min={1}
max={200}
value={value}
onChange={(value) => setValue(value)}
tooltip={false}
/>
);
}
export default RangeSlider;
+75
View File
@@ -0,0 +1,75 @@
/* eslint-disable @next/next/no-img-element */
import Button from "./Button";
import ArrowIcon from "./icons/ArrowIcon";
import ArrowRightIcon from "./icons/ArrowRightIcon";
interface StreamButton {
icon: string;
title: string;
location: string;
background: string;
link: string;
}
function StreamButton({
icon,
title,
location,
background,
link,
}: StreamButton) {
return (
<>
<a
href={link}
target="_blank"
className="group relative xl:grid hidden grid-cols-4 gap-4 h-36 border-b border-[#3D425C] cursor-pointer"
>
<div className="col-span-2 flex items-center xl:gap-12 gap-6">
<img src={icon} alt="" />
<p className="xl:text-2xl text-xl font-gilroy font-medium">{title}</p>
</div>
<div className="flex items-center xl:text-base text-sm">
<p>{location}</p>
</div>
<div className="xl:flex hidden items-center justify-between">
<p>Демоверсия</p>
<ArrowIcon />
</div>
<div
className="group-hover:opacity-100 opacity-0 transition-opacity duration-300 absolute top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(90deg, #14161F 1.07%, rgba(20, 22, 31, 0.30) 49.98%, #14161F 98.78%), url(${background})`,
}}
>
<div className="w-full h-full flex flex-col gap-2 justify-center items-center">
<p className="text-2xl font-gilroy font-medium">
начать демонстрацию
</p>
<p>{title}</p>
</div>
</div>
</a>
<div className="xl:hidden grid sm:grid-cols-2 sm:gap-3 gap-6 border-b border-[#3D425C] py-6">
<div className="flex items-center gap-6">
<img src={icon} alt="" />
<div className="">
<p className="xl:text-2xl text-xl font-gilroy font-medium">
{title}
</p>
<p className="text-sm sm:hidden block">{location}</p>
</div>
</div>
<div className="flex items-center justify-between gap-3">
<p className="text-sm sm:block hidden">{location}</p>
<Button icon={<ArrowRightIcon />}>
<a href={link}>Демоверсия</a>
</Button>
</div>
</div>
</>
);
}
export default StreamButton;
+166
View File
@@ -0,0 +1,166 @@
/* eslint-disable react-hooks/rules-of-hooks */
/* eslint-disable react-hooks/exhaustive-deps */
import { motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import ArrowLeftIcon from "./icons/ArrowLeftIcon";
import ArrowRightIcon from "./icons/ArrowRightIcon";
import { useSwipeable } from "react-swipeable";
function VideoSliderMobile() {
const [items] = useState<any[]>([
{
title: "Виртуальный тур по жилому комплексу",
desc: "Клиент лично оценит угол инсоляции, малые архитектурные формы и ландшафт, перемещаясь по комплексу с помощью тапа.",
video: "/videos/features/virtual_tour.mp4",
},
{
title: "Вся инфрастуктура на одном экране",
desc: "Возможность оценить инфраструктуру района покажет важные для клиента точки интереса и время, за которое он сможет до них дойти.",
video: "/videos/features/nks_infra.mp4",
},
{
title: "Конфигуратор интерьера",
desc: "Клиент может свободно выбирать мебель и дизайн с помощью конфигуратора интерьера. Возможно выбрать стиль всей квартиры или изменить отдельные детали.",
video: "/videos/features/uralsky.mp4",
},
{
title: "Параметрический поиск квартир",
desc: "Фильтр позволит отметить конкретные преимущества, определить количество комнат, желаемый этаж, цену, и получить выборку подходящих вариантов.",
video: "/videos/features/parametric.mp4",
},
{
title: "Любой рендер за несколько секунд",
desc: "Когда для рекламы вам понадобится любой объект с любого ракурса, просто сделайте фотографию внутри презентации.",
video: "/videos/features/render.mp4",
},
{
title: "Формирование вишлиста",
desc: "Клиент может добавить варианты квартир в избранное, сравнить их между собой по основным параметрам и выбрать свою будущую квартиру.",
video: "/videos/features/wish.mp4",
},
{
title: "Интеграция с CRM-системой",
desc: "Приложение передает информацию о клиенте в CRM-систему застройщика и получает актуальную информацию по ценам и статусам квартир.",
video: "/videos/features/integra_crm.mp4",
},
{
title: "Отправка коммерческого предложения",
desc: "Коммерческое предложение с выбранными квартирами может быть отправлено клиенту на почту или распечатано и отдано лично в руки.",
video: "/videos/features/send.mp4",
},
// {
// title: "Интерактивная инсоляция",
// desc: "Функция позволяет в режиме реального времени увидеть уровень освещенности выбранной квартиры, а если вы изучаете экстерьер жилого комплекса – функция покажет архитектурную подсветку.",
// video: "",
// },
// {
// title: "Подбор квартир на генплане",
// desc: "Сделать генплан удобным инструментом выбора квартиры поможет подсветка выбранных квартир прямо на фасаде Жилого комплекса.",
// video: "",
// },
]);
const [activeIndex, setActiveIndex] = useState<number>(0);
const videoRefs = items.map(() => useRef<HTMLVideoElement>(null));
const handlers = useSwipeable({
onSwiped: (e) => {
if (e.dir === "Left") {
handleClickNext();
}
if (e.dir === "Right") {
handleClickPrev();
}
},
});
function handleClickPrev() {
if (activeIndex === 0) {
setActiveIndex(items.length - 1);
return;
}
setActiveIndex((prev) => prev - 1);
}
function handleClickNext() {
if (activeIndex === items.length - 1) {
setActiveIndex(0);
return;
}
setActiveIndex((prev) => prev + 1);
}
useEffect(() => {
items.forEach((_, index) => {
if (activeIndex === index) {
videoRefs[index].current?.play();
} else {
videoRefs[index].current?.pause();
}
});
}, [activeIndex]);
return (
<div
{...handlers}
className="xl:hidden flex flex-col sm:gap-6 gap-4 border-b border-[#3D425C] pb-5"
>
<div
// ref={videosContainerRef}
className={`relative flex sm:gap-[88px] items-start transition-all duration-500`}
style={{ left: `-${activeIndex * 100}%` }}
>
{items.map((item, index) => (
<motion.video
key={index}
ref={videoRefs[index]}
src={item.video}
muted
loop
playsInline
preload="metadata"
className={`relative aspect-video transition-all duration-500 sm:w-[calc(100%-88px)] ${
index !== activeIndex
? "sm:scale-[70%] scale-[80%] sm:-translate-y-[15%] -translate-y-[10%] sm:-translate-x-[calc(15%+88px-12px)] -translate-x-[calc(10%-8px)]"
: ""
}`}
/>
))}
</div>
<div className="flex justify-between sm:gap-[30px] gap-3 sm:h-auto h-[168px]">
<div className="flex flex-col gap-3">
<p className="text-xl font-gilroy font-medium">
{items[activeIndex].title}
</p>
<p className="text-sm">{items[activeIndex].desc}</p>
</div>
<div className="relative flex flex-col items-center gap-4">
<div className="sm:absolute -top-[64px]">
<p className="sm:text-2xl text-xl font-gilroy font-medium">
{activeIndex + 1}/{items.length}
</p>
</div>
<div className="flex flex-col gap-2 self-end">
<button
onClick={handleClickNext}
className="sm:p-4 p-2 border border-[#3D425C] rounded-full outline-none"
>
<ArrowRightIcon />
</button>
<button
onClick={handleClickPrev}
className="sm:p-4 p-2 border border-[#3D425C] rounded-full outline-none"
>
<ArrowLeftIcon />
</button>
</div>
</div>
</div>
</div>
);
}
export default VideoSliderMobile;
+20
View File
@@ -0,0 +1,20 @@
function ArrowIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 12C5 12.5523 5.44771 13 6 13L15.4817 13L10.9504 17.2724C10.5485 17.6513 10.5299 18.2842 10.9088 18.686C11.2876 19.0879 11.9205 19.1065 12.3224 18.7276L18.686 12.7276C18.8864 12.5387 19 12.2754 19 12C19 11.7246 18.8864 11.4614 18.686 11.2724L12.3224 5.27241C11.9205 4.89354 11.2876 4.91215 10.9088 5.31399C10.5299 5.71583 10.5485 6.34872 10.9504 6.72759L15.4817 11L6 11C5.44772 11 5 11.4477 5 12Z"
fill="white"
/>
</svg>
);
}
export default ArrowIcon;
+25
View File
@@ -0,0 +1,25 @@
interface IconProps {
className?: string;
}
function ArrowLeftIcon({ className }: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19 12C19 11.4477 18.5523 11 18 11L8.5183 11L13.0496 6.72759C13.4515 6.34871 13.4701 5.71582 13.0912 5.31398C12.7124 4.91215 12.0795 4.89353 11.6776 5.27241L5.31399 11.2724C5.11359 11.4613 5 11.7246 5 12C5 12.2754 5.11359 12.5386 5.31399 12.7276L11.6776 18.7276C12.0795 19.1065 12.7124 19.0878 13.0912 18.686C13.4701 18.2842 13.4515 17.6513 13.0496 17.2724L8.5183 13L18 13C18.5523 13 19 12.5523 19 12Z"
fill="currentColor"
/>
</svg>
);
}
export default ArrowLeftIcon;
+25
View File
@@ -0,0 +1,25 @@
interface IconProps {
className?: string;
}
function ArrowRightIcon({ className }: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 12C5 12.5523 5.44771 13 6 13L15.4817 13L10.9504 17.2724C10.5485 17.6513 10.5299 18.2842 10.9088 18.686C11.2876 19.0879 11.9205 19.1065 12.3224 18.7276L18.686 12.7276C18.8864 12.5387 19 12.2754 19 12C19 11.7246 18.8864 11.4614 18.686 11.2724L12.3224 5.27241C11.9205 4.89354 11.2876 4.91215 10.9088 5.31399C10.5299 5.71583 10.5485 6.34872 10.9504 6.72759L15.4817 11L6 11C5.44772 11 5 11.4477 5 12Z"
fill="currentColor"
/>
</svg>
);
}
export default ArrowRightIcon;
+18
View File
@@ -0,0 +1,18 @@
function AsteriskIcon() {
return (
<svg
width="12"
height="13"
viewBox="0 0 12 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.81534 12.2727L4.9858 7.58523L1.02273 10.0994L0 8.30966L4.17614 6.13636L0 3.96307L1.02273 2.1733L4.9858 4.6875L4.81534 0H6.8608L6.69034 4.6875L10.6534 2.1733L11.6761 3.96307L7.5 6.13636L11.6761 8.30966L10.6534 10.0994L6.69034 7.58523L6.8608 12.2727H4.81534Z"
fill="white"
/>
</svg>
);
}
export default AsteriskIcon;
@@ -0,0 +1,41 @@
interface IconProps {
className?: string;
}
function CheckGradientIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Check">
<path
id="Vector 1836 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M26.3298 9.78103C26.819 10.3314 26.7694 11.1742 26.2191 11.6634L14.2191 22.3301C13.6914 22.7991 12.8896 22.7755 12.3904 22.2763L5.72378 15.6097C5.20308 15.089 5.20308 14.2447 5.72378 13.724C6.24448 13.2033 7.0887 13.2033 7.60939 13.724L13.3871 19.5017L24.4474 9.6703C24.9978 9.18107 25.8406 9.23065 26.3298 9.78103Z"
fill="url(#paint0_linear_53_10278)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_53_10278"
x1="5.33325"
y1="32.1907"
x2="29.4088"
y2="29.927"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.167052" stopColor="#798FFF" />
<stop offset="0.963542" stopColor="#D375FF" />
</linearGradient>
</defs>
</svg>
);
}
export default CheckGradientIcon;
+20
View File
@@ -0,0 +1,20 @@
function CheckIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.7474 7.33565C20.1143 7.74843 20.0771 8.3805 19.6644 8.74742L10.6644 16.7474C10.2686 17.0992 9.66729 17.0815 9.29289 16.7071L4.29289 11.7071C3.90237 11.3166 3.90237 10.6834 4.29289 10.2929C4.68342 9.90238 5.31658 9.90238 5.70711 10.2929L10.0404 14.6262L18.3356 7.2526C18.7484 6.88568 19.3805 6.92286 19.7474 7.33565Z"
fill="white"
/>
</svg>
);
}
export default CheckIcon;
+22
View File
@@ -0,0 +1,22 @@
function ChevronDown() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.8">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.2929 16.7071C11.6834 17.0976 12.3166 17.0976 12.7071 16.7071L19.7071 9.70713C20.0976 9.31661 20.0976 8.68344 19.7071 8.29292C19.3165 7.90239 18.6834 7.90239 18.2928 8.29292L12 14.5858L5.70711 8.29292C5.31658 7.90239 4.68342 7.90239 4.29289 8.29292C3.90237 8.68344 3.90237 9.31661 4.29289 9.70713L11.2929 16.7071Z"
fill="white"
/>
</g>
</svg>
);
}
export default ChevronDown;
+26
View File
@@ -0,0 +1,26 @@
interface IconProps {
className?: string;
}
function Close2Icon({ className }: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M12.0002 11.9999L17.6572 6.34331M12.0002 11.9999L6.34337 6.34302M12.0002 11.9999L17.6571 17.6567M12.0002 11.9999L6.34326 17.6568"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
export default Close2Icon;
+28
View File
@@ -0,0 +1,28 @@
interface IconProps {
className?: string;
}
function CloseIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g opacity="0.8">
<path
d="M16.0001 15.9995L23.5427 8.45742M16.0001 15.9995L8.45765 8.45703M16.0001 15.9995L23.5426 23.542M16.0001 15.9995L8.4575 23.5421"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
);
}
export default CloseIcon;
+39
View File
@@ -0,0 +1,39 @@
interface IconProps {
className?: string;
}
function LoaderIcon({ className }: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Circle">
<path
id="Ellipse 221"
d="M18.9999 12C19.5523 12 20.0064 11.5505 19.9376 11.0025C19.745 9.46994 19.1116 8.01808 18.1039 6.82871C16.8797 5.38372 15.1826 4.41989 13.3144 4.10872C11.4463 3.79755 9.52839 4.15922 7.90189 5.12938C6.27539 6.09953 5.04582 7.61525 4.43194 9.40685C3.81806 11.1985 3.85968 13.1497 4.54941 14.9135C5.23914 16.6773 6.53224 18.1392 8.19863 19.0391C9.86502 19.9391 11.7966 20.2186 13.6498 19.828C15.1751 19.5066 16.5658 18.7483 17.6578 17.6559C18.0483 17.2653 17.9652 16.6317 17.529 16.2929C17.0927 15.9542 16.4693 16.0409 16.0629 16.4149C15.2735 17.1413 14.2989 17.6472 13.2373 17.8709C11.8475 18.1638 10.3988 17.9541 9.14904 17.2792C7.89928 16.6043 6.92948 15.5079 6.4122 14.1851C5.89491 12.8623 5.86369 11.3989 6.32409 10.0552C6.78449 8.71152 7.70665 7.57476 8.92649 6.84716C10.1463 6.11956 11.5847 5.84832 12.9858 6.08169C14.3869 6.31506 15.6597 7.03791 16.5778 8.12163C17.2791 8.94938 17.7387 9.94667 17.9167 11.0045C18.0083 11.5492 18.4476 12 18.9999 12Z"
fill="url(#paint0_angular_0_1327)"
/>
</g>
<defs>
<radialGradient
id="paint0_angular_0_1327"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12 12) rotate(45) scale(7.9196)"
>
<stop offset="0.874517" stopColor="white" />
<stop offset="0.982613" stopColor="white" stopOpacity="0" />
</radialGradient>
</defs>
</svg>
);
}
export default LoaderIcon;
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
interface IconProps {
className?: string;
}
function MailIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Mail" opacity="0.8">
<g id="Vector">
<path
d="M4 11.3111C4 10.7929 4.5653 10.4728 5.00965 10.7394L15.314 16.9216C15.7363 17.1749 16.2637 17.1749 16.686 16.9216L26.9903 10.739C27.4347 10.4724 28 10.7924 28 11.3106V22.6665C28 23.4029 27.403 23.9998 26.6667 23.9998H5.33333C4.59695 23.9998 4 23.4029 4 22.6665V11.3111Z"
fill="white"
/>
<path
d="M4.73055 7.90483C4.15076 7.55696 4.3974 6.6665 5.07354 6.6665H26.9265C27.6026 6.6665 27.8492 7.55696 27.2695 7.90483L16.686 14.2549C16.2638 14.5083 15.7362 14.5083 15.314 14.2549L4.73055 7.90483Z"
fill="white"
/>
</g>
</g>
</svg>
);
}
export default MailIcon;
+28
View File
@@ -0,0 +1,28 @@
function MobileIcon() {
return (
<svg
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.83338 1.66602C6.17652 1.66602 4.83337 3.00916 4.83337 4.66602V15.3327C4.83337 16.9895 6.17652 18.3327 7.83337 18.3327H13.5C15.1569 18.3327 16.5 16.9895 16.5 15.3327V4.66602C16.5 3.00916 15.1569 1.66602 13.5 1.66602H7.83338ZM7.50004 3.33268C6.94776 3.33268 6.50004 3.7804 6.50004 4.33268L6.50004 15.666C6.50004 16.2183 6.94776 16.666 7.50004 16.666L13.8334 16.666C14.3857 16.666 14.8334 16.2183 14.8334 15.666V4.33268C14.8334 3.7804 14.3857 3.33268 13.8334 3.33268L7.50004 3.33268Z"
fill="currentColor"
/>
<rect
x="8.16675"
y="2.5"
width="5"
height="1.66667"
rx="0.833333"
fill="currentColor"
/>
</svg>
);
}
export default MobileIcon;
+26
View File
@@ -0,0 +1,26 @@
interface IconProps {
className?: string;
}
function PhoneIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Phone" opacity="0.8">
<path
id="phone"
d="M13.2467 8.50638L10.4411 5.70883C9.93919 5.20838 9.12547 5.20839 8.62358 5.70883L6.29613 8.02957C3.39974 10.9176 7.6513 17.8 10.9553 21.0945C14.2395 24.3692 21.0664 28.5923 23.9628 25.7043L26.2902 23.3835C26.7921 22.8831 26.7921 22.0717 26.2902 21.5713L23.4846 18.7737C22.9827 18.2733 22.169 18.2733 21.6671 18.7737L19.5362 20.8984C19.4067 21.0276 19.2417 21.1047 19.0714 21.0372C18.6101 20.8542 17.4325 20.137 14.6538 17.4066C11.8643 14.6655 11.1485 13.4312 10.9713 12.9337C10.9044 12.7461 10.992 12.5669 11.1332 12.4261L13.2473 10.318C13.7492 9.8176 13.7486 9.00683 13.2467 8.50638Z"
fill="white"
/>
</g>
</svg>
);
}
export default PhoneIcon;
+20
View File
@@ -0,0 +1,20 @@
function PlayIcon() {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g opacity="0.8">
<path
d="M26.5708 15.1425C27.2182 15.5309 27.2182 16.4691 26.5708 16.8575L9.5145 27.0913C8.84797 27.4912 8 27.0111 8 26.2338L8 5.76619C8 4.9889 8.84797 4.50878 9.5145 4.9087L26.5708 15.1425Z"
fill="white"
/>
</g>
</svg>
);
}
export default PlayIcon;
+21
View File
@@ -0,0 +1,21 @@
function PlusIcon() {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.6668 11.9998H18.6668M12.6668 11.9998L12.6668 6M12.6668 11.9998L12.6669 18M12.6668 11.9998H6.66675"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default PlusIcon;
+28
View File
@@ -0,0 +1,28 @@
interface IconProps {
className?: string;
}
function SendIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Send">
<path
id="Vector 164 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M25.2649 8.4215C25.432 7.92033 25.2874 7.36779 24.8963 7.01269C24.5051 6.65759 23.9412 6.56689 23.4585 6.78145L6.56115 14.2914C4.82296 15.0639 5.043 17.5979 6.88835 18.0593L10.0482 18.8492C10.6608 19.0024 11.3097 18.8572 11.7987 18.4577L19.8248 11.8996C20.0112 11.7473 20.2583 11.9935 20.1068 12.1805L14.0759 19.62C13.5817 20.2296 13.4898 21.0719 13.8407 21.7738L15.8654 25.8233C16.6623 27.417 18.9882 27.2516 19.5517 25.5613L25.2649 8.4215Z"
fill="white"
/>
</g>
</svg>
);
}
export default SendIcon;
+28
View File
@@ -0,0 +1,28 @@
interface IconProps {
className?: string;
}
function TelegramIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/Telegram" opacity="0.8">
<path
id="Path-3"
fillRule="evenodd"
clipRule="evenodd"
d="M4.35909 15.8481C10.7925 12.8123 15.0929 10.8501 17.2259 9.88748C23.3497 7.1478 24.6226 6.6665 25.4483 6.6665C25.6203 6.6665 26.0331 6.70353 26.3083 6.92566C26.5148 7.11078 26.5836 7.36994 26.618 7.55505C26.6524 7.74016 26.6868 8.14741 26.6524 8.48062C26.3083 12.2199 24.8978 21.3645 24.1409 25.5481C23.8313 27.3252 23.2121 27.9176 22.6272 27.9916C21.3543 28.1027 20.3566 27.066 19.1181 26.2145C17.1915 24.8447 16.0906 23.9932 14.1984 22.6603C12.031 21.1054 13.4415 20.2539 14.6801 18.884C14.9897 18.5138 20.6662 12.9974 20.7694 12.4791C20.7694 12.405 20.8038 12.1829 20.6662 12.0718C20.5286 11.9608 20.3566 11.9978 20.219 12.0348C20.0126 12.0718 16.9163 14.2932 10.8957 18.6619C10.0012 19.3283 9.20995 19.6245 8.48748 19.6245C7.69621 19.6245 6.18246 19.1432 5.04716 18.7359C3.67103 18.2546 2.57013 17.9955 2.67334 17.181C2.77655 16.7367 3.327 16.2924 4.35909 15.8481Z"
fill="white"
/>
</g>
</svg>
);
}
export default TelegramIcon;
+32
View File
@@ -0,0 +1,32 @@
function TouchScreenIcon() {
return (
<svg
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1403_1738)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.333374 5.5V14.5C0.333374 16.1568 1.67652 17.5 3.33338 17.5L17.3334 17.5C18.9902 17.5 20.3334 16.1568 20.3334 14.5V5.5C20.3334 3.84315 18.9902 2.5 17.3334 2.5H3.33337C1.67652 2.5 0.333374 3.84315 0.333374 5.5ZM2.00019 5.16666V14.8333C2.00019 15.3856 2.4479 15.8333 3.00018 15.8333L7.04121 15.8333C7.01915 15.5578 7.0099 15.2789 7.01117 15C7.01327 14.5397 7.38427 14.1666 7.8445 14.1666C8.30474 14.1666 8.67784 14.5397 8.67784 15V10C8.67784 9.53976 9.05093 9.16666 9.51117 9.16666C9.97141 9.16666 10.3445 9.53976 10.3445 10V13.5185C10.3445 13.0582 10.7176 12.6851 11.1778 12.6851C11.6381 12.6851 12.0112 13.0582 12.0112 13.5185V14.1667C12.0112 13.7064 12.3843 13.3333 12.8445 13.3333C13.3047 13.3333 13.6778 13.7064 13.6778 14.1667V15.1851C13.6778 14.7249 14.0509 14.3518 14.5112 14.3518C14.9714 14.3518 15.3445 14.7249 15.3445 15.1851V15.8333L17.6668 15.8333C18.2191 15.8333 18.6669 15.3856 18.6669 14.8333L18.6669 5.16666C18.6669 4.61438 18.2191 4.16666 17.6669 4.16666H3.00019C2.4479 4.16666 2.00019 4.61438 2.00019 5.16666ZM10.6995 6.00828C10.1364 5.8403 9.54417 5.79267 8.96185 5.86806C8.37951 5.94345 7.81826 6.14043 7.31571 6.44773C6.81307 6.75509 6.37973 7.16641 6.04676 7.65644C5.71369 8.1466 5.48922 8.70362 5.39068 9.29055C5.29212 9.87755 5.32223 10.4785 5.47862 11.0524C5.59961 11.4965 6.05767 11.7583 6.50172 11.6373C6.94577 11.5164 7.20765 11.0583 7.08666 10.6142C6.99362 10.2728 6.97574 9.91552 7.03434 9.56651C7.09293 9.21754 7.22649 8.88572 7.42529 8.59314C7.62406 8.30062 7.88334 8.0542 8.18518 7.86963C8.48699 7.68508 8.82474 7.56638 9.17583 7.52093C9.52692 7.47548 9.88389 7.50423 10.2231 7.6054C10.5622 7.70658 10.8764 7.87803 11.1442 8.109C11.4121 8.34002 11.6277 8.62542 11.7757 8.94668C11.9237 9.268 12.0006 9.61737 12.0006 9.97128C12.0006 10.4315 12.3737 10.8046 12.8339 10.8046C13.2942 10.8046 13.6673 10.4315 13.6673 9.97128C13.6673 9.37584 13.5379 8.78859 13.2894 8.24921C13.0409 7.70998 12.6798 7.23243 12.2327 6.84685C11.7857 6.46135 11.2626 6.17627 10.6995 6.00828Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_1403_1738">
<rect
width="20"
height="20"
fill="currentColor"
transform="translate(0.333374)"
/>
</clipPath>
</defs>
</svg>
);
}
export default TouchScreenIcon;
+28
View File
@@ -0,0 +1,28 @@
interface IconProps {
className?: string;
}
function VKIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/VK" opacity="0.8">
<path
id="Vector"
fillRule="evenodd"
clipRule="evenodd"
d="M5.44687 8H2.38363C1.50842 8 1.33337 8.39579 1.33337 8.83368C1.33337 9.61432 2.37137 13.4863 6.16892 18.6072C8.70003 22.1044 12.2665 24 15.5118 24C17.4592 24 17.6998 23.5789 17.6998 22.8539V20.2105C17.6998 19.3684 17.8836 19.2 18.5007 19.2C18.9558 19.2 19.7347 19.4189 21.5516 21.1048C23.6285 23.1032 23.9707 24 25.1391 24H28.2024C29.0776 24 29.5152 23.5789 29.2623 22.7478C28.9866 21.92 27.9949 20.7183 26.6786 19.2943C25.9645 18.4825 24.8932 17.6076 24.5694 17.1705C24.1143 16.6088 24.2455 16.3587 24.5694 15.8594C24.5694 15.8594 28.3013 10.8008 28.6916 9.08379C28.8859 8.45895 28.6916 8 27.7639 8H24.7015C23.9226 8 23.5638 8.39579 23.3695 8.83368C23.3695 8.83368 21.8116 12.4867 19.6052 14.8598C18.891 15.5469 18.5663 15.7659 18.1768 15.7659C17.9825 15.7659 17.7007 15.5469 17.7007 14.9229V9.08379C17.7007 8.33432 17.474 8 16.8255 8H12.0118C11.5252 8 11.2329 8.34779 11.2329 8.6779C11.2329 9.38779 12.3357 9.552 12.4494 11.5495V15.8905C12.4494 16.8421 12.2709 17.0147 11.8805 17.0147C10.8425 17.0147 8.31669 13.3448 6.81833 9.14611C6.52425 8.32842 6.22931 8 5.44687 8Z"
fill="white"
/>
</g>
</svg>
);
}
export default VKIcon;
+18
View File
@@ -0,0 +1,18 @@
function VRIcon() {
return (
<svg
width="21"
height="12"
viewBox="0 0 21 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.63475 11.5L0.746748 0.299999H2.76275L5.73875 9.276L8.73075 0.299999H10.7307L6.84275 11.5H4.63475ZM18.3114 11.5L15.9434 7.42H13.7674V11.5H11.9274V0.299999H16.4074C18.4074 0.299999 20.0074 1.9 20.0074 3.9C20.0074 5.34 19.0954 6.62 17.7834 7.148L20.3274 11.5H18.3114ZM13.7674 2.028V5.772H16.4074C17.3834 5.772 18.1674 4.94 18.1674 3.9C18.1674 2.844 17.3834 2.028 16.4074 2.028H13.7674Z"
fill="currentColor"
/>
</svg>
);
}
export default VRIcon;
+28
View File
@@ -0,0 +1,28 @@
interface IconProps {
className?: string;
}
function YouTubeIcon({ className }: IconProps) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g id="Icon/YouTube" opacity="0.8">
<path
id="Exclude"
fillRule="evenodd"
clipRule="evenodd"
d="M26.4187 7.2304C27.566 7.54067 28.4695 8.45509 28.7762 9.61601C29.3333 11.7204 29.3333 16.1109 29.3333 16.1109C29.3333 16.1109 29.3333 20.5015 28.7762 22.6058C28.4695 23.7666 27.566 24.6811 26.4187 24.9913C24.339 25.5554 16 25.5554 16 25.5554C16 25.5554 7.66111 25.5554 5.58145 24.9913C4.43418 24.6811 3.53051 23.7666 3.22388 22.6058C2.66663 20.5015 2.66663 16.1109 2.66663 16.1109C2.66663 16.1109 2.66663 11.7204 3.22388 9.61601C3.53051 8.45509 4.43418 7.54067 5.58145 7.2304C7.66111 6.6665 16 6.6665 16 6.6665C16 6.6665 24.339 6.6665 26.4187 7.2304ZM13.7777 12.2219V19.9997L20.4444 16.1109L13.7777 12.2219Z"
fill="white"
/>
</g>
</svg>
);
}
export default YouTubeIcon;
@@ -0,0 +1,292 @@
/* eslint-disable @next/next/no-img-element */
/* eslint-disable react-hooks/exhaustive-deps */
import { ChangeEvent, useEffect, useState } from "react";
import api from "../../utils/api";
import Button from "../Button";
import { IProject } from "../../types/IProject";
import useModalStore from "../../stores/useModalStore";
import { format } from "date-fns";
import Close2Icon from "../icons/Close2Icon";
function CreateProjectModal() {
const [project, setProject] = useState<IProject>({
name: "",
company: "",
city: "",
image: "",
releaseDate: format(new Date(), "yyyy-MM-dd"),
devices: [],
});
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState<string>();
const [setModal] = useModalStore((state) => [state.setModal]);
function handleChangeFile(e: ChangeEvent<HTMLInputElement>) {
if (!e.target.files) return;
const targetFile = e.target.files[0];
setFile(targetFile);
setPreviewFile(URL.createObjectURL(targetFile));
}
async function uploadFile() {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
try {
const { file }: { file: string } = await api
.post("upload", { body: formData })
.json();
setProject((prev) => ({
...prev,
image: file,
}));
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
async function createProject() {
try {
await api.post("projects", { json: { ...project } });
setModal(null);
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
async function handleSubmit(e: ChangeEvent<HTMLFormElement>) {
e.preventDefault();
await createProject();
window.location.reload();
}
useEffect(() => {
uploadFile();
}, [file]);
return (
<div className="bg-white shadow-lg text-black p-8 rounded-xl flex flex-col gap-4">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">Создание проекта</p>
<button
onClick={() => setModal(null)}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Close2Icon />
</button>
</div>
<form
onSubmit={handleSubmit}
className="grid grid-cols-2 gap-4 w-[512px]"
>
<div className="flex flex-col gap-1">
<label className="text-sm">Название</label>
<input
autoFocus
required
type="text"
placeholder="Название"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.name}
onChange={(e) =>
setProject((prev) => ({ ...prev, name: e.target.value }))
}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Компания</label>
<input
required
type="text"
placeholder="Компания"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.company}
onChange={(e) =>
setProject((prev) => ({ ...prev, company: e.target.value }))
}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Город</label>
<input
required
type="text"
placeholder="Город"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.city}
onChange={(e) =>
setProject((prev) => ({ ...prev, city: e.target.value }))
}
/>
</div>
<label className="relative border border-dashed border-neutral-500 px-3 py-2 hover:bg-opacity-10 hover:bg-black cursor-pointer rounded-lg flex flex-col gap-2">
<input
required
type="file"
accept="image/*"
className="absolute opacity-0"
onChange={handleChangeFile}
/>
<p className="truncate">
{file ? file.name : "Выберите изображение"}
</p>
{previewFile && <img src={previewFile} alt="" />}
</label>
<div className="flex flex-col gap-1">
<label className="text-sm">Стадия</label>
<select
required
value={project.stage || ""}
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
onChange={(e) =>
setProject((prev) => ({ ...prev, stage: +e.target.value }))
}
>
<option value="" disabled>
Выберите стадию
</option>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Дата релиза</label>
<input
type="date"
required
value={project.releaseDate}
onChange={(e) =>
setProject((prev) => ({
...prev,
releaseDate: e.target.value,
}))
}
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
/>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Девайсы</p>
<div className="">
<label className="flex items-center gap-2">
<input
type="checkbox"
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "stream"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "stream"
),
}));
}
}}
/>
<span>Stream</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "touch"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "touch"
),
}));
}
}}
/>
<span>Touch</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "mobile"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "mobile"
),
}));
}
}}
/>
<span>Mobile</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "vr"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "vr"
),
}));
}
}}
/>
<span>VR</span>
</label>
</div>
</div>
<div className="col-span-full flex justify-end">
<Button className="text-white outline-none">Добавить проект</Button>
</div>
</form>
</div>
);
}
export default CreateProjectModal;
@@ -0,0 +1,48 @@
/* eslint-disable react-hooks/exhaustive-deps */
import api from "../../utils/api";
import Button from "../Button";
import useModalStore from "../../stores/useModalStore";
import Close2Icon from "../icons/Close2Icon";
interface DeleteProjectModalProps {
projectId: string;
}
function DeleteProjectModal({ projectId }: DeleteProjectModalProps) {
const [setModal] = useModalStore((state) => [state.setModal]);
async function deleteProject() {
try {
await api.delete(`projects/${projectId}`).json();
setModal(null);
window.location.reload();
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
return (
<div className="bg-white shadow-lg text-black p-8 rounded-xl flex flex-col gap-4">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">Удаление проекта</p>
<button
onClick={() => setModal(null)}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Close2Icon />
</button>
</div>
<Button
onClick={deleteProject}
className="text-white self-end outline-none"
>
Удалить проект
</Button>
</div>
);
}
export default DeleteProjectModal;
+325
View File
@@ -0,0 +1,325 @@
/* eslint-disable @next/next/no-img-element */
/* eslint-disable react-hooks/exhaustive-deps */
import { ChangeEvent, useEffect, useState } from "react";
import api from "../../utils/api";
import Button from "../Button";
import { IProject } from "../../types/IProject";
import useModalStore from "../../stores/useModalStore";
import { format, parseISO } from "date-fns";
import Close2Icon from "../icons/Close2Icon";
interface EditProjectModalProps {
projectId: string;
}
function EditProjectModal({ projectId }: EditProjectModalProps) {
const [project, setProject] = useState<IProject>({
name: "",
company: "",
city: "",
image: "",
releaseDate: "2023-01-01",
devices: [],
});
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState<string>();
const [setModal] = useModalStore((state) => [state.setModal]);
function handleChangeFile(e: ChangeEvent<HTMLInputElement>) {
if (!e.target.files) return;
const targetFile = e.target.files[0];
setFile(targetFile);
setPreviewFile(URL.createObjectURL(targetFile));
}
async function uploadFile() {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
try {
const { file }: { file: string } = await api
.post("upload", { body: formData })
.json();
setProject((prev) => ({
...prev,
image: file,
}));
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
async function updateProject() {
try {
await api.put(`projects/${projectId}`, { json: { ...project } });
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
async function handleSubmit(e: ChangeEvent<HTMLFormElement>) {
e.preventDefault();
await updateProject();
setModal(null);
window.location.reload();
}
async function getProject() {
try {
const project: IProject = await api.get(`projects/${projectId}`).json();
project.releaseDate = format(parseISO(project.releaseDate), "yyyy-MM-dd");
setProject(project);
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
useEffect(() => {
uploadFile();
}, [file]);
useEffect(() => {
getProject();
}, []);
return (
<div className="bg-white shadow-lg text-black p-8 rounded-xl flex flex-col gap-4">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">Редактирование проекта</p>
<button
onClick={() => setModal(null)}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Close2Icon />
</button>
</div>
<form
onSubmit={handleSubmit}
className="grid grid-cols-2 gap-4 w-[512px]"
>
<div className="flex flex-col gap-1">
<label className="text-sm">Название</label>
<input
autoFocus
required
type="text"
placeholder="Название"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.name}
onChange={(e) =>
setProject((prev) => ({ ...prev, name: e.target.value }))
}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Компания</label>
<input
required
type="text"
placeholder="Компания"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.company}
onChange={(e) =>
setProject((prev) => ({ ...prev, company: e.target.value }))
}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Город</label>
<input
required
type="text"
placeholder="Город"
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
value={project.city}
onChange={(e) =>
setProject((prev) => ({ ...prev, city: e.target.value }))
}
/>
</div>
<label className="relative border border-dashed border-neutral-500 px-3 py-2 hover:bg-opacity-10 hover:bg-black cursor-pointer rounded-lg flex flex-col gap-2">
<input
type="file"
accept="image/*"
className="absolute opacity-0"
onChange={handleChangeFile}
/>
<p className="truncate">
{file ? file.name : "Выберите изображение"}
</p>
{previewFile ? (
<img src={previewFile} alt="" />
) : (
project.image && (
<img
src={`${process.env.VITE_API_URL}/upload/${project.image}`}
alt=""
/>
)
)}
</label>
<div className="flex flex-col gap-1">
<label className="text-sm">Стадия</label>
<select
required
value={project.stage || ""}
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
onChange={(e) =>
setProject((prev) => ({ ...prev, stage: +e.target.value }))
}
>
<option value="" disabled>
Выберите стадию
</option>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
<option value={6}>6</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm">Дата релиза</label>
<input
type="date"
required
value={project.releaseDate}
onChange={(e) =>
setProject((prev) => ({
...prev,
releaseDate: e.target.value,
}))
}
className="border border-neutral-500 px-3 py-2 rounded-lg outline-none"
/>
</div>
<div className="flex flex-col gap-1">
<p className="text-sm">Девайсы</p>
<div className="">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={project.devices!.includes("stream")}
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "stream"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "stream"
),
}));
}
}}
/>
<span>Stream</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={project.devices!.includes("touch")}
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "touch"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "touch"
),
}));
}
}}
/>
<span>Touch</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={project.devices!.includes("mobile")}
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "mobile"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "mobile"
),
}));
}
}}
/>
<span>Mobile</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={project.devices!.includes("vr")}
onChange={(e) => {
if (e.target.checked) {
setProject((prev) => ({
...prev,
devices: [...prev.devices!, "vr"],
}));
} else {
setProject((prev) => ({
...prev,
devices: prev.devices!.filter(
(device) => device !== "vr"
),
}));
}
}}
/>
<span>VR</span>
</label>
</div>
</div>
<div className="col-span-full flex justify-end">
<Button className="text-white outline-none">
Сохранить изменения
</Button>
</div>
</form>
</div>
);
}
export default EditProjectModal;
+13
View File
@@ -0,0 +1,13 @@
import ContactsForm from "../ContactsForm";
function FeedbackModal() {
return (
<div className="fixed top-0 right-0 h-full sm:w-[408px] w-full bg-[#14161F] overflow-y-auto sm:p-8 p-6">
<div className="flex flex-col gap-4">
<ContactsForm />
</div>
</div>
);
}
export default FeedbackModal;