3 Commits

Author SHA1 Message Date
fahricansecer ab5864df2f Merge branch 'main' into v26-shadow
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 4m15s
2026-04-24 23:47:07 +03:00
fahricansecer bff5ea7b5f Update .gitignore 2026-04-24 23:47:04 +03:00
fahricansecer 14159911f0 v28 2026-04-24 23:46:50 +03:00
6 changed files with 246 additions and 97 deletions
+2
View File
@@ -1,3 +1,5 @@
node_modules
.next
.env.local
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --webpack --experimental-https -p 3001",
"dev": "next dev -p 6195",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint"
+155 -13
View File
@@ -12,7 +12,7 @@ import {
Icon,
} from "@chakra-ui/react";
import { useColorModeValue } from "@/components/ui/color-mode";
import { LuUsers, LuUser } from "react-icons/lu";
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";
@@ -21,41 +21,113 @@ interface LineupsCardProps {
prediction?: MatchPredictionDto | null;
}
/**
* Lineup source metadata used for title, badge, and informational banners.
*/
function getLineupSourceMeta(source?: string) {
switch (source) {
case "confirmed_live":
return {
title: "Resmi İlk 11",
badge: "Onaylı Kadro",
badgeColor: "green" as const,
icon: LuShieldCheck,
description: "Kadro resmi olarak onaylandı.",
};
case "confirmed_participation":
return {
title: "Onaylı Kadro",
badge: "Onaylı",
badgeColor: "green" as const,
icon: LuShieldCheck,
description: "Kadro maç katılım verilerinden alındı.",
};
case "probable_xi":
return {
title: "Muhtemel Kadro",
badge: "Muhtemel",
badgeColor: "orange" as const,
icon: LuUsers,
description:
"Son maçlardaki ilk 11 verilerine dayalı muhtemel kadro. AI analizi bu kadro üzerinden yapılmaktadır.",
};
case "none":
default:
return {
title: "Kadro Bilgisi",
badge: "Kadro Bekleniyor",
badgeColor: "gray" as const,
icon: LuClock,
description:
"Kadro henüz açıklanmadı. AI analizi, takımların genel güç dengesi ve istatistiklerine dayalı olarak üretilmiştir.",
};
}
}
export default function LineupsCard({ match, prediction }: LineupsCardProps) {
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");
const homeLineups = match.lineups?.home?.filter((p) => p.isStarting) || [];
const awayLineups = match.lineups?.away?.filter((p) => p.isStarting) || [];
let homeLineups = match.lineups?.home?.filter((p) => p.isStarting) || [];
let awayLineups = match.lineups?.away?.filter((p) => p.isStarting) || [];
if (homeLineups.length === 0 && awayLineups.length === 0) {
return null;
// Determine lineup source from prediction data quality
const source = prediction?.data_quality?.lineup_source;
const meta = getLineupSourceMeta(source);
// 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);
}
// Determine if it's confirmed or probable
const source = prediction?.data_quality?.lineup_source;
const isConfirmed = source === "confirmed_live";
const title = isConfirmed ? "İlk 11" : "Muhtemel Kadro";
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={LuUsers} boxSize={5} color="fg.muted" />
<Icon as={meta.icon} boxSize={5} color="fg.muted" />
<Text fontSize="lg" fontWeight="semibold">
{title}
{meta.title}
</Text>
</HStack>
<Badge
colorPalette={isConfirmed ? "green" : "orange"}
colorPalette={meta.badgeColor}
variant="subtle"
>
{isConfirmed ? "Onaylı" : "Muhtemel"}
{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>
@@ -66,9 +138,14 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
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
@@ -95,6 +172,25 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
</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">
Kadro henüz belli değil
</Text>
<Text fontSize="xs" color="fg.subtle">
Maç saatine yakın güncellenecek
</Text>
</Flex>
)}
</Box>
{/* Away Team Lineup */}
@@ -106,9 +202,14 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
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
@@ -135,8 +236,49 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
</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">
Kadro henüz belli değil
</Text>
<Text fontSize="xs" color="fg.subtle">
Maç saatine yakın güncellenecek
</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">
Kadro Henüz Açıklanmadı
</Text>
<Text fontSize="sm" color="fg.subtle" textAlign="center" maxW="sm">
{match.homeTeamName} ve {match.awayTeamName} kadroları maç saatine
yakın güncellenecektir. AI analizi, takım istatistikleri ve güç
dengesi üzerinden yapılmaktadır.
</Text>
</VStack>
</Flex>
)}
</Card.Body>
</Card.Root>
);
+10 -5
View File
@@ -100,11 +100,16 @@ export default function TeamDetailContent() {
const seasonActiveBg = useColorModeValue("primary.500", "primary.400");
const seasonInactiveBg = useColorModeValue("gray.100", "gray.700");
const team = (teamData as Record<string, unknown> | undefined)?.data as Record<string, unknown> | undefined;
const paginationData = matchesResponse;
const matches: MatchResponseDto[] = paginationData?.data ?? [];
const totalPages = paginationData?.totalPages ?? 1;
const totalMatches = paginationData?.total ?? 0;
// Backend ResponseInterceptor wraps all responses in { success, status, message, data }
const teamWrapper = teamData as Record<string, unknown> | undefined;
const team = teamWrapper?.data as Record<string, unknown> | undefined;
// matchesResponse = { success, status, message, data: { data: [...], total, page, limit, totalPages } }
const paginationWrapper = matchesResponse as Record<string, unknown> | undefined;
const paginationData = paginationWrapper?.data as Record<string, unknown> | undefined;
const matches: MatchResponseDto[] = (Array.isArray(paginationData?.data) ? paginationData.data : paginationData?.data ? [] : []) as MatchResponseDto[];
const totalPages = (paginationData?.totalPages as number) ?? 1;
const totalMatches = (paginationData?.total as number) ?? 0;
// Separate past and upcoming matches
const pastMatches = useMemo(
+1 -1
View File
File diff suppressed because one or more lines are too long