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:
2025-07-30 17:05:36 +09:00
parent 72d04e034d
commit cb37c826e6
27 changed files with 661 additions and 70 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+12 -18
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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={{
+6 -6
View File
@@ -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"
+1
View File
@@ -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')`,
+1 -2
View File
@@ -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"
+113
View File
@@ -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>
);
}
+72
View File
@@ -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;
+109
View File
@@ -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>
);
}
+83
View File
@@ -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>
);
}
+88
View File
@@ -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
View File
@@ -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">
Г. МОСКВА, ЦТВС &apos;МОСКВА&apos;
</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>
+85
View File
@@ -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;
+4
View File
@@ -1,8 +1,12 @@
import { CoverSoon } from "@/components/Cover";
import Head from "next/head";
export default function Home() {
return (
<>
<Head>
<title>Фестиваль технических видов спорта</title>
</Head>
<CoverSoon />
</>
);
+5
View File
@@ -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
View File
@@ -5,6 +5,11 @@
overflow-x: hidden;
}
#__next {
height: 100%;
overflow-x: hidden;
}
body {
height: 100%;
overflow-x: hidden;