mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 12:36:57 +00:00
add /add-club route
remove commented out code dismiss success dialog - onClick was missing locale for add-club url prefer manual entry club if both auto and manual entry versions exist, only if pending = false change manual entry field to a boolean filter out clubs that are pending approval from manual entry add info message on success tournament form adds to DB and send discord message ping discord with club submission details i18n for add club update map to allow coordinate update depending on tournament/club page basic add club form with reusable map component
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslations } from "next-intl";
|
||||
import dynamic from "next/dynamic";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import InfoMessage, { InfoMessageType } from "@/components/InfoMessage";
|
||||
import LoadingMap from "@/components/LoadingMap";
|
||||
import { TextAreaField } from "@/components/form/TextAreaField";
|
||||
import { TextField } from "@/components/form/TextField";
|
||||
import { addClubSchema } from "@/schemas";
|
||||
import { addClub } from "@/server/addClub";
|
||||
import { errorLog } from "@/utils/logger";
|
||||
|
||||
type ClubFormValues = z.infer<typeof addClubSchema>;
|
||||
|
||||
const Map = dynamic(() => import("../components/MapCoordinateSelection"), {
|
||||
ssr: false,
|
||||
loading: LoadingMap,
|
||||
});
|
||||
|
||||
const ClubForm = () => {
|
||||
const t = useTranslations("AddClub");
|
||||
|
||||
const [responseMessage, setResponseMessage] = useState<{
|
||||
type: InfoMessageType;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
const form = useForm<ClubFormValues>({
|
||||
resolver: zodResolver(addClubSchema),
|
||||
defaultValues: {
|
||||
club: {
|
||||
coordinates: [47.0844, 2.3964],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ClubFormValues) => {
|
||||
try {
|
||||
await addClub(data);
|
||||
|
||||
setResponseMessage({
|
||||
type: "success",
|
||||
message: t("success"),
|
||||
});
|
||||
|
||||
form.reset();
|
||||
} catch (err: unknown) {
|
||||
errorLog(err);
|
||||
setResponseMessage({
|
||||
type: "error",
|
||||
message: t("failure"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="grid grid-cols-4 items-start gap-6">
|
||||
<div className="col-span-4">
|
||||
<TextField
|
||||
name="club.name"
|
||||
control={form.control}
|
||||
label={t("clubNameLabel")}
|
||||
placeholder={t("clubNamePlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<TextField
|
||||
name="club.address"
|
||||
control={form.control}
|
||||
label={"Address:"}
|
||||
placeholder={t("addressPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<TextField
|
||||
name="club.email"
|
||||
control={form.control}
|
||||
label={t("emailLabel")}
|
||||
placeholder={t("emailPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<TextField
|
||||
name="club.website"
|
||||
control={form.control}
|
||||
label={t("websiteLabel")}
|
||||
placeholder={t("websitePlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-1">
|
||||
<TextField
|
||||
name="name"
|
||||
control={form.control}
|
||||
label={t("yourNameLabel")}
|
||||
placeholder={t("yourNamePlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-1">
|
||||
<TextField
|
||||
name="email"
|
||||
control={form.control}
|
||||
label={t("yourEmailLabel")}
|
||||
placeholder={t("yourEmailPlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 row-span-2 self-stretch sm:col-span-2">
|
||||
<TextAreaField
|
||||
className="h-full"
|
||||
childrenWrapperClassName="h-full"
|
||||
name="message"
|
||||
control={form.control}
|
||||
label={t("messageLabel")}
|
||||
placeholder={t("messagePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 sm:col-span-1">
|
||||
<TextField
|
||||
name="club.coordinates.0"
|
||||
control={form.control}
|
||||
label={t("latLabel")}
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-1">
|
||||
<TextField
|
||||
name="club.coordinates.1"
|
||||
control={form.control}
|
||||
label={t("lngLabel")}
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section id="map" className="z-0 col-span-4 flex h-auto">
|
||||
<Map page="club" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => form.reset()} type="button" intent="secondary">
|
||||
{t("clearForm")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{responseMessage && (
|
||||
<InfoMessage
|
||||
message={responseMessage.message}
|
||||
type={responseMessage.type}
|
||||
onDismiss={() => setResponseMessage(null)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClubForm;
|
||||
@@ -0,0 +1,20 @@
|
||||
import ClubForm from "@/app/[locale]/(main)/add-club/ClubForm";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AddClubPage() {
|
||||
const t = useTranslations("AddClub");
|
||||
|
||||
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">{t("title")}</h2>
|
||||
|
||||
<p className="mb-8 text-center font-light text-gray-500 dark:text-gray-400 sm:text-xl lg:mb-16">
|
||||
{t("info")}
|
||||
</p>
|
||||
|
||||
<ClubForm />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { TimeControl } from "@/types";
|
||||
|
||||
type TournamentFormValues = z.infer<typeof addTournamentSchema>;
|
||||
|
||||
const Map = dynamic(() => import("./Map"), {
|
||||
const Map = dynamic(() => import("../components/MapCoordinateSelection"), {
|
||||
ssr: false,
|
||||
loading: LoadingMap,
|
||||
});
|
||||
@@ -205,7 +205,7 @@ const TournamentForm = () => {
|
||||
</div>
|
||||
|
||||
<section id="map" className="z-0 col-span-4 flex h-auto">
|
||||
<Map />
|
||||
<Map page="tournament" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { unstable_cache } from "next/cache";
|
||||
|
||||
import { collections, dbConnect } from "@/server/mongodb";
|
||||
import { Club } from "@/types";
|
||||
import { filterClubsByManualEntry } from "@/utils/clubFilters";
|
||||
import { errorLog } from "@/utils/logger";
|
||||
|
||||
import ClubsDisplay from "./ClubsDisplay";
|
||||
@@ -11,9 +12,12 @@ export const revalidate = 3600; // Revalidate cache every 6 hours
|
||||
const getClubs = async () => {
|
||||
try {
|
||||
await dbConnect();
|
||||
const data = await collections.clubs!.find({}).sort({ name: 1 }).toArray();
|
||||
const data = await collections
|
||||
.clubs!.find({ pending: { $ne: true } })
|
||||
.sort({ name: 1 })
|
||||
.toArray();
|
||||
|
||||
return data
|
||||
return filterClubsByManualEntry(data)
|
||||
.filter((c) => !!c.coordinates)
|
||||
.map<Club>((club) => {
|
||||
return {
|
||||
|
||||
+7
-7
@@ -15,11 +15,11 @@ import { MapEvents } from "@/components/MapEvents";
|
||||
|
||||
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||
|
||||
const Map = () => {
|
||||
const MapCoordinateSelection = ( { page }: {page: "club" | "tournament"}) => {
|
||||
const { control, setValue } = useFormContext();
|
||||
|
||||
const latValue = useWatch({ control, name: "tournament.coordinates.0" });
|
||||
const lngValue = useWatch({ control, name: "tournament.coordinates.1" });
|
||||
const latValue = useWatch({ control, name: `${page}.coordinates.0` });
|
||||
const lngValue = useWatch({ control, name: `${page}.coordinates.1` });
|
||||
|
||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||
const markerRef = useRef<L.Marker | null>(null);
|
||||
@@ -31,17 +31,17 @@ const Map = () => {
|
||||
if (marker != null) {
|
||||
const position = marker.getLatLng();
|
||||
setValue(
|
||||
"tournament.coordinates.0",
|
||||
`${page}.coordinates.0`,
|
||||
Math.round((position.lat + Number.EPSILON) * 10000) / 10000,
|
||||
);
|
||||
setValue(
|
||||
"tournament.coordinates.1",
|
||||
`${page}.coordinates.1`,
|
||||
Math.round((position.lng + Number.EPSILON) * 10000) / 10000,
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[setValue],
|
||||
[setValue, page],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -71,4 +71,4 @@ const Map = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
export default MapCoordinateSelection;
|
||||
Reference in New Issue
Block a user