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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Next.js CI

on:
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install Dependencies
run: npm ci

- name: Run Lint
run: npm run lint

- name: Build Application
run: npm run build
4 changes: 2 additions & 2 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export default function AboutPage() {
<p>
We envision a future where fans, analysts, and football
enthusiasts can easily access reliable information about
Cameroon's local leagues. By combining real-time match updates
Cameroon&apos;s local leagues. By combining real-time match updates
with a growing historical data archive, Scoreboards aims to
enhance the experience of following football in Cameroon.
</p>
Expand All @@ -127,7 +127,7 @@ export default function AboutPage() {
<h2 className="text-2xl font-semibold">About Our Data</h2>
<p>
At Scoreboards, we are committed to collecting accurate and
comprehensive data from Cameroon's local football leagues. Every
comprehensive data from Cameroon&apos;s local football leagues. Every
match, player performance, and team statistic is carefully
recorded to create a reliable historical archive.
</p>
Expand Down
2 changes: 1 addition & 1 deletion app/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function ContactPage() {
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold text-primary">Contact Us</h1>
<p className="text-muted-foreground">
Have questions or feedback? Fill out the form below and we'll get
Have questions or feedback? Fill out the form below and we&apos;ll get
back to you as soon as possible.
</p>
</div>
Expand Down
55 changes: 27 additions & 28 deletions app/leagues/[id]/ChampionshipClient.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, MapPin } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
Expand All @@ -22,9 +22,10 @@ interface Props {
export default function ChampionshipClient({ id }: Props) {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();

const [edition, setEdition] = useState<IEdition | null>(null);
const [championshipTab, setChampionshipTab] = useState("table");
const championshipTab = searchParams.get("tab") || "table";
const [matchTab, setMatchTab] = useState("all");

useEffect(() => {
Expand All @@ -33,10 +34,15 @@ export default function ChampionshipClient({ id }: Props) {
setEdition(data);
};
loadEdition();
// Tab logic removed from here to prevent cascading renders
}, [id]);

const tab = searchParams.get("tab");
if (tab) setChampionshipTab(tab);
}, [id, searchParams]);
// 2. Helper to update tabs via URL
const handleTabChange = (value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set("tab", value);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
};

if (!edition) {
return <div className="p-4">Loading...</div>;
Expand All @@ -46,21 +52,19 @@ export default function ChampionshipClient({ id }: Props) {
<div className="flex p-4 gap-4 w-full">
<div className="flex flex-col lg:flex-row p-4 gap-4 w-full">
<div className="w-full lg:w-1/4 flex flex-col items-center">
{edition.championship?.logo && <Avatar className="mb-4 w-24 h-24 lg:w-32 lg:h-32">
<AvatarImage src={edition.championship.logo} />
<AvatarFallback>{edition.label}</AvatarFallback>
</Avatar>}
{edition.championship?.logo && (
<Avatar className="mb-4 w-24 h-24 lg:w-32 lg:h-32">
<AvatarImage src={edition.championship.logo} />
<AvatarFallback>{edition.label}</AvatarFallback>
</Avatar>
)}

<h1 className="text-lg font-semibold text-center">
{edition.label}
</h1>
<h1 className="text-lg font-semibold text-center">{edition.label}</h1>

{edition.championship.country && (
<div className="flex items-center gap-2 pt-4 font-semibold">
<MapPin className="w-4 h-4" />
<span className="text-sm">
{edition.championship.country}
</span>
<span className="text-sm">{edition.championship.country}</span>
</div>
)}
</div>
Expand All @@ -70,24 +74,16 @@ 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={setChampionshipTab}
>
<Tabs value={championshipTab} onValueChange={handleTabChange}>
<TabsList className="flex w-full overflow-x-auto gap-2">
<TabsTrigger value="table">Table</TabsTrigger>
<TabsTrigger value="teams">Teams</TabsTrigger>
<TabsTrigger value="matchs">Matches</TabsTrigger>
<TabsTrigger value="teams_stats">
Team Stats
</TabsTrigger>
<TabsTrigger value="players_stats">
Player Stats
</TabsTrigger>
<TabsTrigger value="teams_stats">Team Stats</TabsTrigger>
<TabsTrigger value="players_stats">Player Stats</TabsTrigger>
</TabsList>

<TabsContent value="table">
Expand All @@ -109,7 +105,10 @@ export default function ChampionshipClient({ id }: Props) {
</TabsContent>

<TabsContent value="teams_stats">
<ChampionshipTeamStats championshipName={edition.label} championshipId={edition.id} />
<ChampionshipTeamStats
championshipName={edition.label}
championshipId={edition.id}
/>
</TabsContent>

<TabsContent value="matchs">
Expand Down
5 changes: 4 additions & 1 deletion components/StandingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { IStanding } from "../interfaces/IStanding";
import Image from "next/image";

interface IStandingCardProps {
standing: IStanding;
Expand All @@ -14,9 +15,11 @@ export const StandingCard: React.FC<IStandingCardProps> = (
<td className="p-2">{props.index + 1}</td>
<td className="p-2">
{props.standing.participation.team.logo && (
<img
<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"
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion components/matchs/TimelineItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const TimelineItem: React.FC<ItemProps> = ({ event }) => {
</span>
<span className="text-[10px] font-bold text-gray-400 bg-gray-50 px-1.5 rounded">
{event.minute}
{event.stoppage_minute ? `+${event.stoppage_minute}` : ""}'
{event.stoppage_minute ? `+${event.stoppage_minute}` : ""}&apos;
</span>
</div>
<div className="flex flex-col items-start justify-center">
Expand Down
5 changes: 4 additions & 1 deletion components/players/PlayerTeams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { PlayerService } from "@/services/PlayerService";
import { IPlayer, IPlayerTeamHistory } from "@/interfaces/IPlayers";
import { Loader2 } from "lucide-react";
import Image from "next/image";

interface IPlayerTeamsProps {
player: IPlayer;
Expand Down Expand Up @@ -61,9 +62,11 @@ export const PlayerTeams: React.FC<IPlayerTeamsProps> = ({ player }) => {
className="flex items-center gap-4 border rounded-lg p-4 shadow-sm hover:shadow-md transition"
>
{h.team.logo && (
<img
<Image
src={h.team.logo}
alt={h.team.name}
width={10}
height={10}
className="w-10 h-10 rounded-full"
/>
)}
Expand Down
9 changes: 7 additions & 2 deletions components/players/PlayerTransferts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IPlayer } from "@/interfaces/IPlayers";
import { IPlayerTransfers } from "@/interfaces/ITransfers";
import { Loader2 } from "lucide-react";
import { ArrowRight } from "lucide-react";
import Image from "next/image";

interface IPlayerTransfertsProps {
player: IPlayer;
Expand Down Expand Up @@ -59,9 +60,11 @@ export const PlayerTransferts: React.FC<IPlayerTransfertsProps> = ({
{/* From team */}
<div className="flex items-center gap-2 w-1/3 justify-end">
{t.team?.logo && (
<img
<Image
src={t.team.logo}
alt={t.team.name}
width={8}
height={8}
className="w-8 h-8 rounded-full"
/>
)}
Expand All @@ -76,9 +79,11 @@ export const PlayerTransferts: React.FC<IPlayerTransfertsProps> = ({
{/* To team */}
<div className="flex items-center gap-2 w-1/3">
{t.team?.logo && (
<img
<Image
src={t.team.logo}
alt={t.team.name}
width={8}
height={8}
className="w-8 h-8 rounded-full"
/>
)}
Expand Down
88 changes: 46 additions & 42 deletions components/standings/StandingDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from "react";
"use client";

import React, { useMemo } from "react";
import {
ColumnDef,
flexRender,
Expand All @@ -21,60 +23,62 @@ interface IDataTableProps {
standings: IStanding[];
}

export const StandingDataTable: React.FC<IDataTableProps> = (
props: IDataTableProps
) => {
export const StandingDataTable: React.FC<IDataTableProps> = ({
columns: propColumns,
standings,
}) => {
const columns = useMemo(() => propColumns, [propColumns]);

const data = useMemo(() => [...standings].sort(standingSort), [standings]);

const table = useReactTable({
data: props.standings.sort(standingSort),
columns: props.columns,
data,
columns,
getCoreRowModel: getCoreRowModel(),
});

return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={props.columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
};
3 changes: 0 additions & 3 deletions components/teams/ChampionshipTeamList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export const ChampionshipTeamList: React.FC<IChampionshipTeamListProps> = ({

return (
<div className="flex flex-col w-full rounded-lg p-4">
<h2 className="text-2xl font-semibold mb-4 w-full">
{championship.name}'s Teams
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 w-full">
{teams.map((team) => (
<Card
Expand Down
Loading
Loading