feat: add form components and exhibition registration page
- Implemented Checkbox, Input, Radio, Select, Textarea components for form handling. - Created exhibition registration page with integrated form components. - Added validation and state management for form inputs. - Included styles and accessibility features for better user experience.
This commit is contained in:
+12
-18
@@ -2,7 +2,7 @@
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
|
||||
interface EventCardProps {
|
||||
interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
image: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -11,7 +11,10 @@ interface EventCardProps {
|
||||
|
||||
function EventCard(props: EventCardProps) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row bg-[#272727] py-5 px-2.5 md:py-5 md:px-2.5 p-4">
|
||||
<div
|
||||
id={props.id}
|
||||
className="flex flex-col md:flex-row bg-[#272727] py-5 px-2.5 md:py-5 md:px-2.5 p-4"
|
||||
>
|
||||
<div className="w-full md:w-1/3 mb-4 md:mb-0">
|
||||
<img
|
||||
className="w-full md:w-2/3 h-auto object-cover rounded"
|
||||
@@ -74,39 +77,30 @@ function Events() {
|
||||
|
||||
<div className="flex flex-col space-y-4 md:space-y-7 mt-16 md:mt-36">
|
||||
<EventCard
|
||||
id="yuka"
|
||||
image="/events/yuka.png"
|
||||
title="YUKA Drive Fest Джимхана"
|
||||
description="YUKA Drive Fest Джимхана впервые врывается в Москву, и местом его дебюта станет наш Фестиваль технических видов спорта!
|
||||
Это не просто гонки, это настоящий танец на асфальте, где мастерство водителя и мощь автомобиля сливаются воедино.
|
||||
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
|
||||
description="YUKA Drive Fest Джимхана впервые врывается в Москву, и местом его дебюта станет наш Фестиваль технических видов спорта! Это не просто гонки, это настоящий танец на асфальте, где мастерство водителя и мощь автомобиля сливаются воедино. Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
|
||||
link="#"
|
||||
/>
|
||||
<EventCard
|
||||
id="moscow_fight"
|
||||
image="/events/moscow_fight.png"
|
||||
title="Дрифт«Битва за Москву»"
|
||||
description="Любительский турнир по дрифту, который вырос из проекта «Дорога в дрифт», созданного в 2021 году для поиска новых талантов. За три года он превратился в полноценные соревнования с привлекательным призовым фондом.
|
||||
Во второй день фестиваля, 8 сентября, пройдет дрифт-гонка, где главным призом станет электромобиль «Москвич».
|
||||
Соревнования проводятся по традиционной олимпийской системе. Чтобы принять участие, необходимо подать заявку на сайте и дождаться приглашения от организаторов."
|
||||
description="Любительский турнир по дрифту, который вырос из проекта «Дорога в дрифт», созданного в 2021 году для поиска новых талантов. За три года он превратился в полноценные соревнования с привлекательным призовым фондом. Во второй день фестиваля, 8 сентября, пройдет дрифт-гонка, где главным призом станет электромобиль «Москвич». Соревнования проводятся по традиционной олимпийской системе. Чтобы принять участие, необходимо подать заявку на сайте и дождаться приглашения от организаторов."
|
||||
link="#"
|
||||
/>
|
||||
<EventCard
|
||||
id="moto"
|
||||
image="/events/moto.png"
|
||||
title="КуБок ШОС по Мотокроссу"
|
||||
description="Уникальная возможность увидеть настоящую битву моторов и мастерства на трассе.
|
||||
|
||||
Лучшие гонщики со всего мира соберутся, чтобы продемонстрировать невероятные прыжки, головокружительные виражи и бескомпромиссную борьбу за победу.
|
||||
|
||||
Приготовьтесь к взрыву адреналина и незабываемым эмоциям, ведь Кубок ШОС по Мотокроссу обещает стать одним из самых ярких зрелищ фестиваля!"
|
||||
description="Уникальная возможность увидеть настоящую битву моторов и мастерства на трассе. Лучшие гонщики со всего мира соберутся, чтобы продемонстрировать невероятные прыжки, головокружительные виражи и бескомпромиссную борьбу за победу. Приготовьтесь к взрыву адреналина и незабываемым эмоциям, ведь Кубок ШОС по Мотокроссу обещает стать одним из самых ярких зрелищ фестиваля!"
|
||||
link="#"
|
||||
/>
|
||||
<EventCard
|
||||
image="/events/cart.png"
|
||||
title="Кубок по Фиджитал картингу"
|
||||
description="На Фестивале технических видов спорта 2025 впервые состоится Кубок по Фиджитал Картингу!
|
||||
|
||||
Это уникальное состязание, где виртуальная реальность встречается с реальной трассой.
|
||||
|
||||
Участники будут сражаться на симуляторах, а затем переносить свои навыки на настоящий картинг, демонстрируя невероятную адаптивность и мастерство."
|
||||
description="На Фестивале технических видов спорта 2025 впервые состоится Кубок по Фиджитал Картингу! Это уникальное состязание, где виртуальная реальность встречается с реальной трассой. Участники будут сражаться на симуляторах, а затем переносить свои навыки на настоящий картинг, демонстрируя невероятную адаптивность и мастерство."
|
||||
link="#"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+35
-42
@@ -1,8 +1,19 @@
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
import Link from "next/link";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
function Footer() {
|
||||
const scrollToElement = useCallback((elementId: string) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<footer className="bg-[#0D0D0D] py-16 md:py-36">
|
||||
<div className="container mx-auto px-4 flex flex-col space-y-9">
|
||||
@@ -19,7 +30,7 @@ function Footer() {
|
||||
<ul className="flex space-x-2 items-center">
|
||||
<li>
|
||||
<Link
|
||||
href="https://t.me/your_channel"
|
||||
href="https://t.me/moscow_drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-blue-500/30"
|
||||
@@ -34,7 +45,7 @@ function Footer() {
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://vk.com/your_group"
|
||||
href="https://vk.com/mos.drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-blue-600/30"
|
||||
@@ -49,7 +60,7 @@ function Footer() {
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://youtube.com/@your_channel"
|
||||
href="https://www.youtube.com/@mossportonline9438"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-red-500/30"
|
||||
@@ -74,30 +85,30 @@ function Footer() {
|
||||
СОБЫТИЯ
|
||||
</h3>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Link
|
||||
href="/#cover"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
<button
|
||||
onClick={() => scrollToElement("yuka")}
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200 text-left`}
|
||||
>
|
||||
YUKA Drive Fest
|
||||
</Link>
|
||||
</button>
|
||||
<Link
|
||||
href="/#cover"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
ЭКСПО
|
||||
</Link>
|
||||
<Link
|
||||
href="/#cover"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
<button
|
||||
onClick={() => scrollToElement("moto")}
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200 text-left`}
|
||||
>
|
||||
Мотокросс
|
||||
</Link>
|
||||
<Link
|
||||
href="/#cover"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollToElement("moscow_fight")}
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200 text-left`}
|
||||
>
|
||||
Дрифт Битва за Москву
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,30 +119,18 @@ function Footer() {
|
||||
ИНФОРМАЦИЯ
|
||||
</h3>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Link
|
||||
href="/#about"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
<button
|
||||
onClick={() => scrollToElement("info")}
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200 text-left`}
|
||||
>
|
||||
О фестивале
|
||||
</Link>
|
||||
</button>
|
||||
<Link
|
||||
href="/#program"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
Программа
|
||||
</Link>
|
||||
<Link
|
||||
href="/#tickets"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
Билеты
|
||||
</Link>
|
||||
<Link
|
||||
href="/#contacts"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
Контакты
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -148,24 +147,18 @@ function Footer() {
|
||||
>
|
||||
Регистрация
|
||||
</Link>
|
||||
<Link
|
||||
href="/#sponsors"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
<button
|
||||
onClick={() => scrollToElement("partners")}
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200 text-left`}
|
||||
>
|
||||
Партнеры
|
||||
</Link>
|
||||
</button>
|
||||
<Link
|
||||
href="/#media"
|
||||
href="https://forms.yandex.ru/u/6888b64502848f0274f5e9df"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
Пресса
|
||||
</Link>
|
||||
<Link
|
||||
href="/#volunteer"
|
||||
className={`${gothampro.className} text-sm md:text-base text-[#E6E6E6] opacity-60 hover:text-blue-500 transition-colors duration-200`}
|
||||
>
|
||||
Волонтерство
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Button from "./Button";
|
||||
|
||||
function Info() {
|
||||
return (
|
||||
<div className={gothampro.className}>
|
||||
<div id="info" className={gothampro.className}>
|
||||
<div
|
||||
className="bg-[#161616] relative overflow-hidden px-4"
|
||||
style={{
|
||||
|
||||
@@ -47,7 +47,7 @@ function Navbar() {
|
||||
<ul className="hidden md:flex space-x-2 items-center">
|
||||
<li>
|
||||
<Link
|
||||
href="https://t.me/your_channel"
|
||||
href="https://t.me/moscow_drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-blue-500/30"
|
||||
@@ -62,7 +62,7 @@ function Navbar() {
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://vk.com/your_group"
|
||||
href="https://vk.com/mos.drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-blue-600/30"
|
||||
@@ -77,7 +77,7 @@ function Navbar() {
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://youtube.com/@your_channel"
|
||||
href="https://www.youtube.com/@mossportonline9438"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 bg-white bg-opacity-10 hover:bg-opacity-20 hover:scale-110 active:scale-95 rounded-full flex items-center justify-center transition-all duration-200 transform hover:shadow-lg hover:shadow-red-500/30"
|
||||
@@ -169,7 +169,7 @@ function Navbar() {
|
||||
{/* Mobile Social Links */}
|
||||
<div className="flex space-x-4 justify-center md:hidden">
|
||||
<Link
|
||||
href="https://t.me/your_channel"
|
||||
href="https://t.me/moscow_drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-white bg-opacity-10 hover:bg-opacity-20 rounded-full flex items-center justify-center transition-all duration-200"
|
||||
@@ -179,7 +179,7 @@ function Navbar() {
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://vk.com/your_group"
|
||||
href="https://vk.com/mos.drift"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-white bg-opacity-10 hover:bg-opacity-20 rounded-full flex items-center justify-center transition-all duration-200"
|
||||
@@ -189,7 +189,7 @@ function Navbar() {
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://youtube.com/@your_channel"
|
||||
href="https://www.youtube.com/@mossportonline9438"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-white bg-opacity-10 hover:bg-opacity-20 rounded-full flex items-center justify-center transition-all duration-200"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fluxgore } from "@/utils/fonts";
|
||||
function Partners() {
|
||||
return (
|
||||
<div
|
||||
id="partners"
|
||||
className="bg-[#161616] relative py-16 sm:py-32 lg:py-52"
|
||||
style={{
|
||||
backgroundImage: `url('/images/noise.svg')`,
|
||||
|
||||
@@ -61,8 +61,7 @@ function Video() {
|
||||
|
||||
<div className="flex mt-8 md:mt-14">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/FVzlAMLPFh0?si=E_EaXkQ3tJYtyf_s"
|
||||
title="YouTube video player"
|
||||
src="https://vkvideo.ru/video_ext.php?oid=-23293707&id=456242039&hd=2"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
|
||||
interface CheckboxOption {
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
}
|
||||
|
||||
interface CheckboxProps {
|
||||
value?: string[];
|
||||
options: CheckboxOption[];
|
||||
onChange?: (values: string[]) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
name: string;
|
||||
direction?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export default function Checkbox({
|
||||
value = [],
|
||||
options,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
name,
|
||||
direction = "vertical",
|
||||
}: CheckboxProps) {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (onChange) {
|
||||
const optionValue = e.target.value;
|
||||
const isChecked = e.target.checked;
|
||||
|
||||
let newValues: string[];
|
||||
if (isChecked) {
|
||||
newValues = [...value, optionValue];
|
||||
} else {
|
||||
newValues = value.filter((v) => v !== optionValue);
|
||||
}
|
||||
|
||||
onChange(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 w-full max-w-sm ${className}`}>
|
||||
<div
|
||||
className={`flex gap-2 ${
|
||||
direction === "vertical" ? "flex-col" : "flex-row flex-wrap"
|
||||
}`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={`
|
||||
flex items-center gap-3 cursor-pointer relative
|
||||
${
|
||||
disabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "hover:text-gray-700"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
value={option.value}
|
||||
checked={value.includes(option.value)}
|
||||
onChange={handleChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 border-2 transition-all duration-200
|
||||
${
|
||||
value.includes(option.value)
|
||||
? "border-black bg-white"
|
||||
: "border-gray-400 bg-white hover:border-gray-600"
|
||||
}
|
||||
${disabled ? "opacity-50" : ""}
|
||||
`}
|
||||
>
|
||||
{value.includes(option.value) && (
|
||||
<svg
|
||||
className="w-6 h-6 text-black absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{error && <span className="text-sm text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
|
||||
import { gothampro } from "@/utils/fonts";
|
||||
|
||||
interface InputProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type?: "text" | "email" | "password" | "number" | "tel";
|
||||
value?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
function Input({
|
||||
label,
|
||||
placeholder,
|
||||
type = "text",
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
}: InputProps) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 w-full max-w-sm ${className}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id || name}
|
||||
className={`${gothampro.className} text-base font-bold text-black`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={id || name}
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-4 py-3
|
||||
border border-gray-300
|
||||
text-base font-normal
|
||||
bg-white
|
||||
placeholder-gray-400
|
||||
transition-all duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-3 focus:ring-gray-100 focus:border-gray-500
|
||||
hover:border-gray-400
|
||||
disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||
`}
|
||||
/>
|
||||
{error && <span className="text-sm text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Input;
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
|
||||
import { gothampro } from "@/utils/fonts";
|
||||
|
||||
interface RadioOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
options: RadioOption[];
|
||||
onChange?: (value: string) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
name: string;
|
||||
direction?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export default function Radio({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
name,
|
||||
direction = "vertical",
|
||||
}: RadioProps) {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (onChange) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 w-full max-w-sm ${className}`}>
|
||||
{label && (
|
||||
<label
|
||||
className={`${gothampro.className} text-base font-bold text-black`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className={`flex gap-2 ${
|
||||
direction === "vertical" ? "flex-col" : "flex-row flex-wrap"
|
||||
}`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={`
|
||||
flex items-center gap-3 cursor-pointer relative
|
||||
${
|
||||
disabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "hover:text-gray-700"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
onChange={handleChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 rounded-full border-2 transition-all duration-200
|
||||
${
|
||||
value === option.value
|
||||
? "border-black bg-white"
|
||||
: "border-gray-400 bg-white hover:border-gray-600"
|
||||
}
|
||||
${disabled ? "opacity-50" : ""}
|
||||
`}
|
||||
>
|
||||
{value === option.value && (
|
||||
<div className="w-4 h-4 rounded-full bg-black absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`${gothampro.className} text-base font-normal text-black select-none`}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{error && <span className="text-sm text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
|
||||
import { gothampro } from "@/utils/fonts";
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
options: SelectOption[];
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLSelectElement>) => void;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function Select({
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
}: SelectProps) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 w-full max-w-sm ${className}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id || name}
|
||||
className={`${gothampro.className} text-base font-bold text-black`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
id={id || name}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-4 py-3
|
||||
border border-gray-300
|
||||
text-base font-normal
|
||||
bg-white
|
||||
transition-all duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-3 focus:ring-gray-100 focus:border-gray-500
|
||||
hover:border-gray-400
|
||||
disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||
`}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled className="text-gray-400">
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <span className="text-sm text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
|
||||
import { gothampro } from "@/utils/fonts";
|
||||
|
||||
interface TextareaProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
maxLength?: number;
|
||||
resize?: "none" | "both" | "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
function Textarea({
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
rows = 4,
|
||||
cols,
|
||||
maxLength,
|
||||
resize = "vertical",
|
||||
}: TextareaProps) {
|
||||
const resizeClass = {
|
||||
none: "resize-none",
|
||||
both: "resize",
|
||||
horizontal: "resize-x",
|
||||
vertical: "resize-y",
|
||||
}[resize];
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 w-full max-w-sm ${className}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id || name}
|
||||
className={`${gothampro.className} text-base font-bold text-black`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
<textarea
|
||||
id={id || name}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
maxLength={maxLength}
|
||||
className={`
|
||||
w-full px-4 py-3
|
||||
border border-gray-300
|
||||
text-base font-normal
|
||||
bg-white
|
||||
placeholder-gray-400
|
||||
transition-all duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-3 focus:ring-gray-100 focus:border-gray-500
|
||||
hover:border-gray-400
|
||||
disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed
|
||||
${resizeClass}
|
||||
`}
|
||||
/>
|
||||
{error && <span className="text-sm text-red-500 mt-1">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Textarea
|
||||
+40
-1
@@ -1,10 +1,49 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<Head>
|
||||
<meta name="description" content="Фестиваль технических видов спорта" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Фестиваль технических видов спорта"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Ежегодный Фестиваль технических видов спорта в ЦТВС 'Москва'! Дрифт, джимхана, мотокросс, картинг, море адреналина и хорошего настроения!"
|
||||
/>
|
||||
<meta property="og:url" content="https://tech-fest.sport.mos.ru/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/preview.jpg" />
|
||||
</Head>
|
||||
<body className="antialiased">
|
||||
<div
|
||||
style={{ display: "none" }}
|
||||
itemScope
|
||||
itemType="https://schema.org/Organization"
|
||||
>
|
||||
<span itemProp="name">Фестиваль технических видов спорта</span>
|
||||
<div
|
||||
itemProp="address"
|
||||
itemScope
|
||||
itemType="https://schema.org/PostalAddress"
|
||||
>
|
||||
Адрес:
|
||||
<span itemProp="streetAddress">
|
||||
Г. МОСКВА, ЦТВС 'МОСКВА'
|
||||
</span>
|
||||
</div>
|
||||
<span itemProp="email">info@mossport.online</span>
|
||||
<div itemScope itemType="https://schema.org/ImageObject">
|
||||
<img
|
||||
src="/preview.jpg"
|
||||
itemProp="contentUrl"
|
||||
alt="Фестиваль технических видов спорта"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import Checkbox from "@/components/form/Checkbox";
|
||||
import Input from "@/components/form/Input";
|
||||
import Radio from "@/components/form/Radio";
|
||||
import Select from "@/components/form/Select";
|
||||
import Textarea from "@/components/form/Textarea";
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
import { useState } from "react";
|
||||
|
||||
function ExhibtionFormPage() {
|
||||
const [selectedOption, setSelectedOption] = useState("online");
|
||||
const [checkboxValues, setCheckboxValues] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div className="bg-[#ffffff] relative h-full">
|
||||
<h1
|
||||
className={`${fluxgore.className} text-4xl md:text-7xl text-[#060606] relative`}
|
||||
>
|
||||
Регистрация участника
|
||||
</h1>
|
||||
|
||||
<Input
|
||||
label="Имя"
|
||||
placeholder="Введите ваше имя"
|
||||
type="text"
|
||||
required
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Выберите тип участия"
|
||||
options={[
|
||||
{ value: "speaker", label: "Спикер" },
|
||||
{ value: "attendee", label: "Посетитель" },
|
||||
{ value: "sponsor", label: "Спонсор" },
|
||||
]}
|
||||
required
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<Radio
|
||||
value={selectedOption}
|
||||
onChange={setSelectedOption}
|
||||
label="Выберите формат участия"
|
||||
options={[
|
||||
{ value: "online", label: "Онлайн" },
|
||||
{ value: "offline", label: "Офлайн" },
|
||||
{ value: "hybrid", label: "Гибридный" },
|
||||
]}
|
||||
required
|
||||
className="mt-4"
|
||||
direction="vertical"
|
||||
name="participationFormat"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Дополнительная информация"
|
||||
placeholder="Введите дополнительную информацию"
|
||||
rows={4}
|
||||
cols={50}
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
value={checkboxValues}
|
||||
onChange={(values) => setCheckboxValues(values)}
|
||||
options={[
|
||||
{
|
||||
value: "terms",
|
||||
label: (
|
||||
<label className={`${gothampro.className} text-base text-black`}>
|
||||
Согласие на обработку персональных данных
|
||||
</label>
|
||||
),
|
||||
},
|
||||
]}
|
||||
required
|
||||
className="mt-4"
|
||||
direction="vertical"
|
||||
name="termsAgreement"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExhibtionFormPage;
|
||||
@@ -1,8 +1,12 @@
|
||||
import { CoverSoon } from "@/components/Cover";
|
||||
import Head from "next/head";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Фестиваль технических видов спорта</title>
|
||||
</Head>
|
||||
<CoverSoon />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,10 +9,15 @@ import Navbar from "@/components/Navbar";
|
||||
import Partners from "@/components/Partners";
|
||||
import Scheme from "@/components/Scheme";
|
||||
import Video from "@/components/Video";
|
||||
import Head from "next/head";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
|
||||
<Head>
|
||||
<title>Фестиваль технических видов спорта</title>
|
||||
</Head>
|
||||
<Navbar />
|
||||
<main className="flex-col min-h-full">
|
||||
<Cover />
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#__next {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
|
||||
Reference in New Issue
Block a user