diff --git a/app/(public)/kamp/[id]/loading.tsx b/app/(public)/kamp/[id]/loading.tsx new file mode 100644 index 0000000..fd44ac0 --- /dev/null +++ b/app/(public)/kamp/[id]/loading.tsx @@ -0,0 +1,7 @@ + + +export default function Loading() { + return ( + "Laster kamp ..." + ) +} \ No newline at end of file diff --git a/app/(public)/kamp/[id]/page.tsx b/app/(public)/kamp/[id]/page.tsx index 19aa078..f606442 100644 --- a/app/(public)/kamp/[id]/page.tsx +++ b/app/(public)/kamp/[id]/page.tsx @@ -20,6 +20,9 @@ import { currentUser } from "@/lib/auth"; import { UserRole } from "@prisma/client"; import { EditMatchEventsModal } from "@/app/components/matches/EditMatchEventsModal"; import { getClubPlayers } from "@/data/getClubPlayers"; +import { Suspense } from "react"; +import { EditMatchSquadModal } from "@/app/components/matches/EditMatchSquadModal"; +import { EditMatchGoalsModal } from "@/app/components/matches/EditMatchGoalsModal"; type MatchPageProps = { params: { id: string }; @@ -43,9 +46,13 @@ const MatchPage = async ({ params: params }: MatchPageProps) => { const awayClubPlayers = await getClubPlayers(match.awayClubId); const canEditHome = - user && (user.role === UserRole.ADMIN || user.club === match.homeClubId); + user && + (user.role === UserRole.ADMIN || user.club === match.homeClubId) && + !match.isMatchEventsConfirmed; const canEditAway = - user && (user.role === UserRole.ADMIN || user.club === match.awayClubId); + user && + (user.role === UserRole.ADMIN || user.club === match.awayClubId) && + !match.isMatchEventsConfirmed; return ( @@ -57,26 +64,39 @@ const MatchPage = async ({ params: params }: MatchPageProps) => { !isFuture(match.kickoffTime) && homeSquad && awaySquad && ( -
+
+ {user && !match.isMatchEventsConfirmed && ( + + )} - {(canEditHome || canEditAway) && ( -
-
+
+ {canEditHome && ( +
<>{match.homeTeam.name} - Ny hendelse
-
+ )} + {canEditAway && ( +
<>{match.awayTeam.name} - Ny hendelse
-
- )} + )} +
)} diff --git a/app/api/match/[id]/result/route.ts b/app/api/match/[id]/result/route.ts new file mode 100644 index 0000000..6b2a999 --- /dev/null +++ b/app/api/match/[id]/result/route.ts @@ -0,0 +1,49 @@ +import { MATCHES_CACHE_TAG } from "@/data/getCompetitionMatchesWithResults"; +import { MATCH_EVENTS_CACHE_TAG } from "@/data/getExtendedMatchEvents"; +import { MATCH_SQUADS_CACHE_TAG } from "@/data/getExtendedMatchSquads"; +import { getUserById } from "@/data/user"; +import { currentUser } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { MatchGoalsSchema } from "@/schemas"; +import { UserRole } from "@prisma/client"; +import { revalidateTag } from "next/cache"; +import { NextResponse } from "next/server"; + +export const POST = async ( + request: Request, + { params }: { params: { id: string } }, +) => { + const { data } = await request.json(); + + const parsedMatchId = Number(params.id); + + const validatedFields = MatchGoalsSchema.safeParse(data); + + if (!validatedFields.success) { + return NextResponse.json({ error: "Bad request" }, { status: 400 }); + } + const user = await currentUser(); + + if (!user || !user.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const dbUser = await getUserById(user.id); + + if (!dbUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const match = await db.match.update({ + where: { + id: parsedMatchId, + }, + data: { ...validatedFields.data }, + }); + + revalidateTag(MATCHES_CACHE_TAG); + + return new Response(JSON.stringify({ message: "Kampresultat oppdatert" }), { + status: 200, + }); +}; diff --git a/app/api/squad/[id]/route.ts b/app/api/squad/[id]/route.ts index 4058bf3..875d764 100644 --- a/app/api/squad/[id]/route.ts +++ b/app/api/squad/[id]/route.ts @@ -40,7 +40,7 @@ export const POST = async ( return NextResponse.json({ error: "Bad request" }, { status: 400 }); } - if (dbUser.role !== UserRole.ADMIN && player.id !== dbUser.clubId) { + if (dbUser.role !== UserRole.ADMIN && player.clubId !== dbUser.clubId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/components/Alert.tsx b/app/components/Alert.tsx deleted file mode 100644 index c2a1cd8..0000000 --- a/app/components/Alert.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { Card, CardBody, CardHeader } from "@nextui-org/card"; -import { useState } from "react"; - -import { CgDanger } from "react-icons/cg"; -import { CgCheckO } from "react-icons/cg"; -import { CgInfo } from "react-icons/cg"; -import { IoWarningOutline } from "react-icons/io5"; -import { MdClose } from "react-icons/md"; - -export type AlertProps = { - variant: "success" | "info" | "danger" | "warning"; - children: JSX.Element; - title?: string; -}; - -type MapperProps = Record< - AlertProps["variant"], - { icon: JSX.Element; color: string } ->; -const AlertMapper: MapperProps = { - danger: { icon: , color: "red-400" }, - success: { icon: , color: "green-400" }, - info: { icon: , color: "blue-400" }, - warning: { icon: , color: "orange-400" }, -}; - -export const Alert = ({ variant, title, children }: AlertProps) => { - const [open, setOpen] = useState(true); - return ( -
-
- {AlertMapper[variant].icon} -
-
{children}
-
- { - setOpen(false); - }} - /> -
-
- ); -}; diff --git a/app/components/admin/NIFUpload.tsx b/app/components/admin/NIFUpload.tsx index 45624ef..ff596a1 100644 --- a/app/components/admin/NIFUpload.tsx +++ b/app/components/admin/NIFUpload.tsx @@ -17,7 +17,6 @@ import { NIFSchema } from "@/schemas"; import { updateNIFData } from "@/actions/update-NIF-data"; import { z } from "zod"; import { useRouter } from "next/navigation"; -import { Alert } from "@/app/components/Alert"; export const NIFUpload = () => { const router = useRouter(); @@ -116,23 +115,7 @@ export const NIFUpload = () => { onChange={handleFileUpload} disabled={isPending} /> - {error ? ( - - <>{error} - - ) : ( - success && ( - - <> - {rowsAdded} spillere ble lagt til, og {rowsUpdated} ble - oppdatert! - - - ) - )} + + + + +

Endre kampresultat

+
+ +
+ ( + + onChange( + e.target.value === "" ? null : Number(e.target.value), + ) + } + /> + )} + /> + ( + + onChange( + e.target.value === "" ? null : Number(e.target.value), + ) + } + /> + )} + /> +
+ + +
+ +
+
+
+ + ); +}; diff --git a/app/components/matches/EditMatchSquadModal.tsx b/app/components/matches/EditMatchSquadModal.tsx index 6c709db..f63084f 100644 --- a/app/components/matches/EditMatchSquadModal.tsx +++ b/app/components/matches/EditMatchSquadModal.tsx @@ -8,25 +8,19 @@ import { ModalBody, ModalContent, ModalHeader, - Select, - SelectItem, useDisclosure, } from "@nextui-org/react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState, useTransition } from "react"; -import axios, { AxiosError } from "axios"; +import axios from "axios"; import { ExtendedMatchSquad } from "@/data/getExtendedMatchSquads"; -import { useCurrentUser } from "@/hooks/use-current-user"; import { Controller, useForm } from "react-hook-form"; -import { NewMatchEventSchema, SquadSchema } from "@/schemas"; +import { SquadSchema } from "@/schemas"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { MatchEventType, Player } from "@prisma/client"; -import { matchEventTypesList } from "@/lib/enum-mappings"; -import { Alert } from "../Alert"; -import { groupPlayersByPosition } from "@/lib/utils"; +import { Player } from "@prisma/client"; type EditMatchSquadModalProps = { allClubPlayers: Player[]; @@ -40,8 +34,6 @@ export const EditMatchSquadModal = ({ }: EditMatchSquadModalProps) => { const router = useRouter(); - const user = useCurrentUser(); - const { isOpen, onOpen, onClose } = useDisclosure(); const [error, setError] = useState(); @@ -142,11 +134,6 @@ export const EditMatchSquadModal = ({ )} /> - {error && ( - - <>{error} - - )} diff --git a/app/components/matches/EventSection.tsx b/app/components/matches/EventSection.tsx index 56e6423..d9724ad 100644 --- a/app/components/matches/EventSection.tsx +++ b/app/components/matches/EventSection.tsx @@ -35,7 +35,7 @@ export const EventSection = ({ awayClubId, icon, }: EventSectionProps) => ( -
+
{events .filter((event) => event.squadPlayer.squad.clubId === homeClubId) @@ -46,8 +46,8 @@ export const EventSection = ({
))}
-
-
{icon}
+
+
{icon}
{events diff --git a/app/components/matches/Match.tsx b/app/components/matches/Match.tsx index cccee68..a35aae7 100644 --- a/app/components/matches/Match.tsx +++ b/app/components/matches/Match.tsx @@ -1,7 +1,7 @@ import { ExtendedMatch } from "@/data/getClubMatches"; import { competitionTypesMap } from "@/lib/enum-mappings"; import { formatDateVerbose } from "@/lib/utils"; -import { Card } from "@nextui-org/react"; +import { Card, Link } from "@nextui-org/react"; import { StatusMatch } from "./StatusMatch"; import { getYear } from "date-fns"; @@ -45,11 +45,10 @@ export const Match = ({
{match.homeTeam.name}
- - +
{match.awayTeam.name}
diff --git a/app/components/matches/MatchMetadata.tsx b/app/components/matches/MatchMetadata.tsx index a4221bc..e65e812 100644 --- a/app/components/matches/MatchMetadata.tsx +++ b/app/components/matches/MatchMetadata.tsx @@ -1,10 +1,12 @@ import { ExtendedMatch } from "@/data/getClubMatches"; import { competitionTypesMap, venuesMap } from "@/lib/enum-mappings"; import { formatDateVerbose, getCurrentSeason } from "@/lib/utils"; -import { getHours, getMinutes } from "date-fns"; +import { format, getHours, getMinutes } from "date-fns"; import { FaRegCalendarAlt } from "react-icons/fa"; import { MdStadium } from "react-icons/md"; +import { nb } from "date-fns/locale"; + type MatchMetadataProps = { match: ExtendedMatch; }; @@ -30,12 +32,9 @@ export const MatchMetadata = ({ match: match }: MatchMetadataProps) => {
{match.kickoffTime ? ( <> - {formatDateVerbose(match.kickoffTime)}{" "} - {getHours(new Date(match.kickoffTime))}: - {String(getMinutes(new Date(match.kickoffTime))).padStart( - 2, - "0", - )} + {format(new Date(match.kickoffTime), "dd. MMM yyyy", { + locale: nb, + })} ) : ( "KO ikke satt" diff --git a/app/components/matches/MatchSquads.tsx b/app/components/matches/MatchSquads.tsx index c5aeef5..0a76e4f 100644 --- a/app/components/matches/MatchSquads.tsx +++ b/app/components/matches/MatchSquads.tsx @@ -70,7 +70,7 @@ export const MatchSquads = async ({
{canEditAway && ( <>Endre bortetropp diff --git a/app/components/matches/StatusMatch.tsx b/app/components/matches/StatusMatch.tsx index 1a6cc42..15a4665 100644 --- a/app/components/matches/StatusMatch.tsx +++ b/app/components/matches/StatusMatch.tsx @@ -1,3 +1,5 @@ +"use client"; + import { ExtendedMatch } from "@/data/getClubMatches"; import { formatDateVerbose, @@ -6,21 +8,29 @@ import { } from "@/lib/utils"; import { MatchState } from "@/types/types"; import { getHours, getMinutes } from "date-fns"; +import { useRouter } from "next/navigation"; export type StatusMatchProps = { viewAsClubId?: number | null; match: ExtendedMatch; isBig?: boolean; }; + export const StatusMatch = ({ - viewAsClubId: viewAsClubId = null, - match: match, - isBig: isBig = false, + viewAsClubId = null, + match, + isBig = false, }: StatusMatchProps) => { + const router = useRouter(); const matchState = getExpectedMatchState(match.kickoffTime); + const handleOnClick = () => { + if (isBig) return; + router.push(`/kamp/${match.id}`); + }; + const getBgColor = () => { - if (!viewAsClubId) { + if (!viewAsClubId || match.homeGoals === null || match.awayGoals === null) { return; } @@ -46,69 +56,74 @@ export const StatusMatch = ({ return "bg-gray-300"; }; - if (matchState === MatchState.NOT_STARTED) { - return ( -
- {match.kickoffTime ? ( -
- {isBig &&
{formatDateVerbose(match.kickoffTime, true)}
} -
- {getHours(new Date(match.kickoffTime))}: - {String(getMinutes(new Date(match.kickoffTime))).padStart(2, "0")} + return ( +
+ {matchState === MatchState.NOT_STARTED && ( +
+ {match.kickoffTime ? ( +
+ {isBig &&
{formatDateVerbose(match.kickoffTime, true)}
} +
+ {getHours(new Date(match.kickoffTime))}: + {String(getMinutes(new Date(match.kickoffTime))).padStart( + 2, + "0", + )} +
+
+ ) : ( + "Ukjent" + )} +
+ )} + + {matchState === MatchState.ONGOING && ( +
+ {isBig && ( +
+ {match.homeGoals !== null ? match.homeGoals : "?"} -{" "} + {match.awayGoals !== null ? match.awayGoals : "?"}
+ )} +
+ {getMatchTime(match.kickoffTime!) !== "Pause" && ( +
+ )} +
{getMatchTime(match.kickoffTime!)}
- ) : ( - "Ukjent" - )} -
- ); - } else if (matchState == MatchState.ONGOING) { - return ( -
- {isBig && ( +
+ )} + + {matchState === MatchState.FINISHED && ( +
- {match.homeGoals !== null ? match.homeGoals : "?"} -{" "} - {match.awayGoals !== null ? match.awayGoals : "?"} + {match.homeGoals} - {match.awayGoals}
- )} -
- {getMatchTime(match.kickoffTime!) !== "Pause" && ( -
- )} -
{getMatchTime(match.kickoffTime!)}
+ {isBig &&
Ferdig
}
-
- ); - } else if ( - matchState === MatchState.FINISHED && - match.homeGoals !== null && - match.awayGoals !== null - ) { - return ( -
-
- {match.homeGoals} - {match.awayGoals} -
- {isBig &&
Ferdig
} -
- ); - } else { - return
Ukjent
; - } + )} + + {[ + MatchState.NOT_STARTED, + MatchState.ONGOING, + MatchState.FINISHED, + ].indexOf(matchState) === -1 &&
Ukjent
} +
+ ); }; diff --git a/app/components/notifications/CreateNotificationModal.tsx b/app/components/notifications/CreateNotificationModal.tsx index a39e396..c0c7bed 100644 --- a/app/components/notifications/CreateNotificationModal.tsx +++ b/app/components/notifications/CreateNotificationModal.tsx @@ -27,7 +27,6 @@ import { useRouter } from "next/navigation"; import { useCallback, useState, useTransition } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; -import { Alert } from "../Alert"; type FormDataProps = { title: string; @@ -199,22 +198,7 @@ export const CreateNotificationModal = ({ }} /> )} - {error ? ( - - <>{error} - - ) : ( - success && ( - - <> - Beskjeden ble opprettet! - - - ) - )} + diff --git a/lib/enum-mappings.tsx b/lib/enum-mappings.tsx index a9aa265..54879cf 100644 --- a/lib/enum-mappings.tsx +++ b/lib/enum-mappings.tsx @@ -63,12 +63,18 @@ export const matchEventTypesMap = { >; export const matchEventTypesList = Object.entries(matchEventTypesMap).map( - ([type, { label, statLabel }]) => ({ + ([type, { label, icon, statLabel }]) => ({ type, label, + icon, statLabel, }), -) satisfies { type: string; label: string; statLabel: string }[]; +) satisfies { + type: string; + label: string; + icon: JSX.Element; + statLabel: string; +}[]; export const venuesMap = { [Venue.TEMPE_HOVEDBANE]: { label: "Tempe hovedbane" }, diff --git a/middleware.ts b/middleware.ts index 65dcc3a..2b71b24 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,8 +1,15 @@ - -export { auth as middleware } from "@/auth"; +import { auth } from "@/auth"; // TODO: Add middleware logic when Authjs 5 - Prisma bug is fixed +export default auth((req) => { + const { nextUrl } = req; + const isLoggedIn = !!req.auth; + + console.log(nextUrl); + console.log(isLoggedIn); +}); + export const config = { matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], }; diff --git a/package.json b/package.json index 4e311ba..954b743 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,22 @@ "docker:stop": "docker-compose stop" }, "dependencies": { - "@auth/prisma-adapter": "^1.4.0", + "@auth/prisma-adapter": "^1.5.2", "@hookform/resolvers": "^3.3.4", - "@nextui-org/react": "^2.2.9", - "@prisma/client": "^5.11.0", - "@tanstack/react-query": "^5.28.4", + "@nextui-org/react": "^2.2.10", + "@prisma/client": "^5.12.0", + "@tanstack/react-query": "^5.28.14", "@types/bcryptjs": "^2.4.6", - "axios": "^1.6.7", - "date-fns": "^3.3.1", - "framer-motion": "^10.16.4", + "axios": "^1.6.8", + "date-fns": "^3.6.0", + "framer-motion": "^11.0.24", "lodash.debounce": "^4.0.8", - "next": "14.0.1", - "next-auth": "^5.0.0-beta.5", + "next": "14.1.4", + "next-auth": "^5.0.0-beta.16", "papaparse": "^5.4.1", "react": "^18", "react-dom": "^18", - "react-hook-form": "^7.51.0", + "react-hook-form": "^7.51.2", "react-icons": "^5.0.1", "server-only": "^0.0.1", "string-similarity-js": "^2.1.4", @@ -51,13 +51,13 @@ "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.8", - "autoprefixer": "^10.4.16", + "autoprefixer": "^10.4.19", "bcryptjs": "^2.4.3", "eslint": "^8", - "eslint-config-next": "14.0.1", - "postcss": "^8.4.31", - "prisma": "^5.11.0", - "tailwindcss": "^3.3.5", + "eslint-config-next": "14.1.4", + "postcss": "^8.4.38", + "prisma": "^5.12.0", + "tailwindcss": "^3.4.3", "typescript": "^5" }, "prisma": { diff --git a/schemas/index.ts b/schemas/index.ts index 6778759..6f8489f 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -84,3 +84,8 @@ export const SquadSchema = z.object({ playerIds: z.array(z.string()), squadId: z.number(), }); + +export const MatchGoalsSchema = z.object({ + homeGoals: z.union([z.number().min(1), z.null()]), + awayGoals: z.union([z.number().min(1), z.null()]), +});