feat: replace img tags with Image component for better performance and optimization
This commit is contained in:
@@ -3,6 +3,7 @@ import { fluxgore } from "@/utils/fonts";
|
||||
import { useRef, useState } from "react";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import type { Swiper as SwiperType } from "swiper";
|
||||
import Image from "next/image";
|
||||
|
||||
import "swiper/css";
|
||||
|
||||
@@ -35,7 +36,13 @@ function SwiperButton({
|
||||
`}
|
||||
aria-label={direction === "next" ? "Next slide" : "Previous slide"}
|
||||
>
|
||||
<img src={iconPath} alt="" className={`w-4 h-4 sm:w-5 sm:h-5`} />
|
||||
<Image
|
||||
src={iconPath}
|
||||
alt=""
|
||||
width={20}
|
||||
height={20}
|
||||
className={`w-4 h-4 sm:w-5 sm:h-5`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -46,18 +53,25 @@ function Slide({ title, imageSrc }: { title: string; imageSrc: string }) {
|
||||
<h2 className={`${fluxgore.className} text-2xl sm:text-3xl lg:text-4xl text-white leading-none uppercase`}>
|
||||
{title}
|
||||
</h2>
|
||||
<img
|
||||
className="w-full h-auto object-cover mt-4 sm:mt-7"
|
||||
<div className="w-full relative mt-4 sm:mt-7 aspect-video">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt="Slide Image"
|
||||
alt={title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="absolute top-full left-0 w-full h-auto object-cover"
|
||||
<div className="absolute top-full left-0 w-full">
|
||||
<Image
|
||||
src="/images/activities/paper_tear.png"
|
||||
alt="Background Tear"
|
||||
width={400}
|
||||
height={50}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +166,6 @@ function Activities() {
|
||||
imageSrc="/images/activities/race_taxi.png"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+39
-23
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { fluxgore } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
import Image from "next/image";
|
||||
|
||||
interface CoverHeadingProps {
|
||||
children: React.ReactNode;
|
||||
@@ -94,49 +95,53 @@ function DateBox() {
|
||||
|
||||
function Cover() {
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center bg-no-repeat relative justify-center items-center py-36 animate-fade-in"
|
||||
style={{ backgroundImage: "url('/images/KV.png')" }}
|
||||
>
|
||||
<div className="relative justify-center items-center py-36 animate-fade-in">
|
||||
{/* Background Image */}
|
||||
<Image
|
||||
src="/images/KV.png"
|
||||
alt="Tech fest background"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
quality={85}
|
||||
/>
|
||||
|
||||
{/* Content overlay */}
|
||||
<div className="relative z-10">
|
||||
<div className="container mx-auto mb-24">
|
||||
{/* Top row with ФЕСТИВАЛЬ and date box */}
|
||||
<div className="flex align-center justify-center pb-7 md:hidden">
|
||||
<DateBox />
|
||||
</div>
|
||||
|
||||
{/* <div className="flex align-center justify-center pb-7 md:hidden">
|
||||
<DateBox />
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-16">
|
||||
<CoverHeading>ФЕСТИВАЛЬ</CoverHeading>
|
||||
<div className="hidden md:block">
|
||||
<DateBox />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CoverHeading textPosition="right">ТЕХНИЧЕСКИХ</CoverHeading>
|
||||
|
||||
<CoverHeading>ВИДОВ СПОРТА</CoverHeading> */}
|
||||
<img
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/images/cover_text.png"
|
||||
alt="Фестиваль технических видов спорта 2025"
|
||||
width={768}
|
||||
height={400}
|
||||
className="w-full max-w-3xl mx-auto hidden md:block animate-slide-in-left"
|
||||
style={{
|
||||
animationDelay: "0.2s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
priority
|
||||
/>
|
||||
|
||||
<img
|
||||
<Image
|
||||
src="/images/cover_text_mobile.png"
|
||||
alt="Фестиваль технических видов спорта 2025"
|
||||
width={576}
|
||||
height={300}
|
||||
className="w-full max-w-3xl mx-auto md:hidden animate-slide-in-left"
|
||||
style={{
|
||||
animationDelay: "0.2s",
|
||||
animationFillMode: "both",
|
||||
}}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
{/* <Button
|
||||
@@ -152,6 +157,7 @@ function Cover() {
|
||||
смотреть карту
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fade-in {
|
||||
@@ -237,10 +243,19 @@ function Cover() {
|
||||
|
||||
function CoverSoon() {
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center bg-no-repeat relative justify-center items-center py-36"
|
||||
style={{ backgroundImage: "url('/images/KV.png')" }}
|
||||
>
|
||||
<div className="relative justify-center items-center py-36">
|
||||
{/* Background Image */}
|
||||
<Image
|
||||
src="/images/KV.png"
|
||||
alt="Tech fest background"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
quality={85}
|
||||
/>
|
||||
|
||||
{/* Content overlay */}
|
||||
<div className="relative z-10">
|
||||
<div className="container mx-auto max-w-5/7 mb-24">
|
||||
{/* Top row with ФЕСТИВАЛЬ and date box */}
|
||||
<div className="flex flex-row items-center space-x-16">
|
||||
@@ -259,6 +274,7 @@ function CoverSoon() {
|
||||
<Button disabled>скоро</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+28
-45
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
image: string;
|
||||
@@ -33,13 +33,16 @@ function EventCard(props: EventCardProps) {
|
||||
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"
|
||||
<div className="w-full md:w-1/3 mb-4 md:mb-0 relative">
|
||||
<div className="w-full md:w-2/3 aspect-video relative">
|
||||
<Image
|
||||
src={props.image}
|
||||
alt={props.title}
|
||||
fill
|
||||
className="object-cover rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-1/3 mb-4 md:mb-0 md:px-4">
|
||||
<h2
|
||||
className={`${fluxgore.className} text-2xl md:text-4xl text-white leading-tight md:leading-none uppercase`}
|
||||
@@ -68,21 +71,30 @@ function Events() {
|
||||
<div
|
||||
id="events"
|
||||
className="bg-[#161616] relative pt-32 md:pt-64 pb-16 md:pb-32"
|
||||
style={{
|
||||
backgroundImage: `url('/images/noise.svg')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundBlendMode: "overlay",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
id="yuka"
|
||||
className="absolute top-0 w-full object-cover"
|
||||
src="/images/events/paper_tear.png"
|
||||
alt="Paper tear"
|
||||
{/* Background noise pattern */}
|
||||
<Image
|
||||
src="/images/noise.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover opacity-50 mix-blend-overlay"
|
||||
quality={75}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Paper tear image */}
|
||||
<div className="absolute top-0 w-full">
|
||||
<Image
|
||||
id="yuka"
|
||||
src="/images/events/paper_tear.png"
|
||||
alt="Paper tear"
|
||||
width={1920}
|
||||
height={200}
|
||||
className="w-full object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="flex flex-col md:flex-row md:justify-between space-y-6 md:space-y-0">
|
||||
<h1
|
||||
className={`${fluxgore.className} text-4xl md:text-7xl text-white relative uppercase`}
|
||||
@@ -185,35 +197,6 @@ function Events() {
|
||||
}
|
||||
link="https://t.me/TechSportFestbot"
|
||||
/>
|
||||
{/* <EventCard
|
||||
disabled={true}
|
||||
id="moto"
|
||||
image="/events/moto.png"
|
||||
title="КуБок ШОС по Мотокроссу"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
Уникальная возможность увидеть настоящую битву моторов и
|
||||
мастерства на трассе.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Лучшие гонщики со всего мира соберутся, чтобы
|
||||
продемонстрировать невероятные прыжки, головокружительные
|
||||
виражи и бескомпромиссную борьбу за победу.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<p>
|
||||
Приготовьтесь к взрыву адреналина и незабываемым эмоциям, ведь
|
||||
Кубок ШОС по Мотокроссу обещает стать одним из самых ярких
|
||||
зрелищ фестиваля!
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
link="#"
|
||||
/> */}
|
||||
<EventCard
|
||||
disabled={true}
|
||||
image="/events/cart.png"
|
||||
|
||||
+29
-15
@@ -2,6 +2,7 @@
|
||||
import { gothampro } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
function Info() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -54,14 +55,17 @@ function Info() {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-[#161616] relative overflow-hidden px-4"
|
||||
style={{
|
||||
backgroundImage: `url('/images/noise.svg')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundBlendMode: "overlay",
|
||||
}}
|
||||
>
|
||||
<div className="container mx-auto pt-36">
|
||||
{/* Background noise pattern */}
|
||||
<Image
|
||||
src="/images/noise.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover opacity-50 mix-blend-overlay"
|
||||
quality={75}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto pt-36 relative z-10">
|
||||
<div
|
||||
ref={addToRefs}
|
||||
className="flex flex-row opacity-0 translate-y-8 transition-all duration-700"
|
||||
@@ -139,37 +143,47 @@ function Info() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto flex flex-col md:flex-row pt-12 md:pt-24 pb-28 md:pb-56 gap-4 md:gap-0">
|
||||
<div className="container mx-auto flex flex-col md:flex-row pt-12 md:pt-24 pb-28 md:pb-56 gap-4 md:gap-0 relative z-10">
|
||||
<div
|
||||
ref={addToRefs}
|
||||
className="w-full md:w-1/3 flex justify-center md:justify-start opacity-0 translate-y-8 transition-all duration-700 delay-700"
|
||||
>
|
||||
<img
|
||||
<div className="w-3/4 md:w-1/2 relative hover:scale-105 transition-transform duration-300">
|
||||
<Image
|
||||
src="/images/info/moto.png"
|
||||
alt="moto"
|
||||
className="w-3/4 md:w-1/2 h-auto object-cover hover:scale-105 transition-transform duration-300"
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={addToRefs}
|
||||
className="w-full md:w-1/3 flex justify-center opacity-0 translate-y-8 transition-all duration-700 delay-900"
|
||||
>
|
||||
<img
|
||||
<div className="w-3/4 md:w-full relative hover:scale-105 transition-transform duration-300">
|
||||
<Image
|
||||
src="/images/info/podium.jpg"
|
||||
alt="car"
|
||||
className="w-3/4 md:w-full h-auto object-cover hover:scale-105 transition-transform duration-300"
|
||||
width={500}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={addToRefs}
|
||||
className="flex w-full md:w-1/3 justify-center md:justify-start opacity-0 translate-y-8 transition-all duration-700 delay-1000"
|
||||
>
|
||||
<div className="w-0 md:w-1/3"></div>
|
||||
<div className="w-3/4 md:w-2/3">
|
||||
<img
|
||||
<div className="w-3/4 md:w-2/3 relative hover:scale-105 transition-transform duration-300">
|
||||
<Image
|
||||
src="/images/info/jump.png"
|
||||
alt="jump"
|
||||
className="w-full h-auto object-cover hover:scale-105 transition-transform duration-300"
|
||||
width={400}
|
||||
height={300}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-12
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { fluxgore } from "@/utils/fonts";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
function Kamaz() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -53,16 +54,17 @@ function Kamaz() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-[#161616] relative md:pt-64 pb-16 md:pb-32"
|
||||
style={{
|
||||
backgroundImage: `url('/images/noise.svg')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundBlendMode: "overlay",
|
||||
}}
|
||||
>
|
||||
<div className="container mx-auto px-4 max-w-7xl">
|
||||
<div className="bg-[#161616] relative md:pt-64 pb-16 md:pb-32">
|
||||
{/* Background noise pattern */}
|
||||
<Image
|
||||
src="/images/noise.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover opacity-50 mix-blend-overlay"
|
||||
quality={75}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 max-w-7xl relative z-10">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-col lg:flex-row gap-8 lg:gap-16 opacity-0 translate-y-8 transition-all duration-700 ease-out"
|
||||
@@ -81,16 +83,20 @@ function Kamaz() {
|
||||
className="mt-12 md:mt-16 opacity-0 translate-y-8 transition-all duration-700 delay-300 ease-out"
|
||||
>
|
||||
<div className="relative group">
|
||||
<img
|
||||
<div className="relative w-full max-h-[600px] aspect-[16/9]">
|
||||
<Image
|
||||
src="/images/kamaz.webp"
|
||||
alt="Tech Festival - Previous Event Highlights"
|
||||
className="w-full h-auto max-h-[600px] object-cover shadow-2xl"
|
||||
fill
|
||||
className="object-cover shadow-2xl"
|
||||
style={{
|
||||
clipPath:
|
||||
"polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px)",
|
||||
}}
|
||||
loading="lazy"
|
||||
quality={85}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+59
-31
@@ -2,6 +2,7 @@
|
||||
import { fluxgore } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
function Partners() {
|
||||
const elementsRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
@@ -33,16 +34,16 @@ function Partners() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="partners"
|
||||
className="bg-[#161616] relative py-16 sm:py-32 lg:py-52"
|
||||
style={{
|
||||
backgroundImage: `url('/images/noise.svg')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundBlendMode: "overlay",
|
||||
}}
|
||||
>
|
||||
<div id="partners" className="bg-[#161616] relative py-16 sm:py-32 lg:py-52">
|
||||
{/* Background noise pattern */}
|
||||
<Image
|
||||
src="/images/noise.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover opacity-50 mix-blend-overlay"
|
||||
quality={75}
|
||||
/>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
@@ -59,7 +60,7 @@ function Partners() {
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div
|
||||
ref={addToRefs}
|
||||
className="opacity-0 translate-y-8 transition-all duration-700"
|
||||
@@ -75,51 +76,78 @@ function Partners() {
|
||||
ref={addToRefs}
|
||||
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-8 sm:gap-12 md:gap-16 lg:gap-24 mt-8 sm:mt-12 lg:mt-16 opacity-0 translate-y-8 transition-all duration-700 delay-200"
|
||||
>
|
||||
<img
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/dep.svg"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Dep Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/mos.svg"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Mos Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/raf.png"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Raf Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/ctvs.png"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Ctvs Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/xenum.svg"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Xenum Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/GNVOil.svg"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="GNVOil Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/Q8oils.svg"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Q8oils Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
<img
|
||||
</div>
|
||||
<div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/IMG_3601.png"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="Q8oils Logo"
|
||||
alt="Partner Logo"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
{/* <img
|
||||
</div>
|
||||
{/* <div className="w-auto h-16 sm:h-20 lg:h-24 relative hover:scale-110 transition-transform duration-300">
|
||||
<Image
|
||||
src="/logos/smp.png"
|
||||
className="w-auto h-16 sm:h-20 lg:h-24 object-contain hover:scale-110 transition-transform duration-300"
|
||||
alt="SMP Logo"
|
||||
/> */}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
import Button from "./Button";
|
||||
import Image from "next/image";
|
||||
import { ReactNode, useState, useMemo } from "react";
|
||||
|
||||
interface ScheduleData {
|
||||
@@ -125,11 +126,16 @@ function Scheme({ scheduleData = [] }: SchemeProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img
|
||||
<div className="mt-10 md:mt-20 w-full relative">
|
||||
<Image
|
||||
src="/images/scheme/map.png"
|
||||
alt="Festival Map"
|
||||
className="mt-10 md:mt-20 w-full h-auto"
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-full h-auto"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Date Selection */}
|
||||
<div className="flex flex-col md:flex-row mt-10 md:mt-40 space-y-3 md:space-y-0 md:space-x-5 items-center justify-center">
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PocketBase from "pocketbase";
|
||||
import { fluxgore, gothampro } from "@/utils/fonts";
|
||||
|
||||
const pb = new PocketBase("https://base.mossport.info");
|
||||
|
||||
interface CSVExporterProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CSVExporter: React.FC<CSVExporterProps> = ({ className = '' }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedCollection, setSelectedCollection] = useState('forms');
|
||||
const [selectedType, setSelectedType] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [credentials, setCredentials] = useState({ email: "", password: "" });
|
||||
const [authError, setAuthError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const exportTypes = [
|
||||
{ value: '', label: 'Все типы' },
|
||||
{ value: 'exhibition', label: 'Выставка' },
|
||||
{ value: 'fight', label: 'Дрифт-битва' },
|
||||
];
|
||||
|
||||
const collections = [
|
||||
{ value: 'forms', label: 'Формы' },
|
||||
// Add other collections if needed
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is already authenticated
|
||||
if (pb.authStore.isValid) {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAuthError("");
|
||||
|
||||
try {
|
||||
await pb.admins.authWithPassword(credentials.email, credentials.password);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
console.error("Auth error:", error);
|
||||
setAuthError("Неверный email или пароль");
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
pb.authStore.clear();
|
||||
setIsAuthenticated(false);
|
||||
setCredentials({ email: "", password: "" });
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
collection: selectedCollection,
|
||||
});
|
||||
|
||||
if (selectedType) {
|
||||
params.append('type', selectedType);
|
||||
}
|
||||
|
||||
// Add auth token if available
|
||||
if (pb.authStore.token) {
|
||||
params.append('token', pb.authStore.token);
|
||||
}
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(`/api/csv?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Ошибка при экспорте данных');
|
||||
}
|
||||
|
||||
// Get the filename from response headers
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'export.csv';
|
||||
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||
if (filenameMatch) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Create blob and download
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Произошла ошибка');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<div className={`${gothampro.className} text-xl text-gray-600`}>
|
||||
Загрузка...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Authentication form
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div className="max-w-md w-full mx-4">
|
||||
<div className="bg-white rounded-xl shadow-xl p-8 space-y-8">
|
||||
<div className="text-center">
|
||||
<h2 className={`${fluxgore.className} text-3xl text-gray-900 mb-2`}>
|
||||
Экспорт данных CSV
|
||||
</h2>
|
||||
<p className={`${gothampro.className} text-gray-600`}>
|
||||
Войдите для доступа к экспорту данных
|
||||
</p>
|
||||
</div>
|
||||
<form className="space-y-6" onSubmit={handleLogin}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
|
||||
>
|
||||
Email администратора
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
|
||||
placeholder="admin@example.com"
|
||||
value={credentials.email}
|
||||
onChange={(e) =>
|
||||
setCredentials((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
|
||||
>
|
||||
Пароль
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
|
||||
placeholder="••••••••"
|
||||
value={credentials.password}
|
||||
onChange={(e) =>
|
||||
setCredentials((prev) => ({
|
||||
...prev,
|
||||
password: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<div className={`${gothampro.className} text-red-600 text-sm text-center bg-red-50 p-3 rounded-lg`}>
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className={`${fluxgore.className} w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
|
||||
>
|
||||
Войти
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Main CSV exporter interface
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className={`${fluxgore.className} text-2xl md:text-4xl text-[#060606] uppercase`}>
|
||||
Экспорт данных в CSV
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={`${fluxgore.className} bg-gray-600 hover:bg-gray-700 text-white px-6 py-3 text-sm font-medium uppercase tracking-wide transition-colors rounded-lg`}
|
||||
>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-2xl mx-auto px-6 py-8">
|
||||
<div className={`csv-exporter ${className} bg-white border border-gray-200 rounded-xl shadow-sm p-8`}>
|
||||
<div className="csv-exporter__content space-y-6">
|
||||
<h3 className={`${fluxgore.className} text-xl text-[#1068B0] border-b border-gray-200 pb-2`}>
|
||||
Настройки экспорта
|
||||
</h3>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg flex items-center gap-3">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<span className={gothampro.className}>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="collection" className={`${gothampro.className} block text-sm font-medium text-gray-700`}>
|
||||
Коллекция:
|
||||
</label>
|
||||
<select
|
||||
id="collection"
|
||||
value={selectedCollection}
|
||||
onChange={(e) => setSelectedCollection(e.target.value)}
|
||||
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{collections.map((collection) => (
|
||||
<option key={collection.value} value={collection.value}>
|
||||
{collection.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="type" className={`${gothampro.className} block text-sm font-medium text-gray-700`}>
|
||||
Тип данных:
|
||||
</label>
|
||||
<select
|
||||
id="type"
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{exportTypes.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={isLoading}
|
||||
className={`${fluxgore.className} w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white px-6 py-4 rounded-lg font-medium uppercase tracking-wide transition-colors duration-200 flex items-center justify-center gap-3`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Экспорт...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-lg">📥</span>
|
||||
Экспортировать CSV
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`${gothampro.className} text-sm text-gray-500 bg-gray-50 p-4 rounded-lg`}>
|
||||
<h4 className="font-medium text-gray-700 mb-2">Информация:</h4>
|
||||
<ul className="space-y-1 text-xs">
|
||||
<li>• Экспорт включает все поля в зависимости от выбранного типа</li>
|
||||
<li>• Файл сохраняется в формате CSV с поддержкой UTF-8</li>
|
||||
<li>• Имя файла содержит коллекцию, тип и дату экспорта</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CSVExporter;
|
||||
Reference in New Issue
Block a user