mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
selection of club and players passes to form data on submit
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import Select from "react-select";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import TeamSelection from "@/app/[locale]/(main)/match/components/TeamSelection";
|
||||||
|
import { matchSchema } from "@/schemas";
|
||||||
|
import { Club } from "@/types";
|
||||||
|
|
||||||
|
type MatchFormValues = z.infer<typeof matchSchema>;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
clubs: Club[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MatchForm = ({ clubs }: IProps) => {
|
||||||
|
const form = useForm<MatchFormValues>({
|
||||||
|
// resolver: zodResolver(addTournamentSchema),
|
||||||
|
// defaultValues: {
|
||||||
|
// tournament: {
|
||||||
|
// norm_tournament: false,
|
||||||
|
// coordinates: [47.0844, 2.3964],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: MatchFormValues) => {
|
||||||
|
try {
|
||||||
|
console.log(data);
|
||||||
|
// await addTournament(data);
|
||||||
|
// setResponseMessage({
|
||||||
|
// type: "success",
|
||||||
|
// message: t("success"),
|
||||||
|
// });
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.log(err);
|
||||||
|
//
|
||||||
|
// setResponseMessage({
|
||||||
|
// type: "error",
|
||||||
|
// message: t("failure"),
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clubOptions = clubs.map((club) => ({
|
||||||
|
value: club.name,
|
||||||
|
label: club.name,
|
||||||
|
url: club.url,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="w-3/4 space-y-8">
|
||||||
|
<div className="grid grid-cols-4 items-start gap-6">
|
||||||
|
<TeamSelection
|
||||||
|
clubOptions={clubOptions}
|
||||||
|
name="team1"
|
||||||
|
label="Team 1"
|
||||||
|
/>
|
||||||
|
<TeamSelection
|
||||||
|
clubOptions={clubOptions}
|
||||||
|
name="team2"
|
||||||
|
label="Team 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MatchForm;
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
useFieldArray,
|
||||||
|
useFormContext,
|
||||||
|
useWatch,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { FaMinus, FaPlus } from "react-icons/fa";
|
||||||
|
import Select from "react-select";
|
||||||
|
|
||||||
|
import { clubPlayersAtom } from "@/atoms";
|
||||||
|
import { fetchClubPlayers } from "@/server/fetchClubPlayers";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
clubOptions: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamSelection = ({ name, label, clubOptions }: IProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const [teamsPlayers, setTeamsPlayers] = useAtom(clubPlayersAtom);
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `${name}_players`, // e.g., "team1_players"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDbId = useWatch({
|
||||||
|
control,
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption = clubOptions.find((c) => c.value === selectedDbId);
|
||||||
|
const selectedId = selectedOption?.url.split("=").pop();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPlayers = async () => {
|
||||||
|
if (!selectedId) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchClubPlayers({ clubId: selectedId });
|
||||||
|
const data = response?.data;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setTeamsPlayers((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPlayers();
|
||||||
|
}, [selectedId, name, setTeamsPlayers]);
|
||||||
|
|
||||||
|
const playerOptions =
|
||||||
|
teamsPlayers[name]?.map((player) => ({
|
||||||
|
value: {
|
||||||
|
nrFFE: player.nrFFE,
|
||||||
|
name: player.name,
|
||||||
|
elo: player.elo,
|
||||||
|
},
|
||||||
|
label: `${player.name} (${player.elo})`,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-span-2 space-y-4">
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Team/Club Select */}
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
instanceId={`${name}-select`}
|
||||||
|
options={clubOptions}
|
||||||
|
isSearchable
|
||||||
|
placeholder="Select a club..."
|
||||||
|
onChange={(option) => {
|
||||||
|
field.onChange(option?.value);
|
||||||
|
// Optional: Clear players if club changes, e.g. remove(0, fields.length)
|
||||||
|
}}
|
||||||
|
value={clubOptions.find((c) => c.value === field.value)}
|
||||||
|
classNames={{
|
||||||
|
control: () => "border-gray-300 rounded-md shadow-sm",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dynamic Player List */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
name={`${name}_players.${index}.playerId`} // Saves as { playerId: "nrFFE_123" }
|
||||||
|
control={control}
|
||||||
|
render={({ field: selectField }) => (
|
||||||
|
<Select
|
||||||
|
{...selectField}
|
||||||
|
instanceId={`${name}-player-${index}`}
|
||||||
|
options={playerOptions}
|
||||||
|
isSearchable
|
||||||
|
placeholder="Search player..."
|
||||||
|
onChange={(option) => selectField.onChange(option?.value)}
|
||||||
|
// Compare values using strict equality since value is now a string (nrFFE)
|
||||||
|
value={playerOptions.find(
|
||||||
|
(op) => op.value === selectField.value,
|
||||||
|
)}
|
||||||
|
classNames={{
|
||||||
|
control: () => "border-gray-300 rounded-md shadow-sm",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
className="p-2 text-red-500 hover:text-red-700"
|
||||||
|
title="Remove player"
|
||||||
|
>
|
||||||
|
<FaMinus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Button */}
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-sm text-gray-500">Loading players...</p>
|
||||||
|
) : (
|
||||||
|
teamsPlayers[name]?.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button" // Important to prevent form submission
|
||||||
|
onClick={() => append({ playerId: "" })} // Add new empty row to field array
|
||||||
|
className="flex items-center gap-2 text-sm font-medium text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
Add Player
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamSelection;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { unstable_cache } from "next/cache";
|
||||||
|
|
||||||
|
import MatchForm from "@/app/[locale]/(main)/match/MatchForm";
|
||||||
|
import { collections, dbConnect } from "@/server/mongodb";
|
||||||
|
import { Club } from "@/types";
|
||||||
|
import { filterClubsByManualEntry } from "@/utils/clubFilters";
|
||||||
|
import { errorLog } from "@/utils/logger";
|
||||||
|
|
||||||
|
const getClubs = async () => {
|
||||||
|
try {
|
||||||
|
await dbConnect();
|
||||||
|
const data = await collections
|
||||||
|
.clubs!.find({ pending: { $ne: true } })
|
||||||
|
.sort({ name: 1 })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return filterClubsByManualEntry(data)
|
||||||
|
.filter((c) => !!c.coordinates)
|
||||||
|
.map<Club>((club) => {
|
||||||
|
return {
|
||||||
|
id: club._id.toString(),
|
||||||
|
name: club.name,
|
||||||
|
url: club.url,
|
||||||
|
address: club.address,
|
||||||
|
email: club.email,
|
||||||
|
website: club.website,
|
||||||
|
latLng: { lat: club.coordinates[0], lng: club.coordinates[1] },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
errorLog(error);
|
||||||
|
throw new Error("Error fetching club data");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Match() {
|
||||||
|
const clubs = await unstable_cache(
|
||||||
|
async () => {
|
||||||
|
const data = await getClubs();
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
["clubs"],
|
||||||
|
{
|
||||||
|
revalidate: 60 * 60 * 6,
|
||||||
|
},
|
||||||
|
)();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="grid place-items-center bg-white pb-20 dark:bg-gray-800">
|
||||||
|
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||||
|
<h2 className="mb-4 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">
|
||||||
|
Match
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
|
||||||
|
Match
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MatchForm clubs={clubs} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { LatLngBounds } from "leaflet";
|
import { LatLngBounds } from "leaflet";
|
||||||
|
|
||||||
|
import { Player } from "@/interfaces";
|
||||||
import { Club, TimeControl, Tournament } from "@/types";
|
import { Club, TimeControl, Tournament } from "@/types";
|
||||||
import atomWithDebounce from "@/utils/atomWithDebounce";
|
import atomWithDebounce from "@/utils/atomWithDebounce";
|
||||||
import { normalizedContains } from "@/utils/string";
|
import { normalizedContains } from "@/utils/string";
|
||||||
@@ -228,3 +229,5 @@ export const dateRangeAtom = atom<any>([
|
|||||||
key: "selection",
|
key: "selection",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const clubPlayersAtom = atom<Record<string, Player[]>>({});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface Player {
|
||||||
|
nrFFE: string;
|
||||||
|
name: string;
|
||||||
|
elo: string;
|
||||||
|
elo_rapid: string;
|
||||||
|
elo_blitz: string;
|
||||||
|
category: string;
|
||||||
|
club: string;
|
||||||
|
}
|
||||||
+23
-2
@@ -20,8 +20,8 @@ export const addClubSchema = z.object({
|
|||||||
address: z.string().min(1, { message: "FormValidation.required" }),
|
address: z.string().min(1, { message: "FormValidation.required" }),
|
||||||
website: z.string().url({ message: "FormValidation.url" }).optional(),
|
website: z.string().url({ message: "FormValidation.url" }).optional(),
|
||||||
coordinates: z.array(z.number()).length(2),
|
coordinates: z.array(z.number()).length(2),
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const addTournamentSchema = z.object({
|
export const addTournamentSchema = z.object({
|
||||||
name: z.string().min(1, { message: "FormValidation.required" }),
|
name: z.string().min(1, { message: "FormValidation.required" }),
|
||||||
@@ -59,3 +59,24 @@ export const zoneSchema = z
|
|||||||
message: "FormValidation.zone",
|
message: "FormValidation.zone",
|
||||||
path: ["features"],
|
path: ["features"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// export const fetchClubPlayersSchema = z.array(
|
||||||
|
// z.object({
|
||||||
|
// nrFFE: z.string(),
|
||||||
|
// name: z.string(),
|
||||||
|
// elo: z.string(),
|
||||||
|
// elo_rapid: z.string(),
|
||||||
|
// elo_blitz: z.string(),
|
||||||
|
// category: z.string(),
|
||||||
|
// club: z.string(),
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
export const fetchClubPlayersSchema = z.object({
|
||||||
|
clubId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const matchSchema = z.object({
|
||||||
|
team1: z.string(),
|
||||||
|
team2: z.string(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export const addClub = actionClient
|
|||||||
try {
|
try {
|
||||||
const { name, email, message, club } = input.parsedInput;
|
const { name, email, message, club } = input.parsedInput;
|
||||||
|
|
||||||
console.log(club);
|
|
||||||
|
|
||||||
const clubData = {
|
const clubData = {
|
||||||
name: club.name ?? "",
|
name: club.name ?? "",
|
||||||
address: club.address ?? "",
|
address: club.address ?? "",
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { fetchClubPlayersSchema } from "@/schemas";
|
||||||
|
import { errorLog } from "@/utils/logger";
|
||||||
|
|
||||||
|
import { actionClient } from "./safeAction";
|
||||||
|
|
||||||
|
const resultsSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
nrFFE: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
elo: z.string(),
|
||||||
|
elo_rapid: z.string(),
|
||||||
|
elo_blitz: z.string(),
|
||||||
|
category: z.string(),
|
||||||
|
club: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
nrFFE: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
elo: z.string(),
|
||||||
|
elo_rapid: z.string(),
|
||||||
|
elo_blitz: z.string(),
|
||||||
|
category: z.string(),
|
||||||
|
club: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ClubPlayerResultsData = z.infer<typeof outputSchema>;
|
||||||
|
|
||||||
|
const reportFetchError = async (url: string, error: any) => {
|
||||||
|
await fetch(process.env.DISCORD_WEBHOOK_ERROR_LOGS_URL as string, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: "Error Fetching Club Player Details",
|
||||||
|
fields: [
|
||||||
|
{ name: "url", value: url },
|
||||||
|
{
|
||||||
|
name: "error.message",
|
||||||
|
value: "message" in error ? error.message : "",
|
||||||
|
},
|
||||||
|
{ name: "error", value: JSON.stringify(error, null, 2) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClubPlayers = actionClient
|
||||||
|
.schema(fetchClubPlayersSchema)
|
||||||
|
.action(async (input) => {
|
||||||
|
const { clubId } = input.parsedInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = new Headers();
|
||||||
|
const apiKey = process.env.RESULTS_API_KEY;
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers.append("api-key", apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawResults = await fetch(
|
||||||
|
`https://api.echecsfrance.com/api/v1/club_player_details/${clubId}`,
|
||||||
|
{
|
||||||
|
headers: headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rawResults.status !== 200) {
|
||||||
|
throw new Error("ERR_CLUB_NOT_FOUND");
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await rawResults.json();
|
||||||
|
|
||||||
|
const parsedResults = resultsSchema.parse(results);
|
||||||
|
|
||||||
|
return outputSchema.parse(parsedResults);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
!("message" in error) ||
|
||||||
|
error.message !== "ERR_TOURNAMENT_NOT_FOUND"
|
||||||
|
) {
|
||||||
|
// reportFetchError(id, error);
|
||||||
|
errorLog(JSON.stringify(error, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user