Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/leagues/[id]/ChampionshipClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export default function ChampionshipClient({ id }: Props) {
if (!edition) {
return <div className="p-4">Loading...</div>;
}

return (
<div className="flex p-4 gap-4 w-full">
<div className="flex flex-col lg:flex-row p-4 gap-4 w-full">
Expand All @@ -74,7 +73,8 @@ export default function ChampionshipClient({ id }: Props) {
onClick={() => router.back()}
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 mb-4"
>
<ArrowLeft size={16} /> Back
<ArrowLeft size={16} />
Back
</button>

<Tabs value={championshipTab} onValueChange={handleTabChange}>
Expand Down
122 changes: 96 additions & 26 deletions app/matchs/[id]/MatchTimelineClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { ArrowLeft } from "lucide-react";
import { FaFutbol, FaExchangeAlt } from "react-icons/fa";
import {
FaFutbol,
FaExchangeAlt,
FaCheckCircle,
FaTimesCircle,
} from "react-icons/fa";
import { BsFillSquareFill } from "react-icons/bs";
import { MatchService } from "@/services/MatchService";
import { MatchTimeline } from "@/components/matchs/MatchTimeline";
Expand All @@ -13,6 +18,8 @@ import { ITimelineEvent } from "@/interfaces/ITimelineEvent";
import { MatchHeader } from "@/components/matchs/MatchHeader";
import { CenteredMessage } from "@/components/CenteredMessage";
import { TimelineEventEnum } from "@/enums/timeline";
import { GoalStatus } from "@/enums/goals";
import { GoalType } from "@/enums/goals";

interface IProps {
id: string;
Expand All @@ -27,31 +34,92 @@ const sortByMatchTime = (a: ITimelineEvent, b: ITimelineEvent) => {
function buildEventsFromMatch(match: IMatch): ITimelineEvent[] {
const events: ITimelineEvent[] = [];

match.goals.forEach((goal) => {
events.push({
id: goal.id,
type: TimelineEventEnum.GOAL,
minute: goal.minute ?? 0,
stoppage_minute: goal.stoppage_minute ?? null,
team: goal.team,
icon: (
<>
<FaFutbol className={goal.is_csc ? "text-red-600" : "text-green-600"} />
{goal.is_csc && "(OG)"}
</>
),
title: goal.scorer ? `${goal.scorer.firstname} ${goal.scorer.lastname}` : "Unknown scorer",
description: goal.is_csc
? `Own goal`
: goal.assister
? `(${goal.assister.firstname} ${goal.assister.lastname})`
: "",
is_penalty: goal.is_penalty,
is_home:
(goal.team.id === match.home_team.id && !goal.is_csc) ||
(goal.team.id === match.away_team.id && goal.is_csc),
match.goals
.filter((elem) => {
return (
elem.status == GoalStatus.VALID &&
elem.goal_type != GoalType.PENALTY_SHOOTOUT
);
})
.forEach((goal) => {
events.push({
id: goal.id,
type: TimelineEventEnum.GOAL,
minute: goal.minute ?? 0,
stoppage_minute: goal.stoppage_minute ?? null,
team: goal.team,
icon: (
<>
<FaFutbol
className={goal.is_csc ? "text-red-600" : "text-green-600"}
/>
</>
),
title: goal.scorer
? `${goal.scorer.firstname} ${goal.scorer.lastname}`
: "Unknown scorer",
description: goal.is_csc
? `Own goal`
: goal.assister
? `(${goal.assister.firstname} ${goal.assister.lastname})`
: "",
is_penalty: goal.is_penalty,
is_home:
(goal.team.id === match.home_team.id && !goal.is_csc) ||
(goal.team.id === match.away_team.id && goal.is_csc),
});
});

// 2. MISSED goal
match.goals
.filter((elem) => {
return (
elem.status == (GoalStatus.CANCELLED || GoalStatus.MISSED) &&
elem.goal_type != GoalType.PENALTY_SHOOTOUT
);
})
.forEach((miss) => {
events.push({
id: miss.id,
type: TimelineEventEnum.GOAL, // Keep as Goal type for UI grouping or use a specific MISSED type
minute: miss.minute ?? 0,
stoppage_minute: miss.stoppage_minute ?? null,
team: miss.team,
is_missed: true,
icon: <FaTimesCircle className="text-gray-400" />,
title: miss.scorer
? `${miss.scorer.firstname} ${miss.scorer.lastname}`
: "Unknown scorer",
description: "Missed Penalty",
is_home: miss.team.id === match.home_team.id,
});
});

match.goals
.filter((elem) => {
return elem.goal_type == GoalType.PENALTY_SHOOTOUT;
})
.forEach((attempt, index) => {
events.push({
id: attempt.id,
type: TimelineEventEnum.PENALTY_SHOOTOUT,
minute: 121,
shootout_order: attempt.shootout_order || index + 1, // Used for side-by-side alignment
team: attempt.team,
is_missed: attempt.status != GoalStatus.VALID,
icon:
attempt.status != GoalStatus.VALID ? (
<FaCheckCircle className="text-green-500" />
) : (
<FaTimesCircle className="text-red-500" />
),
title: attempt.scorer
? `${attempt.scorer.firstname} ${attempt.scorer.lastname}`
: "Unknown scorer",
description: "",
is_home: attempt.team.id === match.home_team.id,
});
});
});

match.cards.forEach((card) => {
events.push({
Expand All @@ -66,7 +134,9 @@ function buildEventsFromMatch(match: IMatch): ITimelineEvent[] {
) : (
<BsFillSquareFill className="text-yellow-400" />
),
title: card.player ? `${card.player.firstname} ${card.player.lastname}` : "Unknown player",
title: card.player
? `${card.player.firstname} ${card.player.lastname}`
: "Unknown player",
description: "",
is_home: card.team.id === match.home_team.id,
});
Expand Down
17 changes: 14 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MatchsPageClient from "./MatchsPageClient";
import { homePageText, appDomain } from "@/constants/seoTexts";
import { DateContextProvider } from "@/contexts/DateContext";

export const metadata = {
title: "Home | Scoreboards",
Expand All @@ -13,6 +14,16 @@ export const metadata = {
},
};

export default function Home() {
return <MatchsPageClient />;
}
export default async function Home({
searchParams,
}: {
searchParams: Promise<{ date?: string }>;
}) {
const { date } = await searchParams;

return (
<DateContextProvider initialDate={date}>
<MatchsPageClient />
</DateContextProvider>
);
}
6 changes: 3 additions & 3 deletions components/StandingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface IStandingCardProps {
}

export const StandingCard: React.FC<IStandingCardProps> = (
props: IStandingCardProps
props: IStandingCardProps,
) => {
return (
<tr className="border-b">
Expand All @@ -18,9 +18,9 @@ export const StandingCard: React.FC<IStandingCardProps> = (
<Image
src={props.standing.participation.team.logo}
alt={props.standing.participation.team.name}
width={6}
height={6}
className="w-6 h-6 mr-2 inline-block"
width={64}
height={64}
/>
)}
{props.standing.participation.team.name}
Expand Down
72 changes: 57 additions & 15 deletions components/matchs/MatchTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,74 @@
"use client"
"use client";

import { TimelineItem } from "./TimelineItem";
import { ITimelineEvent } from "@/interfaces/ITimelineEvent";
import { TimelineEventEnum } from "@/enums/timeline";
import { ShootoutCompactItem } from "./TimelineItem";
import { sortByMatchTime } from "@/lib/utils";

interface IMatchTimelineProps {
events: ITimelineEvent[];
}

export const MatchTimeline: React.FC<IMatchTimelineProps> = (
props: IMatchTimelineProps
) => {
if (!props.events.length) {
return (
<p className="text-center text-gray-500 py-10">
Waiting for match events...
</p>
);
}
export const MatchTimeline: React.FC<IMatchTimelineProps> = ({ events }) => {
if (!events.length)
return <p className="text-center py-10">Waiting for events...</p>;

const regularEvents = events.filter(
(e) => e.type !== TimelineEventEnum.PENALTY_SHOOTOUT,
);
const shootoutEvents = events.filter(
(e) => e.type === TimelineEventEnum.PENALTY_SHOOTOUT,
);

const shootoutRounds = Array.from(
new Set(shootoutEvents.map((e) => e.shootout_order)),
).sort((a, b) => a! - b!);

return (
<div className="relative py-8">
<div className="absolute left-1/2 transform -translate-x-1/2 w-0.5 h-full bg-gray-200" />
<div className="relative py-8 w-full max-w-2xl mx-auto">
<div className="absolute left-1/2 transform -translate-x-1/2 w-px h-full bg-slate-200" />

<div className="space-y-12">
{props.events.map((event) => (
<div className="space-y-6">
{regularEvents.sort(sortByMatchTime).map((event) => (
<TimelineItem key={`${event.type}-${event.id}`} event={event} />
))}
</div>

{shootoutRounds.length > 0 && (
<div className="mt-12 space-y-4">
<h3 className="text-center font-bold text-sm uppercase tracking-widest text-slate-400 mb-8">
Penalty Shootout
</h3>
{shootoutRounds.map((round) => {
const homeShot = shootoutEvents.find(
(e) => e.shootout_order === round && e.is_home,
);
const awayShot = shootoutEvents.find(
(e) => e.shootout_order === round && !e.is_home,
);

return (
<div
key={`round-${round}`}
className="flex items-center justify-between relative"
>
<div className="flex-1 text-right pr-6">
{homeShot && <ShootoutCompactItem event={homeShot} />}
</div>

<div className="z-10 bg-white px-2 text-[10px] font-bold text-slate-400">
{round}
</div>

<div className="flex-1 text-left pl-6">
{awayShot && <ShootoutCompactItem event={awayShot} />}
</div>
</div>
);
})}
</div>
)}
</div>
);
};
Loading
Loading