feat: replace img tags with Image component for better performance and optimization

This commit is contained in:
2025-08-27 22:04:46 +05:00
parent b9cafe7b2f
commit 2560e47b11
8 changed files with 617 additions and 231 deletions
+20 -7
View File
@@ -3,6 +3,7 @@ import { fluxgore } from "@/utils/fonts";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import type { Swiper as SwiperType } from "swiper"; import type { Swiper as SwiperType } from "swiper";
import Image from "next/image";
import "swiper/css"; import "swiper/css";
@@ -35,7 +36,13 @@ function SwiperButton({
`} `}
aria-label={direction === "next" ? "Next slide" : "Previous slide"} 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> </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`}> <h2 className={`${fluxgore.className} text-2xl sm:text-3xl lg:text-4xl text-white leading-none uppercase`}>
{title} {title}
</h2> </h2>
<img <div className="w-full relative mt-4 sm:mt-7 aspect-video">
className="w-full h-auto object-cover mt-4 sm:mt-7" <Image
src={imageSrc} src={imageSrc}
alt="Slide Image" alt={title}
fill
className="object-cover"
/> />
</div>
<img <div className="absolute top-full left-0 w-full">
className="absolute top-full left-0 w-full h-auto object-cover" <Image
src="/images/activities/paper_tear.png" src="/images/activities/paper_tear.png"
alt="Background Tear" alt="Background Tear"
width={400}
height={50}
className="w-full h-auto object-cover"
/> />
</div> </div>
</div>
); );
} }
@@ -152,7 +166,6 @@ function Activities() {
imageSrc="/images/activities/race_taxi.png" imageSrc="/images/activities/race_taxi.png"
/> />
</SwiperSlide> </SwiperSlide>
</Swiper> </Swiper>
</div> </div>
</div> </div>
+39 -23
View File
@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { fluxgore } from "@/utils/fonts"; import { fluxgore } from "@/utils/fonts";
import Button from "./Button"; import Button from "./Button";
import Image from "next/image";
interface CoverHeadingProps { interface CoverHeadingProps {
children: React.ReactNode; children: React.ReactNode;
@@ -94,49 +95,53 @@ function DateBox() {
function Cover() { function Cover() {
return ( return (
<div <div className="relative justify-center items-center py-36 animate-fade-in">
className="bg-cover bg-center bg-no-repeat relative justify-center items-center py-36 animate-fade-in" {/* Background Image */}
style={{ backgroundImage: "url('/images/KV.png')" }} <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"> <div className="container mx-auto mb-24">
{/* Top row with ФЕСТИВАЛЬ and date box */} {/* Top row with ФЕСТИВАЛЬ and date box */}
<div className="flex align-center justify-center pb-7 md:hidden"> <div className="flex align-center justify-center pb-7 md:hidden">
<DateBox /> <DateBox />
</div> </div>
{/* <div className="flex align-center justify-center pb-7 md:hidden"> <div className="flex justify-center">
<DateBox /> <Image
</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
src="/images/cover_text.png" src="/images/cover_text.png"
alt="Фестиваль технических видов спорта 2025" alt="Фестиваль технических видов спорта 2025"
width={768}
height={400}
className="w-full max-w-3xl mx-auto hidden md:block animate-slide-in-left" className="w-full max-w-3xl mx-auto hidden md:block animate-slide-in-left"
style={{ style={{
animationDelay: "0.2s", animationDelay: "0.2s",
animationFillMode: "both", animationFillMode: "both",
}} }}
priority
/> />
<img <Image
src="/images/cover_text_mobile.png" src="/images/cover_text_mobile.png"
alt="Фестиваль технических видов спорта 2025" alt="Фестиваль технических видов спорта 2025"
width={576}
height={300}
className="w-full max-w-3xl mx-auto md:hidden animate-slide-in-left" className="w-full max-w-3xl mx-auto md:hidden animate-slide-in-left"
style={{ style={{
animationDelay: "0.2s", animationDelay: "0.2s",
animationFillMode: "both", animationFillMode: "both",
}} }}
priority
/> />
</div> </div>
</div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
{/* <Button {/* <Button
@@ -152,6 +157,7 @@ function Cover() {
смотреть карту смотреть карту
</Button> */} </Button> */}
</div> </div>
</div>
<style jsx>{` <style jsx>{`
@keyframes fade-in { @keyframes fade-in {
@@ -237,10 +243,19 @@ function Cover() {
function CoverSoon() { function CoverSoon() {
return ( return (
<div <div className="relative justify-center items-center py-36">
className="bg-cover bg-center bg-no-repeat relative justify-center items-center py-36" {/* Background Image */}
style={{ backgroundImage: "url('/images/KV.png')" }} <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"> <div className="container mx-auto max-w-5/7 mb-24">
{/* Top row with ФЕСТИВАЛЬ and date box */} {/* Top row with ФЕСТИВАЛЬ and date box */}
<div className="flex flex-row items-center space-x-16"> <div className="flex flex-row items-center space-x-16">
@@ -259,6 +274,7 @@ function CoverSoon() {
<Button disabled>скоро</Button> <Button disabled>скоро</Button>
</div> </div>
</div> </div>
</div>
); );
} }
+28 -45
View File
@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @next/next/no-img-element */
import { fluxgore, gothampro } from "@/utils/fonts"; import { fluxgore, gothampro } from "@/utils/fonts";
import Button from "./Button"; import Button from "./Button";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> { interface EventCardProps extends React.HTMLAttributes<HTMLDivElement> {
image: string; image: string;
@@ -33,13 +33,16 @@ function EventCard(props: EventCardProps) {
id={props.id} 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" 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"> <div className="w-full md:w-1/3 mb-4 md:mb-0 relative">
<img <div className="w-full md:w-2/3 aspect-video relative">
className="w-full md:w-2/3 h-auto object-cover rounded" <Image
src={props.image} src={props.image}
alt={props.title} alt={props.title}
fill
className="object-cover rounded"
/> />
</div> </div>
</div>
<div className="w-full md:w-1/3 mb-4 md:mb-0 md:px-4"> <div className="w-full md:w-1/3 mb-4 md:mb-0 md:px-4">
<h2 <h2
className={`${fluxgore.className} text-2xl md:text-4xl text-white leading-tight md:leading-none uppercase`} className={`${fluxgore.className} text-2xl md:text-4xl text-white leading-tight md:leading-none uppercase`}
@@ -68,21 +71,30 @@ function Events() {
<div <div
id="events" id="events"
className="bg-[#161616] relative pt-32 md:pt-64 pb-16 md:pb-32" 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 {/* Background noise pattern */}
id="yuka" <Image
className="absolute top-0 w-full object-cover" src="/images/noise.svg"
src="/images/events/paper_tear.png" alt=""
alt="Paper tear" 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"> <div className="flex flex-col md:flex-row md:justify-between space-y-6 md:space-y-0">
<h1 <h1
className={`${fluxgore.className} text-4xl md:text-7xl text-white relative uppercase`} className={`${fluxgore.className} text-4xl md:text-7xl text-white relative uppercase`}
@@ -185,35 +197,6 @@ function Events() {
} }
link="https://t.me/TechSportFestbot" 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 <EventCard
disabled={true} disabled={true}
image="/events/cart.png" image="/events/cart.png"
+29 -15
View File
@@ -2,6 +2,7 @@
import { gothampro } from "@/utils/fonts"; import { gothampro } from "@/utils/fonts";
import Button from "./Button"; import Button from "./Button";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import Image from "next/image";
function Info() { function Info() {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -54,14 +55,17 @@ function Info() {
<div <div
ref={containerRef} ref={containerRef}
className="bg-[#161616] relative overflow-hidden px-4" 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 <div
ref={addToRefs} ref={addToRefs}
className="flex flex-row opacity-0 translate-y-8 transition-all duration-700" className="flex flex-row opacity-0 translate-y-8 transition-all duration-700"
@@ -139,37 +143,47 @@ function Info() {
</div> </div>
</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 <div
ref={addToRefs} 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" 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" src="/images/info/moto.png"
alt="moto" 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>
<div <div
ref={addToRefs} ref={addToRefs}
className="w-full md:w-1/3 flex justify-center opacity-0 translate-y-8 transition-all duration-700 delay-900" 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" src="/images/info/podium.jpg"
alt="car" 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>
<div <div
ref={addToRefs} 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" 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-0 md:w-1/3"></div>
<div className="w-3/4 md:w-2/3"> <div className="w-3/4 md:w-2/3 relative hover:scale-105 transition-transform duration-300">
<img <Image
src="/images/info/jump.png" src="/images/info/jump.png"
alt="jump" 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>
</div> </div>
+18 -12
View File
@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { fluxgore } from "@/utils/fonts"; import { fluxgore } from "@/utils/fonts";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import Image from "next/image";
function Kamaz() { function Kamaz() {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -53,16 +54,17 @@ function Kamaz() {
}; };
return ( return (
<div <div className="bg-[#161616] relative md:pt-64 pb-16 md:pb-32">
className="bg-[#161616] relative md:pt-64 pb-16 md:pb-32" {/* Background noise pattern */}
style={{ <Image
backgroundImage: `url('/images/noise.svg')`, src="/images/noise.svg"
backgroundSize: "cover", alt=""
backgroundRepeat: "repeat", fill
backgroundBlendMode: "overlay", className="object-cover opacity-50 mix-blend-overlay"
}} quality={75}
> />
<div className="container mx-auto px-4 max-w-7xl">
<div className="container mx-auto px-4 max-w-7xl relative z-10">
<div <div
ref={containerRef} 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" 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" className="mt-12 md:mt-16 opacity-0 translate-y-8 transition-all duration-700 delay-300 ease-out"
> >
<div className="relative group"> <div className="relative group">
<img <div className="relative w-full max-h-[600px] aspect-[16/9]">
<Image
src="/images/kamaz.webp" src="/images/kamaz.webp"
alt="Tech Festival - Previous Event Highlights" 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={{ style={{
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)",
}} }}
loading="lazy" 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 className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none rounded-lg" />
</div> </div>
</div> </div>
+59 -31
View File
@@ -2,6 +2,7 @@
import { fluxgore } from "@/utils/fonts"; import { fluxgore } from "@/utils/fonts";
import Button from "./Button"; import Button from "./Button";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import Image from "next/image";
function Partners() { function Partners() {
const elementsRef = useRef<(HTMLDivElement | null)[]>([]); const elementsRef = useRef<(HTMLDivElement | null)[]>([]);
@@ -33,16 +34,16 @@ function Partners() {
}; };
return ( return (
<div <div id="partners" className="bg-[#161616] relative py-16 sm:py-32 lg:py-52">
id="partners" {/* Background noise pattern */}
className="bg-[#161616] relative py-16 sm:py-32 lg:py-52" <Image
style={{ src="/images/noise.svg"
backgroundImage: `url('/images/noise.svg')`, alt=""
backgroundSize: "cover", fill
backgroundRepeat: "repeat", className="object-cover opacity-50 mix-blend-overlay"
backgroundBlendMode: "overlay", quality={75}
}} />
>
<style jsx>{` <style jsx>{`
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
@@ -59,7 +60,7 @@ function Partners() {
} }
`}</style> `}</style>
<div className="container mx-auto px-4"> <div className="container mx-auto px-4 relative z-10">
<div <div
ref={addToRefs} ref={addToRefs}
className="opacity-0 translate-y-8 transition-all duration-700" className="opacity-0 translate-y-8 transition-all duration-700"
@@ -75,51 +76,78 @@ function Partners() {
ref={addToRefs} 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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="Partner Logo"
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/smp.png" 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" alt="SMP Logo"
/> */} fill
className="object-contain"
/>
</div> */}
</div> </div>
<div <div
+8 -2
View File
@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { fluxgore, gothampro } from "@/utils/fonts"; import { fluxgore, gothampro } from "@/utils/fonts";
import Button from "./Button"; import Button from "./Button";
import Image from "next/image";
import { ReactNode, useState, useMemo } from "react"; import { ReactNode, useState, useMemo } from "react";
interface ScheduleData { interface ScheduleData {
@@ -125,11 +126,16 @@ function Scheme({ scheduleData = [] }: SchemeProps) {
</p> </p>
</div> </div>
<img <div className="mt-10 md:mt-20 w-full relative">
<Image
src="/images/scheme/map.png" src="/images/scheme/map.png"
alt="Festival Map" 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 */} {/* 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"> <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">
+320
View File
@@ -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;