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
+21 -1
View File
@@ -10,7 +10,8 @@
"dependencies": { "dependencies": {
"next": "15.3.5", "next": "15.3.5",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"swiper": "^11.2.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@@ -5732,6 +5733,25 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swiper": {
"version": "11.2.10",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.10.tgz",
"integrity": "sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.11", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
+6 -5
View File
@@ -9,19 +9,20 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "15.3.5",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.3.5" "swiper": "^11.2.10"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.5", "eslint-config-next": "15.3.5",
"@eslint/eslintrc": "^3" "tailwindcss": "^4",
"typescript": "^5"
} }
} }
+3
View File
@@ -0,0 +1,3 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5293 33L0 33L16.4709 16.5002L1.44248e-06 -1.39876e-06L15.5293 -7.19958e-07L32 16.5002L15.5293 33Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+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 ( return (
<div className={fluxgore.className}> <div className={`${fluxgore.className} relative inline-block`}>
<div className="relative inline-block"> {/* Shadow element */}
{/* Shadow element */} {shadowEnabled && (
{shadowEnabled && ( <div
<div className="absolute bg-black transition-all duration-150 ease-in-out"
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}
style={{ style={{
fontSize: "18px", top: "8px",
lineHeight: "1.2", left: "8px",
padding: "20px 40px", right: "-8px",
minWidth: "280px", bottom: "-8px",
clipPath: clipPath:
"polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)", "polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)",
border: "none", zIndex: 0,
zIndex: 1,
}} }}
onMouseEnter={ />
shadowEnabled )}
? (e) => { {/* Button */}
const shadow = e.currentTarget <button
.previousElementSibling as HTMLElement; className={`${getButtonStyles()} inline-block relative transition-all duration-150 ease-in-out ${
if (shadow) { shadowEnabled
shadow.style.top = "6px"; ? "active:translate-x-1 active:translate-y-1"
shadow.style.left = "6px"; : "hover:scale-105 active:scale-95"
shadow.style.right = "-6px"; } ${props.className}`}
shadow.style.bottom = "-6px"; 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 }
} : undefined
onMouseLeave={ }
shadowEnabled onMouseLeave={
? (e) => { shadowEnabled
const shadow = e.currentTarget ? (e) => {
.previousElementSibling as HTMLElement; const shadow = e.currentTarget
if (shadow) { .previousElementSibling as HTMLElement;
shadow.style.top = "8px"; if (shadow) {
shadow.style.left = "8px"; shadow.style.top = "8px";
shadow.style.right = "-8px"; shadow.style.left = "8px";
shadow.style.bottom = "-8px"; shadow.style.right = "-8px";
} shadow.style.bottom = "-8px";
} }
: undefined }
} : undefined
onMouseDown={ }
shadowEnabled onMouseDown={
? (e) => { shadowEnabled
const shadow = e.currentTarget ? (e) => {
.previousElementSibling as HTMLElement; const shadow = e.currentTarget
if (shadow) { .previousElementSibling as HTMLElement;
shadow.style.top = "4px"; if (shadow) {
shadow.style.left = "4px"; shadow.style.top = "4px";
shadow.style.right = "-4px"; shadow.style.left = "4px";
shadow.style.bottom = "-4px"; shadow.style.right = "-4px";
} shadow.style.bottom = "-4px";
} }
: undefined }
} : undefined
onMouseUp={ }
shadowEnabled onMouseUp={
? (e) => { shadowEnabled
const shadow = e.currentTarget ? (e) => {
.previousElementSibling as HTMLElement; const shadow = e.currentTarget
if (shadow) { .previousElementSibling as HTMLElement;
shadow.style.top = "6px"; if (shadow) {
shadow.style.left = "6px"; shadow.style.top = "6px";
shadow.style.right = "-6px"; shadow.style.left = "6px";
shadow.style.bottom = "-6px"; shadow.style.right = "-6px";
} shadow.style.bottom = "-6px";
} }
: undefined }
} : undefined
> }
{props.children} >
</button> {props.children}
</div> </button>
</div> </div>
); );
} }
+58 -62
View File
@@ -10,83 +10,79 @@ function CoverHeading({ children, textPosition }: CoverHeadingProps) {
const textAlign = textPosition || "left"; const textAlign = textPosition || "left";
return ( return (
<div className={fluxgore.className}> <h1
<h1 className={`${fluxgore.className} text-white relative`}
className="text-white relative" style={{
style={{ textAlign: textAlign,
textAlign: textAlign, fontSize: "8vw", // Changed from fixed 130px to 8vw
fontSize: "8vw", // Changed from fixed 130px to 8vw lineHeight: "1",
lineHeight: "1", }}
}} >
> {/* Shadow layer */}
{/* Shadow layer */} {textAlign === "left" && (
{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 */}
<span <span
className="relative text-white" className="absolute top-0 text-black"
style={{ style={{
zIndex: 3, transform: `translate(1.1vw, 0.7vw)`, // Changed from fixed pixels to vw
display: "block",
textAlign: textAlign, textAlign: textAlign,
WebkitTextStroke: "0.75vw black", // Changed from fixed 12px to 0.75vw zIndex: 1,
paintOrder: "stroke fill",
}} }}
aria-hidden="true"
> >
{children} {children}
</span> </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() { function DateBox() {
return ( return (
<div className={fluxgore.className}> <div
<div className={`${fluxgore.className} bg-white text-black px-6 py-2 inline-block`}
className="bg-white text-black px-6 py-2 inline-block" style={{
style={{ transform: "skewX(-15deg)",
transform: "skewX(-15deg)", fontSize: "2vw", // Changed from 40px to 2.5vw
fontSize: "2vw", // Changed from 40px to 2.5vw lineHeight: "1.2",
lineHeight: "1.2", filter: `
filter: `
drop-shadow(8px 8px 0px black) drop-shadow(8px 8px 0px black)
drop-shadow(-2px -2px 0px rgba(0,0,0,0.3)) drop-shadow(-2px -2px 0px rgba(0,0,0,0.3))
`, `,
border: "4px solid black", border: "4px solid black",
}} }}
> >
<div style={{ transform: "skewX(15deg)" }}> <div style={{ transform: "skewX(15deg)" }}>
5-7 СЕНТЯБРЯ 5-7 СЕНТЯБРЯ
<br /> <br />
МОСКВА 2025 МОСКВА 2025
</div>
</div> </div>
</div> </div>
); );
+27 -3
View File
@@ -59,9 +59,9 @@ function Events() {
<div className="container mx-auto"> <div className="container mx-auto">
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<div className={fluxgore.className}> <h1 className={`${fluxgore.className} text-7xl text-white relative`}>
<h1 className="text-7xl text-white relative">что вас ждет</h1> что вас ждет
</div> </h1>
<p <p
className={`${gothampro.className} text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none self-end`} className={`${gothampro.className} text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none self-end`}
@@ -79,6 +79,30 @@ function Events() {
Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана." Под чутким руководством и вдохновляющим присутствием легендарного Аркадия Цареградцева, амбассадора и супер-босса соревнований, лучшие джимханисты страны покажут невероятные трюки, демонстрируя виртуозное владение машиной. Скорость, точность, дым из-под колес и филигранные маневры в ограниченном пространстве – вот что такое Джимхана."
link="#" 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> </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> </Button>
</div> </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) => ( {venues.map((venue) => (
<SchemeSelect <SchemeSelect
key={venue.name} key={venue.name}
+10 -10
View File
@@ -42,17 +42,17 @@ function Video() {
<div className="container mx-auto"> <div className="container mx-auto">
<div className="flex flex-row space-x-16"> <div className="flex flex-row space-x-16">
<div className={fluxgore.className}> <h1 className={`${fluxgore.className} text-7xl text-white relative`}>
<h1 className="text-7xl text-white relative">как это Было</h1> как это Было
</div> </h1>
<div className={gothampro.className}> <p
<p className="text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none"> className={`${gothampro.className} text-[#E6E6E6] opacity-90 text-xl max-w-[536px] leading-none`}
Прошлый фестиваль технических видов спорта стал незабываемым >
праздником скорости и мастерства, собрав рекордное количество Прошлый фестиваль технических видов спорта стал незабываемым
участников и зрителей. праздником скорости и мастерства, собрав рекордное количество
</p> участников и зрителей.
</div> </p>
</div> </div>
<div className="flex mt-14"> <div className="flex mt-14">
+2
View File
@@ -1,3 +1,4 @@
import Activities from "@/components/Activities";
import Cover from "@/components/Cover"; import Cover from "@/components/Cover";
import Events from "@/components/Events"; import Events from "@/components/Events";
import Info from "@/components/Info"; import Info from "@/components/Info";
@@ -15,6 +16,7 @@ export default function Home() {
<Video /> <Video />
<Scheme /> <Scheme />
<Events /> <Events />
<Activities />
</main> </main>
</> </>
); );
+1
View File
@@ -2,6 +2,7 @@
:root { :root {
height: 100%; height: 100%;
overflow-x: hidden;
} }
body { body {