feat: add Activities component with Swiper functionality and Partners component

This commit is contained in:
2025-07-17 11:39:19 +09:00
parent 0c815c53e2
commit 02389bf051
14 changed files with 387 additions and 169 deletions
+168
View File
@@ -0,0 +1,168 @@
/* eslint-disable @next/next/no-img-element */
import { fluxgore } from "@/utils/fonts";
import { useRef, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import type { Swiper as SwiperType } from "swiper";
import "swiper/css";
const iconPath = "/icons/swiper.svg";
function SwiperButton({
onClick,
direction = "next",
disabled = false,
}: {
onClick: () => void;
direction?: "next" | "prev";
disabled?: boolean;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`
w-12 h-12
border border-gray-200
transition-colors duration-200
flex items-center justify-center
${direction === "prev" ? "rotate-180" : ""}
${
disabled
? "bg-[#0e5a9c] cursor-not-allowed opacity-50"
: "bg-[#0e5a9c] hover:bg-[#0c4f87] active:bg-[#0a4373] hover:shadow-md"
}
`}
aria-label={direction === "next" ? "Next slide" : "Previous slide"}
>
<img src={iconPath} alt="" className={`w-5 h-5`} />
</button>
);
}
function Slide({ title, imageSrc }: { title: string; imageSrc: string }) {
return (
<div className="flex flex-col bg-[#1068B0] py-6 px-3.5 relative">
<h2 className={`${fluxgore.className} text-4xl text-white leading-none`}>
{title}
</h2>
<img
className="w-full h-auto object-cover mt-7"
src={imageSrc}
alt="Slide Image"
/>
<img
className="absolute top-full left-0 w-full h-auto object-cover"
src="/images/activities/paper_tear.png"
alt="Background Tear"
/>
</div>
);
}
function Activities() {
const swiperRef = useRef<SwiperType | null>(null);
const [isBeginning, setIsBeginning] = useState(true);
const [isEnd, setIsEnd] = useState(false);
const handlePrevious = () => {
swiperRef.current?.slidePrev();
};
const handleNext = () => {
swiperRef.current?.slideNext();
};
const handleSwiperInit = (swiper: SwiperType) => {
swiperRef.current = swiper;
setIsBeginning(swiper.isBeginning);
setIsEnd(swiper.isEnd);
};
const handleSlideChange = (swiper: SwiperType) => {
setIsBeginning(swiper.isBeginning);
setIsEnd(swiper.isEnd);
};
return (
<div className="bg-[#F4F4F4] relative pt-20 pb-32">
<div className="container mx-auto">
<div className="flex flex-row justify-between items-center">
<h1
className={`${fluxgore.className} text-7xl text-[#060606] relative`}
>
Активности фестиваля
</h1>
<div className="flex items-center space-x-5">
<SwiperButton
onClick={handlePrevious}
direction="prev"
disabled={isBeginning}
/>
<SwiperButton
onClick={handleNext}
direction="next"
disabled={isEnd}
/>
</div>
</div>
</div>
<div className="container mx-auto overflow-visible">
<div className="mt-16 overflow-visible">
<Swiper
spaceBetween={20}
slidesPerView={3}
onSwiper={handleSwiperInit}
onSlideChange={handleSlideChange}
watchOverflow={false}
className="!overflow-visible"
breakpoints={{
640: {
slidesPerView: 1,
},
768: {
slidesPerView: 2,
},
}}
>
<SwiperSlide>
<Slide
title="Автовыставка"
imageSrc="/images/activities/car1.png"
/>
</SwiperSlide>
<SwiperSlide>
<Slide
title="Автовыставка"
imageSrc="/images/activities/car1.png"
/>
</SwiperSlide>
<SwiperSlide>
<Slide
title="Автовыставка"
imageSrc="/images/activities/car1.png"
/>
</SwiperSlide>
<SwiperSlide>
<Slide
title="Автовыставка"
imageSrc="/images/activities/car1.png"
/>
</SwiperSlide>
<SwiperSlide>
<Slide
title="Автовыставка"
imageSrc="/images/activities/car1.png"
/>
</SwiperSlide>
</Swiper>
</div>
</div>
</div>
);
}
export default Activities;
+85 -87
View File
@@ -23,101 +23,99 @@ export default function Button(props: ButtonProps) {
};
return (
<div className={fluxgore.className}>
<div className="relative inline-block">
{/* Shadow element */}
{shadowEnabled && (
<div
className="absolute bg-black transition-all duration-150 ease-in-out"
style={{
top: "8px",
left: "8px",
right: "-8px",
bottom: "-8px",
clipPath:
"polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)",
zIndex: 0,
}}
/>
)}
{/* Button */}
<button
className={`${getButtonStyles()} inline-block relative transition-all duration-150 ease-in-out ${
shadowEnabled
? "active:translate-x-1 active:translate-y-1"
: "hover:scale-105 active:scale-95"
} ${props.className}`}
onClick={props.onClick}
<div className={`${fluxgore.className} relative inline-block`}>
{/* Shadow element */}
{shadowEnabled && (
<div
className="absolute bg-black transition-all duration-150 ease-in-out"
style={{
fontSize: "18px",
lineHeight: "1.2",
padding: "20px 40px",
minWidth: "280px",
top: "8px",
left: "8px",
right: "-8px",
bottom: "-8px",
clipPath:
"polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)",
border: "none",
zIndex: 1,
zIndex: 0,
}}
onMouseEnter={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "6px";
shadow.style.left = "6px";
shadow.style.right = "-6px";
shadow.style.bottom = "-6px";
}
/>
)}
{/* Button */}
<button
className={`${getButtonStyles()} inline-block relative transition-all duration-150 ease-in-out ${
shadowEnabled
? "active:translate-x-1 active:translate-y-1"
: "hover:scale-105 active:scale-95"
} ${props.className}`}
onClick={props.onClick}
style={{
fontSize: "18px",
lineHeight: "1.2",
padding: "20px 40px",
minWidth: "280px",
clipPath:
"polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)",
border: "none",
zIndex: 1,
}}
onMouseEnter={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "6px";
shadow.style.left = "6px";
shadow.style.right = "-6px";
shadow.style.bottom = "-6px";
}
: undefined
}
onMouseLeave={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "8px";
shadow.style.left = "8px";
shadow.style.right = "-8px";
shadow.style.bottom = "-8px";
}
}
: undefined
}
onMouseLeave={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "8px";
shadow.style.left = "8px";
shadow.style.right = "-8px";
shadow.style.bottom = "-8px";
}
: undefined
}
onMouseDown={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "4px";
shadow.style.left = "4px";
shadow.style.right = "-4px";
shadow.style.bottom = "-4px";
}
}
: undefined
}
onMouseDown={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "4px";
shadow.style.left = "4px";
shadow.style.right = "-4px";
shadow.style.bottom = "-4px";
}
: undefined
}
onMouseUp={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "6px";
shadow.style.left = "6px";
shadow.style.right = "-6px";
shadow.style.bottom = "-6px";
}
}
: undefined
}
onMouseUp={
shadowEnabled
? (e) => {
const shadow = e.currentTarget
.previousElementSibling as HTMLElement;
if (shadow) {
shadow.style.top = "6px";
shadow.style.left = "6px";
shadow.style.right = "-6px";
shadow.style.bottom = "-6px";
}
: undefined
}
>
{props.children}
</button>
</div>
}
: undefined
}
>
{props.children}
</button>
</div>
);
}
+58 -62
View File
@@ -10,83 +10,79 @@ function CoverHeading({ children, textPosition }: CoverHeadingProps) {
const textAlign = textPosition || "left";
return (
<div className={fluxgore.className}>
<h1
className="text-white relative"
style={{
textAlign: textAlign,
fontSize: "8vw", // Changed from fixed 130px to 8vw
lineHeight: "1",
}}
>
{/* Shadow layer */}
{textAlign === "left" && (
<span
className="absolute top-0 text-black"
style={{
transform: `translate(1.1vw, 0.7vw)`, // Changed from fixed pixels to vw
textAlign: textAlign,
zIndex: 1,
}}
aria-hidden="true"
>
{children}
</span>
)}
{textAlign === "right" && (
<span
className="absolute top-0 text-black"
style={{
transform: `translate(calc(-100% + 1.1vw), 0.7vw)`, // Changed from fixed pixels to vw
textAlign: textAlign,
zIndex: 1,
}}
aria-hidden="true"
>
{children}
</span>
)}
{/* Main text */}
<h1
className={`${fluxgore.className} text-white relative`}
style={{
textAlign: textAlign,
fontSize: "8vw", // Changed from fixed 130px to 8vw
lineHeight: "1",
}}
>
{/* Shadow layer */}
{textAlign === "left" && (
<span
className="relative text-white"
className="absolute top-0 text-black"
style={{
zIndex: 3,
display: "block",
transform: `translate(1.1vw, 0.7vw)`, // Changed from fixed pixels to vw
textAlign: textAlign,
WebkitTextStroke: "0.75vw black", // Changed from fixed 12px to 0.75vw
paintOrder: "stroke fill",
zIndex: 1,
}}
aria-hidden="true"
>
{children}
</span>
</h1>
</div>
)}
{textAlign === "right" && (
<span
className="absolute top-0 text-black"
style={{
transform: `translate(calc(-100% + 1.1vw), 0.7vw)`, // Changed from fixed pixels to vw
textAlign: textAlign,
zIndex: 1,
}}
aria-hidden="true"
>
{children}
</span>
)}
{/* Main text */}
<span
className="relative text-white"
style={{
zIndex: 3,
display: "block",
textAlign: textAlign,
WebkitTextStroke: "0.75vw black", // Changed from fixed 12px to 0.75vw
paintOrder: "stroke fill",
}}
>
{children}
</span>
</h1>
);
}
function DateBox() {
return (
<div className={fluxgore.className}>
<div
className="bg-white text-black px-6 py-2 inline-block"
style={{
transform: "skewX(-15deg)",
fontSize: "2vw", // Changed from 40px to 2.5vw
lineHeight: "1.2",
filter: `
<div
className={`${fluxgore.className} bg-white text-black px-6 py-2 inline-block`}
style={{
transform: "skewX(-15deg)",
fontSize: "2vw", // Changed from 40px to 2.5vw
lineHeight: "1.2",
filter: `
drop-shadow(8px 8px 0px black)
drop-shadow(-2px -2px 0px rgba(0,0,0,0.3))
`,
border: "4px solid black",
}}
>
<div style={{ transform: "skewX(15deg)" }}>
5-7 СЕНТЯБРЯ
<br />
МОСКВА 2025
</div>
border: "4px solid black",
}}
>
<div style={{ transform: "skewX(15deg)" }}>
5-7 СЕНТЯБРЯ
<br />
МОСКВА 2025
</div>
</div>
);
+27 -3
View File
@@ -59,9 +59,9 @@ function Events() {
<div className="container mx-auto">
<div className="flex flex-row justify-between">
<div className={fluxgore.className}>
<h1 className="text-7xl text-white relative">что вас ждет</h1>
</div>
<h1 className={`${fluxgore.className} text-7xl text-white relative`}>
что вас ждет
</h1>
<p
className={`${gothampro.className} text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none self-end`}
@@ -79,6 +79,30 @@ function Events() {
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
link="#"
/>
<EventCard
image="/events/yuka.png"
title="YUKA Drive Fest Джимхана"
description="YUKA Drive Fest Джимхана впервые врывается в Москву, и местом его дебюта станет наш Фестиваль технических видов спорта!
Это не просто гонки, это настоящий танец на асфальте, где мастерство водителя и мощь автомобиля сливаются воедино.
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
link="#"
/>
<EventCard
image="/events/yuka.png"
title="YUKA Drive Fest Джимхана"
description="YUKA Drive Fest Джимхана впервые врывается в Москву, и местом его дебюта станет наш Фестиваль технических видов спорта!
Это не просто гонки, это настоящий танец на асфальте, где мастерство водителя и мощь автомобиля сливаются воедино.
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
link="#"
/>
<EventCard
image="/events/yuka.png"
title="YUKA Drive Fest Джимхана"
description="YUKA Drive Fest Джимхана впервые врывается в Москву, и местом его дебюта станет наш Фестиваль технических видов спорта!
Это не просто гонки, это настоящий танец на асфальте, где мастерство водителя и мощь автомобиля сливаются воедино.
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
link="#"
/>
</div>
</div>
</div>
+5
View File
@@ -0,0 +1,5 @@
function Partners() {
return <div>Partners</div>;
}
export default Partners;
+1 -1
View File
@@ -97,7 +97,7 @@ function Scheme() {
</Button>
</div>
<div className="flex flex-row mt-14 space-x-12 justify-center">
<div className="flex flex-row mt-14 space-x-12 justify-center min-h-10">
{venues.map((venue) => (
<SchemeSelect
key={venue.name}
+10 -10
View File
@@ -42,17 +42,17 @@ function Video() {
<div className="container mx-auto">
<div className="flex flex-row space-x-16">
<div className={fluxgore.className}>
<h1 className="text-7xl text-white relative">как это Было</h1>
</div>
<h1 className={`${fluxgore.className} text-7xl text-white relative`}>
как это Было
</h1>
<div className={gothampro.className}>
<p className="text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none">
Прошлый фестиваль технических видов спорта стал незабываемым
праздником скорости и мастерства, собрав рекордное количество
участников и зрителей.
</p>
</div>
<p
className={`${gothampro.className} text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none`}
>
Прошлый фестиваль технических видов спорта стал незабываемым
праздником скорости и мастерства, собрав рекордное количество
участников и зрителей.
</p>
</div>
<div className="flex mt-14">
+2
View File
@@ -1,3 +1,4 @@
import Activities from "@/components/Activities";
import Cover from "@/components/Cover";
import Events from "@/components/Events";
import Info from "@/components/Info";
@@ -15,6 +16,7 @@ export default function Home() {
<Video />
<Scheme />
<Events />
<Activities />
</main>
</>
);
+1
View File
@@ -2,6 +2,7 @@
:root {
height: 100%;
overflow-x: hidden;
}
body {