5dc3d7e3a3
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m37s
- Fix flex 'align=flex-start' shrinking content to min width in column
mode on matches, dashboard, and coupon-builder layouts (mobile/tablet
cards now span full width)
- Localize all user-facing hardcoded Turkish strings: lineups card,
odds card, v28 odds band panel, team/league pages, teams search,
auth toasts, league headers (new keys in tr/en messages)
- Populate empty predictions.prediction-reasons group (20 veto reasons)
and missing predictions.ui keys so the EN locale gets full coverage
- Use the active locale for all date formatting instead of hardcoded
tr-TR (match card, match detail, team detail)
- Replace raw native date input with Chakra Input (dark mode + theming)
- Rename match-detail first tab from 'Tüm Maçlar' to 'Genel Bakış'
- Fix '4 maçlar' grammar with a proper {count} maç message
- Delete dead 3300-line Chakra showcase component (home-card.tsx)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
306 lines
9.7 KiB
TypeScript
306 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Box,
|
|
Card,
|
|
Flex,
|
|
HStack,
|
|
Text,
|
|
VStack,
|
|
Badge,
|
|
SimpleGrid,
|
|
Icon,
|
|
} from "@chakra-ui/react";
|
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
|
import { useTranslations } from "next-intl";
|
|
import {
|
|
LuUsers,
|
|
LuUser,
|
|
LuInfo,
|
|
LuShieldCheck,
|
|
LuClock,
|
|
} from "react-icons/lu";
|
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
|
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
|
|
|
interface LineupsCardProps {
|
|
match: MatchResponseDto;
|
|
prediction?: MatchPredictionDto | null;
|
|
}
|
|
|
|
/**
|
|
* Lineup source metadata used for title, badge, and informational banners.
|
|
*/
|
|
function getLineupSourceMeta(
|
|
source: string | undefined,
|
|
t: ReturnType<typeof useTranslations>,
|
|
) {
|
|
switch (source) {
|
|
case "confirmed_live":
|
|
return {
|
|
title: t("lineup-official-xi"),
|
|
badge: t("lineup-confirmed-squad"),
|
|
badgeColor: "green" as const,
|
|
icon: LuShieldCheck,
|
|
description: t("lineup-confirmed-live-desc"),
|
|
};
|
|
case "confirmed_participation":
|
|
return {
|
|
title: t("lineup-confirmed-squad"),
|
|
badge: t("lineup-confirmed"),
|
|
badgeColor: "green" as const,
|
|
icon: LuShieldCheck,
|
|
description: t("lineup-participation-desc"),
|
|
};
|
|
case "probable_xi":
|
|
return {
|
|
title: t("lineup-probable-squad"),
|
|
badge: t("lineup-probable"),
|
|
badgeColor: "orange" as const,
|
|
icon: LuUsers,
|
|
description: t("lineup-probable-desc"),
|
|
};
|
|
case "none":
|
|
default:
|
|
return {
|
|
title: t("lineup-info"),
|
|
badge: t("lineup-pending"),
|
|
badgeColor: "gray" as const,
|
|
icon: LuClock,
|
|
description: t("lineup-pending-desc"),
|
|
};
|
|
}
|
|
}
|
|
|
|
export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|
const t = useTranslations("matches");
|
|
const cardBg = useColorModeValue("white", "gray.800");
|
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
|
const headerBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
|
const infoBg = useColorModeValue("blue.50", "whiteAlpha.100");
|
|
const infoBorder = useColorModeValue("blue.200", "blue.800");
|
|
|
|
let homeLineups = match.lineups?.home?.filter((p) => p.isStarting) || [];
|
|
let awayLineups = match.lineups?.away?.filter((p) => p.isStarting) || [];
|
|
|
|
// Determine lineup source from prediction data quality
|
|
const source = prediction?.data_quality?.lineup_source;
|
|
const meta = getLineupSourceMeta(source, t);
|
|
|
|
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
|
if (
|
|
homeLineups.length === 0 &&
|
|
match.lineups?.home &&
|
|
match.lineups.home.length > 0
|
|
) {
|
|
homeLineups = match.lineups.home.slice(0, 11);
|
|
}
|
|
if (
|
|
awayLineups.length === 0 &&
|
|
match.lineups?.away &&
|
|
match.lineups.away.length > 0
|
|
) {
|
|
awayLineups = match.lineups.away.slice(0, 11);
|
|
}
|
|
|
|
const hasLineups = homeLineups.length > 0 || awayLineups.length > 0;
|
|
|
|
return (
|
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
|
|
<Card.Body>
|
|
{/* ── Header ────────────────────────────────── */}
|
|
<Flex justify="space-between" align="center" mb={4}>
|
|
<HStack gap={2}>
|
|
<Icon as={meta.icon} boxSize={5} color="fg.muted" />
|
|
<Text fontSize="lg" fontWeight="semibold">
|
|
{meta.title}
|
|
</Text>
|
|
</HStack>
|
|
<Badge colorPalette={meta.badgeColor} variant="subtle">
|
|
{meta.badge}
|
|
</Badge>
|
|
</Flex>
|
|
|
|
{/* ── Info Banner ───────────────────────────── */}
|
|
{source !== "confirmed_live" && (
|
|
<Flex
|
|
bg={infoBg}
|
|
borderWidth="1px"
|
|
borderColor={infoBorder}
|
|
borderRadius="md"
|
|
p={3}
|
|
mb={4}
|
|
align="center"
|
|
gap={2}
|
|
>
|
|
<Icon as={LuInfo} color="blue.500" flexShrink={0} />
|
|
<Text fontSize="xs" color="fg.muted">
|
|
{meta.description}
|
|
</Text>
|
|
</Flex>
|
|
)}
|
|
|
|
{/* ── Lineups Grid ─────────────────────────── */}
|
|
{hasLineups ? (
|
|
<SimpleGrid columns={{ base: 1, md: 2 }} gap={6}>
|
|
{/* Home Team Lineup */}
|
|
<Box>
|
|
<Flex
|
|
bg={headerBg}
|
|
p={3}
|
|
borderRadius="md"
|
|
align="center"
|
|
justify="center"
|
|
mb={3}
|
|
gap={2}
|
|
>
|
|
<Text fontWeight="bold">{match.homeTeamName}</Text>
|
|
<Badge size="sm" variant="outline" colorPalette="blue">
|
|
Ev Sahibi
|
|
</Badge>
|
|
</Flex>
|
|
{homeLineups.length > 0 ? (
|
|
<VStack align="stretch" gap={2}>
|
|
{homeLineups.map((p, idx) => (
|
|
<HStack
|
|
key={p.player?.id || idx}
|
|
p={2}
|
|
borderWidth="1px"
|
|
borderColor={borderColor}
|
|
borderRadius="md"
|
|
>
|
|
<Icon as={LuUser} color="fg.muted" />
|
|
{p.shirtNumber && (
|
|
<Text fontSize="xs" fontWeight="bold" w="20px">
|
|
{p.shirtNumber}
|
|
</Text>
|
|
)}
|
|
<Text fontSize="sm" fontWeight="medium">
|
|
{p.player?.name || "Bilinmiyor"}
|
|
</Text>
|
|
{p.position && (
|
|
<Badge ml="auto" size="sm" variant="surface">
|
|
{p.position}
|
|
</Badge>
|
|
)}
|
|
</HStack>
|
|
))}
|
|
</VStack>
|
|
) : (
|
|
<Flex
|
|
p={4}
|
|
borderWidth="1px"
|
|
borderColor={borderColor}
|
|
borderRadius="md"
|
|
justify="center"
|
|
align="center"
|
|
direction="column"
|
|
gap={1}
|
|
>
|
|
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
|
{t("lineup-tbd")}
|
|
</Text>
|
|
<Text fontSize="xs" color="fg.subtle">
|
|
{t("lineup-tbd-sub")}
|
|
</Text>
|
|
</Flex>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Away Team Lineup */}
|
|
<Box>
|
|
<Flex
|
|
bg={headerBg}
|
|
p={3}
|
|
borderRadius="md"
|
|
align="center"
|
|
justify="center"
|
|
mb={3}
|
|
gap={2}
|
|
>
|
|
<Text fontWeight="bold">{match.awayTeamName}</Text>
|
|
<Badge size="sm" variant="outline" colorPalette="red">
|
|
Deplasman
|
|
</Badge>
|
|
</Flex>
|
|
{awayLineups.length > 0 ? (
|
|
<VStack align="stretch" gap={2}>
|
|
{awayLineups.map((p, idx) => (
|
|
<HStack
|
|
key={p.player?.id || idx}
|
|
p={2}
|
|
borderWidth="1px"
|
|
borderColor={borderColor}
|
|
borderRadius="md"
|
|
>
|
|
<Icon as={LuUser} color="fg.muted" />
|
|
{p.shirtNumber && (
|
|
<Text fontSize="xs" fontWeight="bold" w="20px">
|
|
{p.shirtNumber}
|
|
</Text>
|
|
)}
|
|
<Text fontSize="sm" fontWeight="medium">
|
|
{p.player?.name || "Bilinmiyor"}
|
|
</Text>
|
|
{p.position && (
|
|
<Badge ml="auto" size="sm" variant="surface">
|
|
{p.position}
|
|
</Badge>
|
|
)}
|
|
</HStack>
|
|
))}
|
|
</VStack>
|
|
) : (
|
|
<Flex
|
|
p={4}
|
|
borderWidth="1px"
|
|
borderColor={borderColor}
|
|
borderRadius="md"
|
|
justify="center"
|
|
align="center"
|
|
direction="column"
|
|
gap={1}
|
|
>
|
|
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
|
{t("lineup-tbd")}
|
|
</Text>
|
|
<Text fontSize="xs" color="fg.subtle">
|
|
{t("lineup-tbd-sub")}
|
|
</Text>
|
|
</Flex>
|
|
)}
|
|
</Box>
|
|
</SimpleGrid>
|
|
) : (
|
|
/* ── Empty State: No lineups at all ─────── */
|
|
<Flex
|
|
direction="column"
|
|
align="center"
|
|
justify="center"
|
|
py={8}
|
|
gap={3}
|
|
>
|
|
<Icon as={LuClock} boxSize={8} color="fg.subtle" />
|
|
<VStack gap={1}>
|
|
<Text fontWeight="semibold" color="fg.muted">
|
|
{t("lineup-not-announced")}
|
|
</Text>
|
|
<Text
|
|
fontSize="sm"
|
|
color="fg.subtle"
|
|
textAlign="center"
|
|
maxW="sm"
|
|
>
|
|
{t("lineup-not-announced-desc", {
|
|
home: match.homeTeamName,
|
|
away: match.awayTeamName,
|
|
})}
|
|
</Text>
|
|
</VStack>
|
|
</Flex>
|
|
)}
|
|
</Card.Body>
|
|
</Card.Root>
|
|
);
|
|
}
|