mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 14:27:10 +00:00
fix: merge conflicts
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import Navbar from "@/modules/Navbar";
|
||||
import Navbar from "@/widgets/Navbar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Competition } from "@/shared/types/types";
|
||||
import { Competition } from "@/shared/types";
|
||||
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
|
||||
|
||||
|
||||
const CompetitionPreview = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -17,7 +16,7 @@ const CompetitionPreview = () => {
|
||||
const fetchCompetition = async () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
const found = mockCompetitions.find(comp => comp.id === id);
|
||||
const found = mockCompetitions.find((comp) => comp.id === id);
|
||||
setCompetition(found || null);
|
||||
setIsLoading(false);
|
||||
}, 500);
|
||||
@@ -48,49 +47,55 @@ const CompetitionPreview = () => {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="container mx-auto px-4 py-8 mt-16">
|
||||
<button
|
||||
<div className="container mx-auto mt-16 px-4 py-8">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center text-gray-600 mb-8 font-hse-sans"
|
||||
className="font-hse-sans mb-8 flex items-center text-gray-600"
|
||||
>
|
||||
<ArrowLeft size={16} className="mr-2" />
|
||||
Назад к соревнованиям
|
||||
</button>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p className="font-hse-sans text-gray-500">Загрузка...</p>
|
||||
</div>
|
||||
) : competition ? (
|
||||
<div className="max-w-5xl mx-auto bg-white rounded-lg overflow-hidden shadow-lg">
|
||||
<div className="w-full h-80 overflow-hidden">
|
||||
<img
|
||||
src={competition.imageUrl}
|
||||
<div className="mx-auto max-w-5xl overflow-hidden rounded-lg bg-white shadow-lg">
|
||||
<div className="h-80 w-full overflow-hidden">
|
||||
<img
|
||||
src={competition.imageUrl}
|
||||
alt={competition.name}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="p-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold font-hse-sans mr-6 flex-1">{competition.name}</h1>
|
||||
<Button
|
||||
className="bg-yellow-400 hover:bg-yellow-500 text-black font-hse-sans text-base px-12 min-w-[180px]"
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<h1 className="font-hse-sans mr-6 flex-1 text-3xl font-semibold">
|
||||
{competition.name}
|
||||
</h1>
|
||||
<Button
|
||||
className="font-hse-sans min-w-[180px] bg-yellow-400 px-12 text-base text-black hover:bg-yellow-500"
|
||||
onClick={handleContinue}
|
||||
>
|
||||
Продолжить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-700 font-hse-sans text-lg leading-relaxed">
|
||||
|
||||
<div className="font-hse-sans text-lg leading-relaxed text-gray-700">
|
||||
<p>{competition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<h2 className="text-2xl font-bold mb-2 font-hse-sans">Соревнование не найдено</h2>
|
||||
<p className="text-gray-600 font-hse-sans">Запрошенное соревнование не существует или было удалено.</p>
|
||||
<div className="py-12 text-center">
|
||||
<h2 className="font-hse-sans mb-2 text-2xl font-bold">
|
||||
Соревнование не найдено
|
||||
</h2>
|
||||
<p className="font-hse-sans text-gray-600">
|
||||
Запрошенное соревнование не существует или было удалено.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -98,4 +103,4 @@ const CompetitionPreview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitionPreview;
|
||||
export default CompetitionPreview;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Task } from "@/shared/types/types";
|
||||
import { Task } from "@/shared/types";
|
||||
import { getTaskBgColor, getTaskTextColor } from "./utils/utils";
|
||||
import { mockTasks } from "@/shared/mocks/mocks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -41,16 +41,17 @@ const CompetitionRunnerPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-0 z-10 bg-white">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="py-3 text-center">
|
||||
<h1 className="text-lg font-semibold font-hse-sans">{competitionTitle}</h1>
|
||||
<>
|
||||
|
||||
<div className="sticky top-16 z-10 bg-white border-b border-gray-200 shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="py-4">
|
||||
<h1 className="text-xl font-semibold font-hse-sans">{competitionTitle}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 pb-3 overflow-x-auto no-scrollbar">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
<div
|
||||
key={task.id}
|
||||
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)}
|
||||
rounded-lg px-3 py-1.5 font-medium text-sm font-hse-sans cursor-pointer
|
||||
@@ -147,4 +148,4 @@ const CompetitionRunnerPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitionRunnerPage;
|
||||
export default CompetitionRunnerPage;
|
||||
|
||||
@@ -1,55 +1,47 @@
|
||||
import { Competition } from "@/shared/types/types";
|
||||
import { Competition, CompetitionStatus } from "@/shared/types";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from "@/components/ui/card";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
interface CompetitionCardProps {
|
||||
competition: Competition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CompetitionCard({ competition, className }: CompetitionCardProps) {
|
||||
const { id, name, imageUrl, isOlympics, status } = competition;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(`/competition/${id}`);
|
||||
};
|
||||
|
||||
export function CompetitionCard({
|
||||
competition,
|
||||
className,
|
||||
}: CompetitionCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={cn("overflow-hidden h-full", className)}
|
||||
onClick={handleClick}
|
||||
<Card
|
||||
className={cn(
|
||||
"aspect-square h-full max-h-80 w-auto overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={name}
|
||||
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<img
|
||||
src={competition.imageUrl}
|
||||
alt={competition.name}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardFooter className="p-4 pb-0 flex items-center text-xs font-medium font-hse-sans">
|
||||
<span className="text-gray-500">
|
||||
{isOlympics ? "Олимпиада" : "Тренировка"}
|
||||
</span>
|
||||
<span className="mx-2 w-1.5 h-1.5 rounded-full bg-gray-300"></span>
|
||||
<span className={cn(
|
||||
status === 'В процессе' && "text-yellow-500",
|
||||
status === 'Завершено' && "text-green-500",
|
||||
status === 'Не участвую' && "text-gray-500"
|
||||
)}>
|
||||
{status.replace(/^\w/, c => c.toUpperCase())}
|
||||
</span>
|
||||
</CardFooter>
|
||||
|
||||
<CardContent className="p-4 pt-2">
|
||||
<h3 className="font-semibold text-lg line-clamp-2 font-hse-sans">{name}</h3>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="text-muted-foreground flex items-center gap-2 *:text-sm *:font-semibold">
|
||||
<span>{competition.isOlympics ? "Олимпиада" : "Тренировка"}</span>
|
||||
{competition.status != CompetitionStatus.NotParticipating && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-primary-foreground">
|
||||
{competition.status}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{competition.name}</h3>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +1,122 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Competition, Status } from '@/shared/types/types';
|
||||
import { CompetitionGrid } from './modules/CompetitionGrid';
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Competition, CompetitionStatus } from "@/shared/types";
|
||||
import { CompetitionGrid } from "./modules/CompetitionGrid";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import Navbar from '@/modules/Navbar';
|
||||
import { mockCompetitions } from '@/shared/mocks/mocks';
|
||||
|
||||
const mockCompetitions: Competition[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: true,
|
||||
status: CompetitionStatus.InProgress,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: false,
|
||||
status: CompetitionStatus.NotParticipating,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: false,
|
||||
status: CompetitionStatus.InProgress,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: true,
|
||||
status: CompetitionStatus.Completed,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: false,
|
||||
status: CompetitionStatus.Completed,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: true,
|
||||
status: CompetitionStatus.NotParticipating,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: true,
|
||||
status: CompetitionStatus.NotParticipating,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Олимпиада DANO 2025. Индивидуальный этап",
|
||||
imageUrl: "/DANO.png",
|
||||
isOlympics: true,
|
||||
status: CompetitionStatus.NotParticipating,
|
||||
},
|
||||
];
|
||||
|
||||
const CompetitionsPage = () => {
|
||||
const [competitions, setCompetitions] = useState<Competition[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [competitions] = useState<Competition[]>(mockCompetitions);
|
||||
const [activeTab, setActiveTab] = useState("ongoing");
|
||||
|
||||
useEffect(() => {
|
||||
// ! симуляция фетча
|
||||
const fetchCompetitions = async () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
setCompetitions(mockCompetitions);
|
||||
setIsLoading(false);
|
||||
}, 800);
|
||||
} catch (error) {
|
||||
setError('Соревнования не найдены, пожалуйста, попробуйте позже');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCompetitions();
|
||||
}, []);
|
||||
|
||||
const myCompetitions = competitions.filter(comp =>
|
||||
comp.status === Status.InProgress || comp.status === Status.Completed
|
||||
const myCompetitions = competitions.filter(
|
||||
(comp) =>
|
||||
comp.status === CompetitionStatus.InProgress ||
|
||||
comp.status === CompetitionStatus.Completed,
|
||||
);
|
||||
|
||||
const filteredMyCompetitions = myCompetitions.filter(comp =>
|
||||
activeTab === "ongoing" ? comp.status === Status.InProgress : comp.status === Status.Completed
|
||||
|
||||
const filteredMyCompetitions = myCompetitions.filter((comp) =>
|
||||
activeTab === "ongoing"
|
||||
? comp.status === CompetitionStatus.InProgress
|
||||
: comp.status === CompetitionStatus.Completed,
|
||||
);
|
||||
|
||||
const availableCompetitions = competitions.filter(comp =>
|
||||
comp.status === 'Не участвую'
|
||||
|
||||
const availableCompetitions = competitions.filter(
|
||||
(comp) => comp.status === "Не участвую",
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="container mx-auto px-4 py-8 mt-16">
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-12">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-semibold font-hse-sans">Мои события</h2>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger value="ongoing" className="font-hse-sans">Текущие</TabsTrigger>
|
||||
<TabsTrigger value="completed" className="font-hse-sans">Завершенные</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<CompetitionGrid competitions={[]} isLoading={true} />
|
||||
) : filteredMyCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={filteredMyCompetitions} isLoading={false} />
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500 font-hse-sans">
|
||||
{activeTab === "ongoing" ? "У вас нет текущих соревнований" : "У вас нет завершенных соревнований"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6 font-hse-sans">Доступные события</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<CompetitionGrid competitions={[]} isLoading={true} />
|
||||
) : availableCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={availableCompetitions} isLoading={false} />
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500 font-hse-sans">Нет доступных соревнований</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<div className="flex flex-col gap-8">
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Мои события</SectionTitle>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="ongoing">В процессе</TabsTrigger>
|
||||
<TabsTrigger value="completed">Завершенные</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</SectionHeader>
|
||||
<CompetitionGrid competitions={filteredMyCompetitions} />
|
||||
</Section>
|
||||
|
||||
export default CompetitionsPage;
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>События</SectionTitle>
|
||||
</SectionHeader>
|
||||
<CompetitionGrid competitions={availableCompetitions} />
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Section = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex flex-col gap-8">{children}</div>;
|
||||
};
|
||||
|
||||
const SectionHeader = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex h-[58px] items-center gap-2">{children}</div>;
|
||||
};
|
||||
|
||||
const SectionTitle = ({ children }: { children: React.ReactNode }) => {
|
||||
return <h1 className="w-full text-3xl font-semibold">{children}</h1>;
|
||||
};
|
||||
|
||||
export default CompetitionsPage;
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
import { Competition } from "@/shared/types/types";
|
||||
import { Competition } from "@/shared/types";
|
||||
import { CompetitionCard } from "../../components/CompetitionCard";
|
||||
import CompetitionSkeleton from "../../components/CompetitionSkeleton";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
interface CompetitionGridProps {
|
||||
competitions: Competition[];
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
skeletonCount?: number;
|
||||
}
|
||||
|
||||
export function CompetitionGrid({
|
||||
competitions,
|
||||
isLoading = false,
|
||||
className,
|
||||
skeletonCount
|
||||
}: CompetitionGridProps) {
|
||||
const gridClasses = cn(
|
||||
"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6",
|
||||
className
|
||||
);
|
||||
|
||||
const numberOfSkeletons = skeletonCount ?? (competitions.length > 0 ? competitions.length : 4);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={gridClasses}>
|
||||
{Array.from({ length: numberOfSkeletons }).map((_, index) => (
|
||||
<CompetitionSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompetitionGrid({ competitions }: CompetitionGridProps) {
|
||||
return (
|
||||
<div className={gridClasses}>
|
||||
<div className="grid grid-cols-3 gap-9">
|
||||
{competitions.map((competition) => (
|
||||
<CompetitionCard
|
||||
key={competition.id}
|
||||
competition={competition}
|
||||
/>
|
||||
<CompetitionCard key={competition.id} competition={competition} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user