diff --git a/package.json b/package.json index 2a87682..2db2e78 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "axios": "^1.4.0", "dayjs": "^1.11.7", "decimal.js": "^10.4.3", + "html-react-parser": "^5.0.0", "i18next-browser-languagedetector": "^7.0.1", "lodash": "^4.17.21", "moment": "^2.29.4", diff --git a/src/components/AbstractTeam/AbstractTeam.tsx b/src/components/AbstractTeam/AbstractTeam.tsx index c522e2e..1009484 100644 --- a/src/components/AbstractTeam/AbstractTeam.tsx +++ b/src/components/AbstractTeam/AbstractTeam.tsx @@ -3,11 +3,12 @@ import { useSelector } from "react-redux"; import { FootballPicker } from "@/lib/football-picker"; import { FootballMaxPositionsPicks, FootballPositionIds } from "@/lib/constants"; import Decimal from "decimal.js"; -import { useAddTeamMutation, useUpdateTeamSelectionMutation } from "@/services/teamsApi"; +import { useAddTeamMutation, useSubmitTransfersMutation, useUpdateTeamSelectionMutation } from "@/services/teamsApi"; import { openErrorNotification, openSuccessNotification } from "@/lib/helpers"; import { t } from "i18next"; import { useLazyGetTeamsQuery } from "@/services/usersApi"; import { useGetDeadlineInfoQuery } from "@/services/weeksApi"; +import { pick } from "lodash"; declare type AbstractTeamProps = { matches?: any; @@ -52,6 +53,12 @@ declare type AbstractTeamState = { teamUser?: any validator?: any savingTeamPending?: any + visibleWeekId: number | null + initializedExternally: boolean + boosters: Boosters + deadlineWeekTransfers: Transfer[], + draftTransfers: Transfer[], + pastTransfers: Transfer[], } function playersToValidatorFormat(players: any) { @@ -73,7 +80,9 @@ const getInitializedList = (size: number, forStarting?: boolean) => { export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: AbstractTeamProps, options?: Options,) => { const [addTeam] = useAddTeamMutation(); const [updateTeamSelections, { isSuccess: updateTeamSelectionsSucces, data: updateTeamSelectionsResult }] = useUpdateTeamSelectionMutation(); + const { data: deadlineInfo, isSuccess: deadlineInfoSuccess, isLoading: deadlineInfoLoading, isError: deadlineInfoError } = useGetDeadlineInfoQuery(); const [getTeams] = useLazyGetTeamsQuery(); + const [submitTransfers, { isSuccess: submitTransfersSucces, data: submitTransfersResult }] = useSubmitTransfersMutation(); const application = useSelector((state: StoreState.All) => state.application); @@ -92,6 +101,12 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: swapPlayerId: null, swapPlayer: null, swappedFrom: null, + visibleWeekId: deadlineInfoSuccess ? (options && options.mode === 'points' ? deadlineInfo.deadlineInfo.displayWeek : deadlineInfo.deadlineInfo.deadlineWeek) : 0, + boosters: {}, + + deadlineWeekTransfers: [], + draftTransfers: [], + pastTransfers: [], initialStarting: getInitializedList(application.competition.lineupSize, true), initialBench: getInitializedList(application.competition.benchSize), @@ -99,6 +114,7 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: teamUser: undefined, activePositionFilter: -1, + initializedExternally: false, }); const setStarting = (starting: any[]) => { @@ -131,10 +147,18 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: starting: any[], bench: any[], teamName: string, + captainId: number, budget: number, - captainId?: number, + leagues?: any[] | undefined, + visibleWeekId?: number | undefined, + teamPointsInfo?: any, + rawTransfers?: any[] | undefined, + deadlineWeekTransfers?: any[] | undefined, + pastTransfers?: any[] | undefined, viceCaptainId?: number, - teamUser?: any, + boosters?: Boosters, + isTeamOwner?: boolean, + teamUser?: any ) => { const startingPlayersValidatorFormat = playersToValidatorFormat(starting); const benchPlayersValidatorFormat = playersToValidatorFormat(bench); @@ -154,10 +178,16 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: initialBench: bench, initialStarting: starting, initialBudget: budget, + visibleWeekId: visibleWeekId | state.visibleWeekId, + initializedExternally: true, + boosters: boosters || {}, + deadlineWeekTransfers: deadlineWeekTransfers || [], + draftTransfers: [], + pastTransfers: pastTransfers || [], }); }; - const pickPlayer = (player: Player) => { + const pickPlayer = (player: Player, taxForPicking?: boolean) => { const nilPlayer: any = null; const alreadyInTeam: Player = [].concat(state.initialStarting, state.initialBench).find((item: Player) => item && player && item.id === player.id); @@ -257,7 +287,7 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: const captainId = state.captainId === player.id ? undefined : state.captainId; const viceCaptainId = state.viceCaptainId === player.id ? undefined : state.captainId; - // const removeResult = state.validator.remove(player); + const removeResult = state.validator.remove(player); setState({ ...state, starting: newStarting, budget, captainId, viceCaptainId }); }; @@ -276,7 +306,7 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: .plus(player.value.toFixed(2)) .toString()); - // const removeResult = state.validator.remove(player); + const removeResult = state.validator.remove(player); setState({ ...state, bench: newBench, budget }); }; @@ -503,7 +533,7 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: } }; - const isPickable = (player: Player) => { + const isPickable = (player: Player, taxForPicking?: boolean, isTransferPick?: boolean) => { const notInStarting = !state.starting.find(startingPlayer => startingPlayer && startingPlayer.id && startingPlayer.id === player.id); const notInBench = !state.bench.find(benchPlayer => benchPlayer && benchPlayer.id && benchPlayer.id === player.id); const affordable = player.value <= state.budget; @@ -568,27 +598,27 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: const onTransferPlayerOut = (player: Player, extra?: boolean) => { removePlayer(player); - // const draftTransfers = state.draftTransfers - // .concat([{ - // inId: null, - // outId: player.id, - // outPlayer: player, - // extra, - // // weekId: matches.info.deadlineWeek - // }]); - // setState({ ...state, draftTransfers }); + const draftTransfers = state.draftTransfers + .concat([{ + inId: null, + outId: player.id, + outPlayer: player, + extra, + weekId: deadlineInfo.deadlineInfo.deadlineWeek + }]); + setState(previousState => ({ ...previousState, draftTransfers })); }; const onTransferPlayerIn = (player: Player) => { - // const draftTransfers = ([] as Transfer[]).concat(state.draftTransfers); - // for (let tfIdx = 0; tfIdx < draftTransfers.length; tfIdx++) { - // if (!draftTransfers[tfIdx].inId && draftTransfers[tfIdx].outPlayer?.positionId === player.positionId) { - // draftTransfers[tfIdx].inId = player.id; - // draftTransfers[tfIdx].inPlayer = player; - // break; - // } - // } - // setState({ ...state, draftTransfers }); + const draftTransfers = ([] as Transfer[]).concat(state.draftTransfers); + for (let tfIdx = 0; tfIdx < draftTransfers.length; tfIdx++) { + if (!draftTransfers[tfIdx].inId && draftTransfers[tfIdx].outPlayer?.positionId === player.positionId) { + draftTransfers[tfIdx].inId = player.id; + draftTransfers[tfIdx].inPlayer = player; + break; + } + } + setState(previousState => ({ ...previousState, draftTransfers })); }; const onDraftTransfersClear = () => { @@ -598,16 +628,18 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: setState({ ...state, + draftTransfers: [], starting: state.initialStarting, bench: state.initialBench, budget: state.initialBudget }); }; const onTransfersSubmit = (teamId: number) => { - // const transfers = state.draftTransfers - // .map((transfer: Transfer) => pick(transfer, ["inId", "outId"])); - // return teamsActions.submitTransfers(teamId, transfers) - // TODO: POST team/transfers/:teamid + console.log("TRANSFERS SUBMITTED"); + const transfers = state.draftTransfers + .map((transfer: Transfer) => pick(transfer, ["inId", "outId"])); + + submitTransfers({teamId, transfers}).unwrap().then((res) => openSuccessNotification({ title: res.msg })).catch((err) => openErrorNotification({ title: t(`team.transfers.failed`) })); }; const onTransfersReset = (teamId: number) => { @@ -668,6 +700,12 @@ export const AbstractTeam = (Component: (props: AbstractTeamType) => any, props: onTransfersReset={onTransfersReset} reloadUserTeams={reloadUserTeams} teamUser={state.teamUser} + visibleWeekId={state.visibleWeekId} + initializedExternally={state.initializedExternally} + boosters={state.boosters} + draftTransfers={state.draftTransfers} + deadlineWeekTransfers={state.deadlineWeekTransfers} + pastTransfers={state.pastTransfers} {...props} /> diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 8be7c60..e4bad4d 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -81,9 +81,7 @@ export const Calendar = (props: CalendarProps) => { render: (homeId: any, record: any) => { const clubBadge = `${config.API_URL}/static/badges/${record.home.externalId}.png`; - return + return ; diff --git a/src/components/Calendar/CalendarStyle.tsx b/src/components/Calendar/CalendarStyle.tsx index c17c96b..7c7e888 100644 --- a/src/components/Calendar/CalendarStyle.tsx +++ b/src/components/Calendar/CalendarStyle.tsx @@ -56,7 +56,7 @@ table { } `; -export const ClubDetails = styled.div` +export const ClubDetails = styled.div<{left?: boolean}>` display: flex; padding: 5px; justify-content: ${(props: any) => props.left ? "flex-end": "flex-start"}; diff --git a/src/components/ConfirmModal/ConfirmModal.tsx b/src/components/ConfirmModal/ConfirmModal.tsx new file mode 100644 index 0000000..b0bf39e --- /dev/null +++ b/src/components/ConfirmModal/ConfirmModal.tsx @@ -0,0 +1,40 @@ +import { Button } from "../UI/Button/Button" +import { Col, Row } from "../UI/Grid/Grid" +import { ConfirmModalStyle } from "./ConfirmModalStyle" +import { useTranslation } from "react-i18next" + +declare type ConfirmModalProps = { + visible: boolean + title: string + text: string + onCancel: any + onConfirm: any +} + +export const ConfirmModal = (props: any) => { + const { visible, onCancel, onConfirm, text, title } = props; + const { t } = useTranslation(); + + return ( + + + + {text} + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/ConfirmModal/ConfirmModalStyle.tsx b/src/components/ConfirmModal/ConfirmModalStyle.tsx new file mode 100644 index 0000000..35a1875 --- /dev/null +++ b/src/components/ConfirmModal/ConfirmModalStyle.tsx @@ -0,0 +1,58 @@ +import styled from "@/styles/styled-components"; +import { Modal } from "antd"; + +export const ConfirmModalStyle = styled(Modal)` +.ant-modal-content { + border-radius: 0px; + max-width: 575px; + + .ant-modal-title { + font-family: "C-Bold"; + text-transform: uppercase; + background: #000; + padding: 5px 5px 5px 36.5px; + } + + .ant-modal-close-x { + width: 30px; + height: 30px; + font-size: 20px; + color: 84FF00; + line-height: 30px; + } + + .ant-modal-header { + border: 0px; + border-radius: 0px; + padding: 0; + + .ant-modal-title { + color: white; + p { + margin: 0px; + } + + .custom-title-container { + text-align: right; + + .anticon { + margin-top: 5px; + margin-right: 5px; + } + } + } + } + + .ant-modal-footer { + display: none; + } +} + +.actions { + text-align: right; + margin-top: 15px; + button { + margin: 5px; + } +} +`; \ No newline at end of file diff --git a/src/components/Player/Player.tsx b/src/components/Player/Player.tsx index 95bbc9c..52a7fdd 100644 --- a/src/components/Player/Player.tsx +++ b/src/components/Player/Player.tsx @@ -49,6 +49,9 @@ declare type PlayerProps = { onSwap?: any onCaptainSelect?: any onViceCaptainSelect?: any + benchPlayer?: boolean + showPlayerValue?: boolean + showPlayerValueInsteadOfPoints?: boolean className?: string } @@ -76,6 +79,10 @@ export const Player = (props: PlayerProps) => { onSwap, onCaptainSelect, onViceCaptainSelect, + showPlayerValueInsteadOfPoints, + showPlayerValue, + benchPlayer, + // showPlayerStatsPoints, } = props; const [state, setState] = useState({ portraitFace: props.portraitFace, @@ -132,7 +139,7 @@ export const Player = (props: PlayerProps) => { const isCaptain = useMemo(() => player && player.id && player.id === captainId, [player, captainId]); const isViceCaptain = useMemo(() => player && player.id && player.id === viceCaptainId, [player, viceCaptainId]); - const showPoints = true; + const showPoints = (player && player.points !== undefined && player.points !== null) || showPlayerValueInsteadOfPoints; //todo const showPlayerName = !avatarOnly; const onRemoveHandler = (e: any, player: Player) => { @@ -189,7 +196,14 @@ export const Player = (props: PlayerProps) => {

: null } - + { + ( + player && showPlayerValue && + + {(player.value) ? `€${player.value}M` : null} + + ) || null + } { !player || (player && !player.id) && onPlaceholderClick(player) : () => { }}> diff --git a/src/components/PlayerList/PlayerList.tsx b/src/components/PlayerList/PlayerList.tsx index ee1a019..c543b6f 100644 --- a/src/components/PlayerList/PlayerList.tsx +++ b/src/components/PlayerList/PlayerList.tsx @@ -34,6 +34,9 @@ declare type PlayerListProps = { actionLabel?: string isLoading?: boolean playerType: PlayerType + matches?: any + deadlineWeek?: any; + playerTax?: number | undefined; } declare type PlayerListState = { diff --git a/src/components/PlayerList/PlayerListStyle.ts b/src/components/PlayerList/PlayerListStyle.ts index df2ebd8..ed8de7c 100644 --- a/src/components/PlayerList/PlayerListStyle.ts +++ b/src/components/PlayerList/PlayerListStyle.ts @@ -114,10 +114,6 @@ export const TableStyle = styled(Table)` border: none; padding: 3.5px; - &:first-child { - padding: 2.5px 10px 0 15px; - } - & p { margin-top:0; } diff --git a/src/components/PlayerModal/PlayerModal.tsx b/src/components/PlayerModal/PlayerModal.tsx index 44286c0..5500b58 100644 --- a/src/components/PlayerModal/PlayerModal.tsx +++ b/src/components/PlayerModal/PlayerModal.tsx @@ -107,7 +107,7 @@ export const PlayerModal = (props: PlayerModalProps) => { { - onCaptainSelect ? + props.onCaptainSelect ?
@@ -117,7 +117,7 @@ export const PlayerModal = (props: PlayerModalProps) => { null } { - onViceCaptainSelect ? + props.onViceCaptainSelect ?
@@ -128,7 +128,7 @@ export const PlayerModal = (props: PlayerModalProps) => { } { - onSwap && isSwapAble && isSwapAble(player) && (player.id !== swapPlayerId) ? + props.onSwap && isSwapAble && props.isSwapAble(player) && (player.id !== swapPlayerId) ?
@@ -139,7 +139,7 @@ export const PlayerModal = (props: PlayerModalProps) => { } { - onSwap && (player.id === swapPlayerId) ? + props.onSwap && (player.id === swapPlayerId) ?
diff --git a/src/components/Stats/TransfersOverview.tsx b/src/components/Stats/TransfersOverview.tsx new file mode 100644 index 0000000..62a9140 --- /dev/null +++ b/src/components/Stats/TransfersOverview.tsx @@ -0,0 +1,33 @@ +import { StatsStyle } from "@/components/Stats/StatsStyle" +import { Col, Row } from "../UI/Grid/Grid" +import { useTranslation } from "react-i18next" + +type TransfersOverviewProps = { + budget: number + totalPlayers: number + totalPlayersSelected: number + remainingFreeTransfers: number + minusPoints: number +} + +export const TransfersOverview = (props: TransfersOverviewProps) => { + const {t} = useTranslation(); + return ( + + + +

{props.budget.toFixed(2)}M

+

{t('TransfersPage.overviewBudget')}

+ + +

{props.totalPlayersSelected}/{props.totalPlayers}

+

{t('TransfersPage.overviewPlayers')}

+ + +

{props.remainingFreeTransfers < 0 ? 0 : props.remainingFreeTransfers}

+

{t('TransfersPage.overviewTransfers')}

+ +
+
+ ) +} \ No newline at end of file diff --git a/src/components/Team/Team.tsx b/src/components/Team/Team.tsx index a63eec6..ce87120 100644 --- a/src/components/Team/Team.tsx +++ b/src/components/Team/Team.tsx @@ -94,11 +94,11 @@ export const Team = (props: TeamProps) => { viceCaptainId={viceCaptainId} player={player} // showPlayerStatsPoints={showPlayerStatsPoints} - // showPlayerValue={showPlayerValue} + showPlayerValue={showPlayerValue} showCaptainBadge={showCaptainBadge} // type={playerType} onRemove={onRemove} - // showPlayerValueInsteadOfPoints={showPlayerValueInsteadOfPoints} + showPlayerValueInsteadOfPoints={showPlayerValueInsteadOfPoints} onSwap={onSwap} isSwapable={isSwapAble} onCaptainSelect={onCaptainSelect} diff --git a/src/components/TransfersList/TransfersList.tsx b/src/components/TransfersList/TransfersList.tsx new file mode 100644 index 0000000..79263f4 --- /dev/null +++ b/src/components/TransfersList/TransfersList.tsx @@ -0,0 +1,88 @@ +import { useTranslation } from "react-i18next" +import { ContainerStyle, TableStyle } from "../PlayerList/PlayerListStyle" +import React from "react" + +type TransfersListProps = { + data: any + size: number + isLoading?: boolean + showHeader?: boolean + showWeek?: boolean + tax?: number | undefined +} + +export const TransfersList = (props: TransfersListProps) => { + const { t } = useTranslation(); + + const {data,size, isLoading, showHeader, showWeek} = props; + const columns = [ + { + title: '#', + key: 'number', + dataIndex: 'number', + width: '20%', + render: (txt: string, rec: any, idx: number) => { + return {idx + 1}; + }, + }, + { + title: t('team.transferOut'), + key: 'outPlayer', + dataIndex: 'outPlayer', + width: showWeek ? '30%' : '40%', + render: (txt: string, rec: any, idx: number) => { + const playerName = (rec.outPlayer && `${rec.outPlayer.short}`) || ''; + return ( + + + {playerName} + + + ); + }, + }, + { + title: t('team.transferIn'), + key: 'inPlayer', + dataIndex: 'inPlayer', + width: showWeek ? '30%' : '40%', + render: (txt: string, rec: any, idx: number) => { + const playerName = (rec.inPlayer && `${rec.inPlayer.short}`) || ''; + return ( + + + {playerName} + + + ); + }, + }, + ]; + + if(showWeek) { + columns.push({ + key: 'weeId', + title: t('general.footballWeek'), + width: '20%', + dataIndex: 'weekId', + render: (text: string, team: any) => { + return {text}; + } + }) + }; + + return ( + + size ? { pageSize: size } : false} + rowKey={(rec: any, idx: number) => `record-${idx+1}`} + rowClassName={(rec: object, idx: number) => idx%2 ? 'ant-table-row--odd' : 'ant-table-row--even'} + /> + + ); +} \ No newline at end of file diff --git a/src/lib/helpers.tsx b/src/lib/helpers.tsx index ac778ea..5896aa0 100644 --- a/src/lib/helpers.tsx +++ b/src/lib/helpers.tsx @@ -71,6 +71,37 @@ export const getPlayerPositionHexColor = (player: any, theme: any) => { } }; + +export const selectionPlayerSellValue = (player: any) => { + const current = Object.assign({}, player); + const currentValue = current.value; + const selectionValue = player.selection.value; + const diff = currentValue - selectionValue; + let playerSellValue = null; + + if(selectionValue >= currentValue) { + playerSellValue = currentValue; + } else { + // const profit = roundDownDecimal() + const profit = diff / 2; + playerSellValue = selectionValue + profit; + } + return parseFloat((playerSellValue).toFixed(2)); +} + +export const roundNextHalf = (number: number) => { + const integerPart = Math.floor(number); + const decimalPart = number - integerPart; + + if (decimalPart === 0) { + return integerPart; + } else if (decimalPart <= 0.5) { + return integerPart + 0.5; + } else { + return integerPart + 1; + } +} + export const firstLetterUppercased = (string: string) => { return string.charAt(0).toUpperCase(); }; diff --git a/src/pages/Team/Team.tsx b/src/pages/Team/Team.tsx index 9824fb8..a38d3b9 100644 --- a/src/pages/Team/Team.tsx +++ b/src/pages/Team/Team.tsx @@ -51,16 +51,16 @@ export const _Team = (props: AbstractTeamType) => { } const playerProps = ["id", "name", "short", "positionId", "clubId", "value", "ban", "injury", "form", "forename", "surname", "points", "portraitUrl", "externalId"]; const selectionProps: any[] = []; - const starting = teamResult.players.filter((p: Player) => p.selection?.starting === 1) - .map((p: Player) => { + const starting = teamResult.players.filter((p: any) => p.selection.starting === 1) + .map((p: any) => { const displayWeekMatches: any[] = matches.filter( (match: Match) => match.weekId === weekId && ([match.homeId, match.awayId].includes(p.clubId)) ); return Object.assign({ inStarting: true, upcomingMatches: displayWeekMatches }, pick(p, playerProps), pick(p.selection, selectionProps)); }); - const bench = teamResult.players.filter((p: Player) => p.selection?.starting === 0) - .map((p: Player) => { + const bench = teamResult.players.filter((p: any) => p.selection.starting === 0) + .map((p: any) => { const displayWeekMatches: any[] = matches.filter( (match: Match) => match.weekId === weekId && ([match.homeId, match.awayId].includes(p.clubId)) ); @@ -68,10 +68,10 @@ export const _Team = (props: AbstractTeamType) => { }); const teamName = teamResult.team?.name; - const captainPlayer = teamResult.players.find((p: Player) => p && p.selection && p.selection.captain === 1); + const captainPlayer = teamResult.players.find((p: any) => p && p.selection && p.selection.captain === 1); const captainId = captainPlayer && captainPlayer.id; - const viceCaptainPlayer = teamResult.players.find((p: Player) => p && p.selection && p.selection.captain === 2); + const viceCaptainPlayer = teamResult.players.find((p: any) => p && p.selection && p.selection.captain === 2); const viceCaptainId = viceCaptainPlayer && viceCaptainPlayer.id; const budget = teamResult.players.reduce((acc: any, player: Player) => acc - player.value, application.competition.budget); @@ -79,7 +79,14 @@ export const _Team = (props: AbstractTeamType) => { const isTeamOwner = !!(teamResult.team?.userId === user?.id); - props.initTeamState(starting, bench, teamName, budget, captainId, viceCaptainId, true); + const boosters = { + freeHit: teamResult.team.freeHit, + bank: teamResult.team.bank, + tripleCaptain: teamResult.team.tripleCaptain, + wildCard: teamResult.team.wildCard + }; + + props.initTeamState(starting, bench, teamName, captainId, budget, undefined, undefined, undefined, [], [], [], viceCaptainId, boosters); }; const startingByPositions = useMemo(() => startingListToPositionsList(props.starting, application.competition.lineupPositionRows), [props.starting]); diff --git a/src/pages/Transfers/Transfers.tsx b/src/pages/Transfers/Transfers.tsx new file mode 100644 index 0000000..02d9e7a --- /dev/null +++ b/src/pages/Transfers/Transfers.tsx @@ -0,0 +1,364 @@ +import { AbstractTeam } from "@/components/AbstractTeam/AbstractTeam" +import { Block } from "@/components/Block/Block"; +import { Team } from "@/components/Team/Team"; +import { Col, Row } from "@/components/UI/Grid/Grid"; +import { roundNextHalf, selectionPlayerSellValue, startingListToPositionsList } from "@/lib/helpers"; +import { useAppSelector } from "@/reducers"; +import { useGetTeamQuery } from "@/services/teamsApi"; +import { useGetDeadlineInfoQuery } from "@/services/weeksApi"; +import Title from "antd/es/typography/Title"; +import { pick } from "lodash"; +import React, { useMemo } from "react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { Navigate, useParams } from "react-router-dom"; + +import teamBackground from "./../../assets/img/fpl-pitch-no-boarding.svg"; +import { PlayerType } from "@/types/PlayerTypes"; +import { theme } from "@/styles/theme"; +import { useGetClubsQuery } from "@/services/clubsApi"; +import { TransfersList } from "@/components/TransfersList/TransfersList"; +import { Button } from "@/components/UI/Button/Button"; +import Icon from "@ant-design/icons/lib/components/Icon"; +import { CloseCircleFilled, SaveFilled } from "@ant-design/icons"; +import { Element } from "react-scroll"; +import { PlayerList } from "@/components/PlayerList/PlayerList"; +import { useGetMatchesQuery } from "@/services/matchesApi"; +import { useGetPlayersQuery } from "@/services/playersApi"; +import { notification } from "antd"; +import { TransfersOverview } from "@/components/Stats/TransfersOverview"; +import { ConfirmModal } from "@/components/ConfirmModal/ConfirmModal"; + +type TransfersProps = { + +}; + +type TransfersState = { + notFound: boolean + performingTransfer: boolean + performingTransferReset: boolean + transferConfirmModalVisible: boolean +}; + +const _Transfers = (props: AbstractTeamType) => { + const [state, setState] = useState({ + notFound: false, + performingTransfer: false, + performingTransferReset: false, + transferConfirmModalVisible: false, + }); + const application = useSelector((state: StoreState.All) => state.application); + const { user, teams } = useAppSelector((state) => state.userState); + + const { id } = useParams(); + const { t } = useTranslation(); + + const { data: teamResult, isLoading: teamLoading, isError: teamError, isSuccess: teamSuccess } = useGetTeamQuery(+(id || 0)); + const { data: deadlineInfo, isSuccess: deadlineInfoSuccess, isLoading: deadlineInfoLoading, isError: deadlineInfoError } = useGetDeadlineInfoQuery(); + const { data: clubs, isLoading: clubsLoading, isError: clubsError, isSuccess: clubsSucces } = useGetClubsQuery(); + const { data: matches, isLoading: matchesLoading, isError: matchesError, isSuccess: matchesSuccess } = useGetMatchesQuery(); + const { data: players, isLoading: playersLoading, isError: playersError, isSuccess: playersSuccess } = useGetPlayersQuery(); + + useEffect(() => { + if (teamSuccess) { + const playerProps = + ['id', 'name', 'short', 'positionId', 'clubId', 'value', 'ban', 'injury', 'form', 'forename', 'surname', 'portraitUrl']; + const selectionProps: any[] = []; + + const starting = teamResult.players + .filter((player: any) => player.selection.starting === 1) + .map((player: any) => { + const tfValue = selectionPlayerSellValue(player); + return Object.assign({ inStarting: true }, pick(player, playerProps), pick(player.selection, selectionProps), { value: tfValue }); + }); + const bench = teamResult.players + .filter((player: any) => player.selection.starting === 0) + .map((player: any) => { + const tfValue = selectionPlayerSellValue(player); + return Object.assign({ inStarting: false }, pick(player, playerProps), pick(player.selection, selectionProps), { value: tfValue }); + }); + + const teamName = teamResult.team.name; + + const captainPlayer = teamResult.players.find((player: any) => player.selection.captain === 1); + const captainId = captainPlayer && captainPlayer.id; + const viceCaptainPlayer = teamResult.players.find((player: any) => player.selection.captain === 2); + const viceCaptainId = (viceCaptainPlayer && viceCaptainPlayer.id) || null; + + const pastTransfers = !teamResult.transfers ? ([] as Transfer[]) : + teamResult.transfers + .filter((tf: any) => deadlineInfo.deadlineInfo.deadlineWeek && (tf.weekId < deadlineInfo.deadlineInfo.deadlineWeek)) + .map((tf: any) => ({ inId: tf.inId, outId: tf.outId, weekId: tf.weekId })); + const deadlineWeekTransfers = !teamResult ? ([] as Transfer[]) : + teamResult.transfers + .filter((tf: any) => deadlineInfo.deadlineInfo.deadlineWeek && (tf.weekId === deadlineInfo.deadlineInfo.deadlineWeek)) + .map((tf: any) => ({ inId: tf.inId, outId: tf.outId, weekId: tf.weekId, extra: tf.extra })); + + const getPlayersValueWithTransfers = (players: any) => { + const playersValue = players + .reduce((acc: any, player: any) => { + const wasTfed = teamResult.transfers.find((tf: any) => tf.inId === player.id); + const playerValue = wasTfed ? + roundNextHalf(player.value + (player.value * (application.competition.transferTaxPercentage || 0) / 100)) : + player.value; + return acc + playerValue; + }, 0); + return application.competition.budget - playersValue; + }; + + const budget = teamResult.team.budget !== null ? + teamResult.team.budget : getPlayersValueWithTransfers(teamResult.players); + + + const boosters = { + freeHit: teamResult.team.freeHit, + bank: teamResult.team.bank, + tripleCaptain: teamResult.team.tripleCaptain, + wildCard: teamResult.team.wildCard + }; + + props.initTeamState(starting, bench, teamName, captainId, budget, undefined, undefined, undefined, teamResult.transfers, deadlineWeekTransfers, pastTransfers, viceCaptainId, boosters); + } + if (teamError) { + console.error(teamError); + setState({ ...state, notFound: true }); + } + }, [teamResult]); + + const formatTransfers = (tf: any) => { + const inPIDmeta = tf && (tf.inId && Object.keys(tf.inId).length); + const outPIDmeta = tf && (tf.outId && Object.keys(tf.outId).length); + + const inPlayer = players && players.find((player: any) => player.id === tf.inId); + const outPlayer = players && players.find((player: any) => player.id === tf.outId); + return { + inPlayer: inPIDmeta ? tf.inId : inPlayer, + outPlayer: outPIDmeta ? tf.outId : outPlayer, + weekId: tf && tf.weekId, + inId: inPIDmeta ? tf.inId.id : tf.inId, + outId: outPIDmeta ? tf.outId.id : tf.outId, + }; + }; + + const showTransferConfirmModal = () => { + setState({ ...state, transferConfirmModalVisible: true }); + }; + + const onTransferConfirmCancel = () => { + setState({ ...state, transferConfirmModalVisible: false }); + }; + + const onTransferConfirmAccept = () => { + console.log("TRANSFERS ACCEPTED"); + props.onTransfersSubmit(+(id || 0)); + setState({ ...state, transferConfirmModalVisible: false }); + } + + const isNotExtraTransfer = (transfer: any) => { + return !transfer.extra; + }; + + const playerIsExtra = (player: Player) => { + + } + + const onPlayerOut = (player: Player, isLineup: boolean, extraOutTransfer: boolean, clubsWithExtraPlayers: any[]) => { + const clubsWithExtraPlayersIds = clubsWithExtraPlayers.map((club: any) => club.id); + if (extraOutTransfer && !clubsWithExtraPlayersIds.includes(player.clubId)) { + const clubNames = clubsWithExtraPlayers.map((club => club.name)).join(','); + notification.warning({ message: `Je team is momenteel ongeldig. Je kan enkel een speler van ${clubNames} transfereren.` }) + return; + } + if (isLineup) { + props.removeStartingPlayer(player); + } else { + props.removeBenchPlayer(player); + } + + const playerExtra = playerIsExtra(player); + props.onTransferPlayerOut(player, false); + } + + const onPlayerIn = (player: Player) => { + props.pickPlayer(player, false); + props.onTransferPlayerIn(player); + } + + const { + starting, bench, boosters, initializedExternally, captainId, + viceCaptainId, deadlineWeekTransfers, draftTransfers, activePositionFilter, + pastTransfers, budget + } = props; + + const pastTransfersFormatted = pastTransfers + .map(formatTransfers); + const deadlineWeekTransfersFormatted = deadlineWeekTransfers + .concat(draftTransfers) + .map(formatTransfers); + const canSaveDraftTransfers = draftTransfers + .filter(draftTf => !!draftTf.inId && !!draftTf.outId) + .length === draftTransfers.length; + const deadlineWeekTransfersFormattedWithoutExtra = deadlineWeekTransfers + .concat(draftTransfers) + .filter(isNotExtraTransfer) + .map(formatTransfers); + + const team = useMemo(() => teamResult && teamResult.team, [teamResult]); + const notTeamOwner = useMemo(() => team && team.userId && user && (team.userId !== user.id), [team, user]); + const gameStarted = useMemo(() => deadlineInfo && deadlineInfo.deadlineInfo && deadlineInfo.deadlineInfo.deadlineWeek && deadlineInfo.deadlineInfo.deadlineWeek > application.competition.officialStartWeek, [deadlineInfo]); + const deadlineWeek = useMemo(() => deadlineInfo && deadlineInfo.deadlineInfo && deadlineInfo.deadlineInfo.deadlineWeek, [deadlineInfo]) + const enabledWildOrFreeHit = useMemo(() => boosters.wildCard === deadlineWeek || boosters.freeHit === deadlineWeek, [boosters]); + const startingByPositions = useMemo(() => startingListToPositionsList([].concat(starting as any, bench as any), [2, 5, 5, 3]), [starting, bench]); + const remainingTransfers = useMemo(() => { + let remainingTransfers = null; + if (application.competition.weeklyTransfers) { + remainingTransfers = application.competition.transfersAllowed - deadlineWeekTransfersFormattedWithoutExtra.length; + } else if (application.competition.transferCanBeSaved) { + remainingTransfers = application.competition.transfersAllowed * (deadlineWeek - 1) - (deadlineWeekTransfersFormattedWithoutExtra.length + pastTransfersFormatted.length); + } else { + remainingTransfers = application.competition.transfersAllowed - (deadlineWeekTransfersFormattedWithoutExtra.length + pastTransfersFormatted.length); + } + return remainingTransfers; + }, [application, deadlineWeekTransfersFormattedWithoutExtra, pastTransfersFormatted, deadlineWeek]); + const canTransferOut = useMemo(() => remainingTransfers > 0, [remainingTransfers]); + + const startingPicked = useMemo(() => starting.filter(player => player && player.id), [starting]); + const benchPicked = useMemo(() => bench.filter(player => player && player.id), [bench]); + + return ( + (clubs && teamResult && matches && players && deadlineInfo) && ( + + {(notTeamOwner || state.notFound) && + + } + {(team && deadlineInfo.deadlineInfo.deadlineWeek && (!gameStarted || enabledWildOrFreeHit)) && + + } + { + (initializedExternally && + + + + {t('transfersPage.transfersBlockTitle')} + +
+ {`${t('general.footballWeek')} ${deadlineWeek}`} +
+ +
+ { + (draftTransfers && draftTransfers.length && canSaveDraftTransfers && team && + + + + ) || null + } + { + (draftTransfers && draftTransfers.length && + + + ) || null + } +
+
+
+ +
+
+ + {t('general.footballLineup')} + props.onTransferPlayerOut(player))} + onPlaceholderClick={null} + actionLessPlayerIds={null} + playerPointsColor={"#000"} + playerPointsBgColor="#84FF00" + assetsCdn={""} + /> + + + + + {t('general.footballAllPlayers')} + + props.isPickAble(player, false, true)} + playerType={PlayerType.SoccerShirt} + actionLabel={t('transferPage.transferButtonLabel')} + data={players} + playerTax={application.competition.transferTaxPercentage} + onPick={onPlayerIn} + action + showHeader={false} + size={10} + /> + + + +
+ ) || null + } + onTransferConfirmCancel()} + onConfirm={(e: any) => onTransferConfirmAccept()} + title={t('transfersPage.transferConfirmTitle')} + text={t('transfersPage.transferConfirmMessage')} + /> +
+ ) + ) + +}; + +export const TransfersPage = () => AbstractTeam(_Transfers, {}); \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index 3960d92..f5c0c6e 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -20,6 +20,7 @@ import { Profile } from "./pages/Profile/Profile"; import { Deadlines } from "./pages/Deadlines/Deadlines"; import { Stats } from "./pages/Stats/Stats"; import { Footer } from "./components/Footer/Footer"; +import { TransfersPage } from "./pages/Transfers/Transfers"; const Layout = ({children}: any) => { return ( @@ -62,7 +63,7 @@ export const router = createBrowserRouter([ }, { path: "/transfers/:id", //todo - element: + element: }, { path: "/edit/:id", //todo diff --git a/src/services/teamsApi.ts b/src/services/teamsApi.ts index cb2d7e0..d9a60ca 100644 --- a/src/services/teamsApi.ts +++ b/src/services/teamsApi.ts @@ -5,17 +5,17 @@ import { url } from "inspector"; export const teamsApi = createApi({ reducerPath: "teamsApi", - tagTypes: ["userTeams"], + tagTypes: ["userTeam"], baseQuery: fetchBaseQuery({ baseUrl: `${config.API_URL}/teams`, credentials: "include" }), endpoints: (builder) => ({ - getTeam: builder.query<{team:Team, players:Player[]}, number>({ + getTeam: builder.query<{ team: Team, players: Player[], transfers: Transfer[] }, number>({ query: (teamId) => `${teamId}`, - providesTags: ["userTeams"], + providesTags: ["userTeam"], }), - addTeam: builder.mutation<{team:Team}, object>({ - invalidatesTags: ["userTeams"], + addTeam: builder.mutation<{ team: Team }, object>({ + invalidatesTags: ["userTeam"], query: (data) => ({ url: "add", method: "POST", @@ -23,15 +23,24 @@ export const teamsApi = createApi({ }), }), - updateTeamSelection: builder.mutation<{msg: string}, {teamId:number, bench: number[], starting: number[], teamName: string}>({ - invalidatesTags: ["userTeams"], - query: ({teamId, ...data}) => ({ + updateTeamSelection: builder.mutation<{ msg: string }, { teamId: number, bench: number[], starting: number[], teamName: string }>({ + invalidatesTags: ["userTeam"], + query: ({ teamId, ...data }) => ({ url: `${teamId}/selections`, method: "POST", body: data }), }), + submitTransfers: builder.mutation<{ msg: string, team: Team }, { teamId: number, transfers: Transfer[] }>({ + invalidatesTags: ["userTeam"], + query: ({ teamId, ...data }) => ({ + url: `${teamId}/transfers`, + method: "POST", + body: data, + }) + }), + // updatePlayer: builder.mutation & Pick>({ // query: ({ id, ...put }) => ({ // url: `${id}`, @@ -43,4 +52,4 @@ export const teamsApi = createApi({ }) }); -export const { useGetTeamQuery, useLazyGetTeamQuery, useAddTeamMutation, useUpdateTeamSelectionMutation } = teamsApi; \ No newline at end of file +export const { useGetTeamQuery, useLazyGetTeamQuery, useAddTeamMutation, useUpdateTeamSelectionMutation, useSubmitTransfersMutation } = teamsApi; \ No newline at end of file diff --git a/src/services/usersApi.ts b/src/services/usersApi.ts index 590c5f2..9b66e45 100644 --- a/src/services/usersApi.ts +++ b/src/services/usersApi.ts @@ -4,7 +4,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; export const usersApi = createApi({ reducerPath: "usersApi", - tagTypes: ["userTeams"], + tagTypes: ["userTeam"], baseQuery: fetchBaseQuery({ baseUrl: `${config.API_URL}/user/`, credentials: "include" }), endpoints: (builder) => ({ getProfile: builder.query({ @@ -19,7 +19,7 @@ export const usersApi = createApi({ getTeams: builder.query<{ teams: Team[], user: User }, void>({ query: () => "teams", - providesTags: ["userTeams"], + providesTags: ["userTeam"], async onQueryStarted(args, { dispatch, queryFulfilled }) { try { const { data } = await queryFulfilled; diff --git a/src/types/component_types.tsx b/src/types/component_types.tsx index 866b44e..60618ba 100644 --- a/src/types/component_types.tsx +++ b/src/types/component_types.tsx @@ -5,7 +5,7 @@ declare type AbstractTeamType = { setTeamName: (name: string) => void, setCaptainId: (captainId: number) => void, setActivePositionFilter: (positionId: number) => void, - isPickAble: (player: Player) => any, + isPickAble: (player: Player, taxForPicking?: boolean, isTransferPick?: boolean) => any, isSwapAble: (player: Player) => any, onTeamSave: () => Promise, onTeamReset: (team: any) => Promise, @@ -15,7 +15,7 @@ declare type AbstractTeamType = { removeBenchPlayer: (player: Player) => void, removeStartingPlayer: (player: Player) => void, removePlayer: (player: Player) => void, - pickPlayer: (player: Player) => void, + pickPlayer: (player: Player, taxForPicking?: boolean) => void, activePositionFilter?: any, starting?: any[], bench?: any[], @@ -26,7 +26,7 @@ declare type AbstractTeamType = { swapPlayerId?: any, swappedFrom?: string | null, teamNameChanged?: boolean, - initTeamState: (starting: any[], bench: any[], teamName: string, budget: number, captainId?: number, viceCaptainId?: number, teamUser?: any) => void, + initTeamState: (starting: any[], bench: any[], teamName: string, captainId: number, budget: number, leagues?: any[] | undefined, visibleWeekId?: number | undefined, teamPointsInfo?: any, rawTransfers?: any[] | undefined, deadlineWeekTransfers?: any[] | undefined, pastTransfers?: any[] | undefined, viceCaptainId?: number, boosters?: Boosters, isTeamOwner?: boolean, teamUser?: any) => void, resetTeamName: () => void, onTeamNameUpdate: (teamId: number) => void, onTeamEdit: (team: any) => void, @@ -41,5 +41,10 @@ declare type AbstractTeamType = { reloadUserTeams: () => void, teamUser?: any, savingTeamPending?: any, - // visibleWeekId: number | null, + visibleWeekId: number | null, + boosters: Boosters, + initializedExternally: boolean, + deadlineWeekTransfers: Transfer[] | null, + draftTransfers: Transfer[] | null, + pastTransfers: Transfer[] | null, } \ No newline at end of file diff --git a/src/types/index.tsx b/src/types/index.tsx index a9f8019..bbfe323 100644 --- a/src/types/index.tsx +++ b/src/types/index.tsx @@ -47,7 +47,7 @@ type Player = { pointsOverview: boolean stats?: Statistic[] - selection?: PlayerSelection + selections?: PlayerSelection[] upcomingMatches: Match[] } @@ -97,6 +97,18 @@ type Team = { name: string userId: number weekId: number + budget: number + freeHit: number + bank: number + tripleCaptain: number + wildCard: number +} + +type Boosters = { + freeHit?: number + bank?: number + tripleCaptain?: number + wildCard?: number } type User = { @@ -180,19 +192,19 @@ type MatchEvent = { } -// type Transfer = { -// id?: number -// datetime?: string -// weekId?: number -// inValue?: number -// outValue?: number -// inPlayer?: Player -// outPlayer?: Player -// extra?: boolean -// teamId?: number -// inId?: number | null -// outId?: number | null -// } +type Transfer = { + id?: number + datetime?: string + weekId?: number + inValue?: number + outValue?: number + inPlayer?: Player + outPlayer?: Player + extra?: boolean + teamId?: number + inId?: number | null + outId?: number | null +} // type MatchState = { // id: number