fix: merge conflicts

This commit is contained in:
rngsurrounded
2025-03-01 19:34:35 +09:00
44 changed files with 908 additions and 404 deletions
@@ -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>
);
}
}