diff --git a/api/src/controllers/iframe.ts b/api/src/controllers/iframe.ts index 72aae9a..934e811 100644 --- a/api/src/controllers/iframe.ts +++ b/api/src/controllers/iframe.ts @@ -108,7 +108,10 @@ router.get("/widget/:widgetId/msearch", async (req: Request, res: Response, next } as { [key: string]: any }; if (query.data.domain) where.domain = Array.isArray(query.data.domain) ? { $in: query.data.domain } : query.data.domain; - if (query.data.department) where.departmentName = Array.isArray(query.data.department) ? { $in: query.data.department } : query.data.department; + + if (query.data.department === "none") where.$or = [{ departmentName: "" }, { departmentName: null }]; + else if (query.data.department) where.departmentName = Array.isArray(query.data.department) ? { $in: query.data.department } : query.data.department; + if (query.data.organization) where.organizationName = Array.isArray(query.data.organization) ? { $in: query.data.organization } : query.data.organization; if (query.data.schedule) where.schedule = Array.isArray(query.data.schedule) ? { $in: query.data.schedule } : query.data.schedule; if (query.data.action) where.organizationActions = Array.isArray(query.data.action) ? { $in: query.data.action } : query.data.action; @@ -157,7 +160,13 @@ router.get("/widget/:widgetId/msearch", async (req: Request, res: Response, next if (key !== "domain" && query.data.domain) filters.domain = Array.isArray(query.data.domain) ? { $in: query.data.domain } : query.data.domain; if (key !== "organization" && query.data.organization) filters.organizationName = Array.isArray(query.data.organization) ? { $in: query.data.organization } : query.data.organization; - if (key !== "department" && query.data.department) filters.departmentName = Array.isArray(query.data.department) ? { $in: query.data.department } : query.data.department; + if (key !== "department" && query.data.department) { + if (query.data.department === "none") { + filters.departmentName = { $in: ["", null] }; + } else { + filters.departmentName = Array.isArray(query.data.department) ? { $in: query.data.department } : query.data.department; + } + } if (key !== "schedule" && query.data.schedule) filters.schedule = Array.isArray(query.data.schedule) ? { $in: query.data.schedule } : query.data.schedule; if (key !== "action" && query.data.action) filters.organizationActions = Array.isArray(query.data.action) ? { $in: query.data.action } : query.data.action; if (key !== "beneficiary" && query.data.beneficiary) diff --git a/app/src/scenes/widget/Edit.jsx b/app/src/scenes/widget/Edit.jsx index 172c6b4..6e0c906 100644 --- a/app/src/scenes/widget/Edit.jsx +++ b/app/src/scenes/widget/Edit.jsx @@ -590,7 +590,7 @@ const Frame = ({ widget }) => { }; return ( -
+

Aperçu du widget

Enregistrez le widget pour mettre à jour l'aperçu @@ -625,7 +625,7 @@ const IFRAMES = { const JVA_LOGO = `
-
Proposé par la plateforme publique du bénévolat +
Proposé par la plateforme publique du bénévolat JeVeuxAider.gouv.fr
`; diff --git a/widget-benevolat/components/card.js b/widget-benevolat/components/card.js index 7474846..da9663e 100644 --- a/widget-benevolat/components/card.js +++ b/widget-benevolat/components/card.js @@ -1,11 +1,11 @@ import React from "react"; import Image from "next/image"; import iso from "i18n-iso-countries"; -import { RiArrowRightSLine } from "react-icons/ri"; +import { RiBuildingFill } from "react-icons/ri"; import { DOMAINES } from "../config"; -const Card = ({ widget, mission, color, request }) => { +const Card = ({ widget, mission, request }) => { if (!mission) return null; return ( @@ -13,33 +13,42 @@ const Card = ({ widget, mission, color, request }) => { tabIndex={0} href={mission.url} target="_blank" - className="border min-h-[500px] w-full max-w-[90%] mx-auto flex flex-col border-neutral-grey-950 rounded-xl overflow-hidden focus:outline-none focus-visible:ring focus-visible:ring-blue-800" + className={`${ + widget.style === "carousel" ? "max-w-[336px] max-h-[456px]" : "w-full" + } group border h-full mx-auto flex flex-col border-neutral-grey-950 overflow-hidden focus:outline-none focus-visible:ring focus-visible:ring-blue-800 hover:shadow-lg transition-shadow duration-300`} > -
- {mission.title} +
+ {mission.title}
-
-
-
- + +
+
+ {DOMAINES[mission.domain] || mission.domain} +
+ + {mission.organizationName}
-

{mission.title}

- - {mission.remote === "full" ? "À distance" : `${mission.city} ${mission.postalCode}${mission.country !== "FR" ? `- ${iso.getName(mission.country, "fr")}` : ""}`} -
-
- - {DOMAINES[mission.domain] || mission.domain} + +
+

{mission.title}

+ + {mission.remote === "full" ? "À distance" : `${mission.city} ${mission.postalCode}${mission.country !== "FR" ? `- ${iso.getName(mission.country, "fr")}` : ""}`} -

- Par - {mission.organizationName} -

+
+ +
-
+ +
{`${mission.places} ${mission.places > 1 ? "bénévoles recherchés" : "bénévole recherché"}`} -
diff --git a/widget-benevolat/components/carousel.js b/widget-benevolat/components/carousel.js index d6669ea..45d33ca 100644 --- a/widget-benevolat/components/carousel.js +++ b/widget-benevolat/components/carousel.js @@ -41,49 +41,57 @@ export const Carousel = ({ widget, missions, color, request }) => { } return ( -
-
-
- {missions.slice(0, 60).map((mission, i) => ( -
- -
- ))} -
-
+
+
+ - +
+
+ {missions.slice(0, 60).map((mission, i) => ( +
+ +
+ ))} +
+
- + +
-
-
+
+
- - {Math.floor(currentSlide / slidesToShow) + 1} / {Math.ceil(missions.length / slidesToShow)} - -
-
+
); }; diff --git a/widget-benevolat/components/filters.js b/widget-benevolat/components/filters.js index afa97f2..1d0286e 100644 --- a/widget-benevolat/components/filters.js +++ b/widget-benevolat/components/filters.js @@ -1,6 +1,5 @@ -import React, { useState, Fragment, useRef } from "react"; -import { Listbox, Combobox, Transition } from "@headlessui/react"; -import { RiSearchLine, RiSubtractLine, RiAddLine, RiArrowDownSLine, RiCheckboxFill, RiCheckboxBlankLine, RiMapPin2Fill, RiCloseFill } from "react-icons/ri"; +import React, { useState, Fragment, useRef, useEffect } from "react"; +import { RiSearchLine, RiArrowUpSLine, RiArrowDownSLine, RiCheckboxFill, RiCheckboxBlankLine, RiMapPin2Fill, RiCloseFill } from "react-icons/ri"; export const MobileFilters = ({ options, filters, setFilters, color, showFilters, setShowFilters, disabledLocation = false, carousel }) => { if (!Object.keys(options).length) return null; @@ -19,71 +18,76 @@ export const MobileFilters = ({ options, filters, setFilters, color, showFilters return ( <> -
+
setFilters({ ...filters, location: l })} disabled={disabledLocation} color={color} width="w-full" />
-
- + - {showFilters && ( -
-
- setFilters({ ...filters, remote: f })} - placeholder="Présentiel / Distance" - width="w-full" - color={color} - /> -
-
- setFilters({ ...filters, domain: v })} - placeholder="Domaines" - width="w-full" - color={color} - /> -
-
- setFilters({ ...filters, department: v })} - placeholder="Départements" - width="w-full" - color={color} - /> -
-
- setFilters({ ...filters, organization: v })} - placeholder="Organisations" - width="w-full" - color={color} - /> -
- + {showFilters && ( +
+
+ setFilters({ ...filters, remote: f })} + placeholder="Présentiel / Distance" + width="w-full" + color={color} + />
- )} -
+
+ setFilters({ ...filters, domain: v })} + placeholder="Domaines" + width="w-full" + color={color} + /> +
+
+ setFilters({ ...filters, department: v })} + placeholder="Départements" + width="w-full" + color={color} + /> +
+
+ setFilters({ ...filters, organization: v })} + placeholder="Organisations" + width="w-full" + color={color} + /> +
+ + +
+ )} ); }; @@ -96,7 +100,7 @@ export const Filters = ({ options, filters, setFilters, color, disabledLocation setFilters({ ...filters, location: l })} disabled={disabledLocation} color={color} />
- setFilters({ ...filters, remote: f })} @@ -132,116 +136,135 @@ export const Filters = ({ options, filters, setFilters, color, disabledLocation }; const SelectFilter = ({ options, selectedOptions, onChange, color, placeholder = "Choissiez une option", position = "left-0", width = "w-80" }) => { - const [keyboardNav, setKeyboardNav] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); + const ref = useRef(null); const searchRef = useRef(null); - const searchOptions = options && options.filter ? options.filter((o) => o.label?.toLowerCase().includes(search.toLowerCase())) : []; - const handleKeyDown = () => { - setKeyboardNav(true); - }; - const handleMouseOver = () => { - setKeyboardNav(false); - }; - const handleSearch = (e) => { - setSearch(e.target.value); + + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const toggleOption = (option) => { + if (!selectedOptions) return onChange([option]); + if (selectedOptions.some((o) => o.value === option.value)) { + onChange(selectedOptions.filter((o) => o.value !== option.value)); + } else { + onChange([...selectedOptions, option]); + } }; return ( - -
- - {({ open }) => ( - <> - 0 ? color : "black" }}> - {!selectedOptions || selectedOptions.some((o) => o === undefined) - ? (onChange([]), placeholder) - : selectedOptions.length > 0 - ? `${selectedOptions[0].label}${selectedOptions.length > 1 ? ` +${selectedOptions.length - 1}` : ""}` - : placeholder} - - {open ? : } - - )} - - searchRef.current?.focus()}> - -
-
- - { - if (e.code === "Space") { - e.stopPropagation(); - } - }} - type="text" - value={search} - onChange={handleSearch} - placeholder="Rechercher" - className="w-full text-sm rounded-lg pl-3 focus:outline-none" - /> -
+
+ + + + {isOpen && ( +
+
+
+ + setSearch(e.target.value)} + placeholder="Rechercher" + className="w-full text-sm rounded-lg pl-3 focus:outline-none" + />
-
- {searchOptions?.length === 0 ? ( -
Aucune option disponible
- ) : ( - searchOptions?.map((o) => { +
+ +
+ {!options?.filter((o) => o.label?.toLowerCase().includes(search.toLowerCase()))?.length ? ( +
Aucune option disponible
+ ) : ( + options + ?.filter((o) => o.label?.toLowerCase().includes(search.toLowerCase())) + ?.map((o) => { + const isSelected = selectedOptions?.some((so) => so.value === o.value); return ( - - {({ active, selected }) => ( -
-
-
- {selected ? : } -
- {o.label} -
- {o.count && {o.count}} -
- )} -
+
toggleOption(o)} + className="cursor-pointer w-full flex items-center justify-between text-sm py-2 pl-3 pr-4 hover:bg-gray-100" + style={{ + color: isSelected ? color : "black", + }} + > +
+
{isSelected ? : }
+ {o.label} +
+ {o.count && {o.count}} +
); }) - )} -
- - {({ active }) => ( -
- -
- )} -
- - -
- + )} +
+ +
+ +
+
+ )} +
); }; -const LocationFilter = ({ selected, onChange, color, disabled = false, width = "w-80" }) => { +const LocationFilter = ({ selected, onChange, disabled = false, width = "w-80" }) => { + const [isOpen, setIsOpen] = useState(false); const [options, setOptions] = useState([]); + const [inputValue, setInputValue] = useState(selected?.label || ""); + const ref = useRef(null); + + useEffect(() => { + setInputValue(selected?.label || ""); + }, [selected]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); const handleInputChange = async (e) => { - e.preventDefault(); const search = e.target.value; + setInputValue(search); + if (search?.length > 3) { const res = await fetch(`https://api-adresse.data.gouv.fr/search?q=${search}&type=municipality&autocomplete=1&limit=6`).then((r) => r.json()); if (!res.features) return; @@ -256,59 +279,152 @@ const LocationFilter = ({ selected, onChange, color, disabled = false, width = " name: f.properties.name, })) ); - } else if (search?.length === 0) { + setIsOpen(true); + } else { setOptions([]); + setIsOpen(false); } }; return ( - - {({ disabled }) => ( -
-
- - {disabled ? ( - +
+ +
+ + {disabled ? ( + + ) : ( + <> + + {selected && ( + + )} + + )} +
+ + {isOpen && options.length > 0 && ( +
+ {options.map((option) => ( +
{ + onChange(option); + setInputValue(option.label); + setIsOpen(false); + }} + > + {option.label} +
+ ))} +
+ )} +
+ ); +}; + +const RemoteFilter = ({ options, selectedOptions, onChange, color, placeholder = "Choissiez une option", position = "left-0", width = "w-80" }) => { + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const toggleOption = (option) => { + if (!selectedOptions) return onChange([option]); + const exists = selectedOptions.find((o) => o.value === option.value); + if (exists) { + onChange(selectedOptions.filter((o) => o.value !== option.value)); + } else { + onChange([...selectedOptions, option]); + } + }; + + return ( +
+ + + + {isOpen && ( +
+
+ {options?.length === 0 ? ( +
Aucune option disponible
) : ( - <> - location?.label} - placeholder="Localisation" - onChange={handleInputChange} - /> - {selected && ( - - )} - + options?.map((o) => { + const isSelected = selectedOptions?.some((so) => so.value === o.value); + return ( +
toggleOption(o)} + className="cursor-pointer w-full flex items-center justify-between text-sm py-2 pl-3 pr-4 hover:bg-gray-100" + style={{ + color: isSelected ? color : "black", + }} + > +
+
{isSelected ? : }
+ {o.label} +
+ {o.count && {o.count}} +
+ ); + }) )}
- - {options.length > 0 && ( - - - {options.map((option) => ( - - {({ active }) => ( -
- {option.label} -
- )} -
- ))} -
-
- )} +
+ +
)} - +
); }; diff --git a/widget-benevolat/components/grid.js b/widget-benevolat/components/grid.js index e7530a0..e233192 100644 --- a/widget-benevolat/components/grid.js +++ b/widget-benevolat/components/grid.js @@ -15,7 +15,7 @@ export const Grid = ({ widget, missions, color, total, page, handlePageChange, r } return (
-
+
{missions.map((mission, i) => (
diff --git a/widget-benevolat/pages/index.js b/widget-benevolat/pages/index.js index 754fb98..64fd654 100644 --- a/widget-benevolat/pages/index.js +++ b/widget-benevolat/pages/index.js @@ -5,11 +5,13 @@ import iso from "i18n-iso-countries"; import isoFR from "i18n-iso-countries/langs/fr.json"; import { useRouter } from "next/router"; iso.registerLocale(isoFR); +import Image from "next/image"; import { API_URL, DOMAINES, ENV } from "../config"; import { Carousel } from "../components/carousel"; import { Grid } from "../components/grid"; import { Filters, MobileFilters } from "../components/filters"; +import LogoJVA from "../public/images/LogoJVA.svg"; /** * Layout widget --> max-width: 1152px @@ -64,7 +66,8 @@ const Home = ({ widget, missions, options, total, request, environment }) => { if (filters.domain?.length) query.domain = JSON.stringify(filters.domain.filter((item) => item && item.value).map((item) => item.value)); if (filters.organization?.length) query.organization = JSON.stringify(filters.organization.filter((item) => item && item.value).map((item) => item.value)); - if (filters.department?.length) query.department = JSON.stringify(filters.department.filter((item) => item && item.value).map((item) => item.value)); + if (filters.department?.length) + query.department = JSON.stringify(filters.department.filter((item) => item && item.value).map((item) => (item.value === "" ? "none" : item.value))); if (filters.remote?.length) query.remote = JSON.stringify(filters.remote.filter((item) => item && item.value).map((item) => item.value)); if (filters.size) query.size = filters.size; if (filters.page > 1) query.from = (filters.page - 1) * filters.size; @@ -103,13 +106,17 @@ const Home = ({ widget, missions, options, total, request, environment }) => { if (!widget) return
Erreur lors du chargement du widget
; return ( -
-
-
-

Trouvez une mission de bénévolat

-

{total > 1 ? `${total.toLocaleString("fr")} missions` : `${total} mission`}

+
+
+
+

Trouvez une mission de bénévolat

+

{total > 1 ? `${total.toLocaleString("fr")} missions` : `${total} mission`}

-
+
{ setShowFilters={setShowFilters} />
-
+
setFilters({ ...filters, ...newFilters })} color={color} disabledLocation={!!widget.location} />
-
+
{widget?.style === "carousel" ? ( ) : ( @@ -164,7 +171,11 @@ export const getServerSideProps = async (context) => { if (context.query.domain) JSON.parse(context.query.domain).forEach((item) => searchParams.append("domain", item)); if (context.query.organization) JSON.parse(context.query.organization).forEach((item) => searchParams.append("organization", item)); - if (context.query.department) JSON.parse(context.query.department).forEach((item) => searchParams.append("department", item)); + if (context.query.department) { + JSON.parse(context.query.department).forEach((item) => { + searchParams.append("department", item === "" ? "none" : item); + }); + } if (context.query.remote) JSON.parse(context.query.remote).forEach((item) => searchParams.append("remote", item)); if (context.query.size) searchParams.append("size", context.query.size); if (context.query.from) searchParams.append("from", context.query.from); @@ -181,13 +192,16 @@ export const getServerSideProps = async (context) => { const newOptions = { organizations: response.data.aggs.organization.map((b) => ({ value: b.key, count: b.doc_count, label: b.key })), domains: response.data.aggs.domain.map((b) => ({ value: b.key, count: b.doc_count, label: DOMAINES[b.key] || b.key })), - departments: response.data.aggs.department.map((b) => ({ value: b.key, count: b.doc_count, label: b.key })), + departments: response.data.aggs.department.map((b) => ({ + value: b.key === "" ? "none" : b.key, + count: b.doc_count, + label: b.key === "" ? "Non renseigné" : b.key, + })), remote: [ { value: "no", label: "Présentiel", count: presentiel.reduce((acc, b) => acc + b.doc_count, 0) }, { value: "yes", label: "Distance", count: remote.reduce((acc, b) => acc + b.doc_count, 0) }, ], }; - const query = new URLSearchParams({ widgetId: widget._id, requestId: response.request, diff --git a/widget-volontariat/components/card.js b/widget-volontariat/components/card.js index 030fe6a..3a4acb4 100644 --- a/widget-volontariat/components/card.js +++ b/widget-volontariat/components/card.js @@ -2,7 +2,7 @@ import React from "react"; import Image from "next/image"; import iso from "i18n-iso-countries"; -import { RiArrowRightLine, RiBuildingFill, RiCalendarEventFill } from "react-icons/ri"; +import { RiBuildingFill, RiCalendarEventFill } from "react-icons/ri"; import { DOMAINS } from "../config"; import LogoSCE from "../public/images/logo-sce.svg"; @@ -15,27 +15,12 @@ const Card = ({ widget, mission, request }) => { tabIndex={0} href={mission.url} target="_blank" - className="border min-h-[311px] w-full max-w-[90%] md:max-w-full flex flex-col focus:outline-none focus-visible:ring focus-visible:ring-blue-800 border-grey-400 bg-white rounded-xl overflow-hidden mx-auto" + className={`${ + widget.style === "carousel" ? "md:max-w-[336px]" : "w-full" + } border h-full mx-auto flex flex-col focus:outline-none focus-visible:ring focus-visible:ring-blue-800 border-grey-400 bg-white overflow-hidden group hover:shadow-lg transition-shadow duration-300`} > -
-
-
- -
- -

{mission.title}

- - {mission.remote === "full" ? "À distance" : `${mission.city} ${mission.postalCode}${mission.country !== "FR" ? ` - ${iso.getName(mission.country, "fr")}` : ""}`} - -
-
- {mission.tags?.includes("Service Civique Écologique") ? ( - logo service civique écologique - ) : ( - "" - )} -
-
+
+
@@ -49,14 +34,36 @@ const Card = ({ widget, mission, request }) => {
+ +
+
+ +
+ +

{mission.title}

+
+ + {mission.remote === "full" ? "À distance" : `${mission.city} ${mission.postalCode}${mission.country !== "FR" ? ` - ${iso.getName(mission.country, "fr")}` : ""}`} + +
+
+ +
+ {mission.tags?.includes("Service Civique Écologique") ? ( + logo service civique écologique + ) : ( +
+ )} +
+
Dès que possible
-
+
); diff --git a/widget-volontariat/components/carousel.js b/widget-volontariat/components/carousel.js index d6669ea..45a56cd 100644 --- a/widget-volontariat/components/carousel.js +++ b/widget-volontariat/components/carousel.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { RiArrowLeftLine, RiArrowRightLine } from "react-icons/ri"; import Card from "./card"; -export const Carousel = ({ widget, missions, color, request }) => { +export const Carousel = ({ widget, missions, request }) => { const [currentSlide, setCurrentSlide] = useState(0); const [slidesToShow, setSlidesToShow] = useState(3); @@ -41,51 +41,62 @@ export const Carousel = ({ widget, missions, color, request }) => { } return ( -
-
-
- {missions.slice(0, 60).map((mission, i) => ( -
- -
- ))} -
-
+
+
+ - +
+
+ {missions.slice(0, 60).map((mission, i) => ( +
+ +
+ ))} +
+
- + +
-
-
+
+
- - {Math.floor(currentSlide / slidesToShow) + 1} / {Math.ceil(missions.length / slidesToShow)} - -
-
+
); }; diff --git a/widget-volontariat/components/filters.js b/widget-volontariat/components/filters.js index 9b22605..2bcb8ab 100644 --- a/widget-volontariat/components/filters.js +++ b/widget-volontariat/components/filters.js @@ -1,27 +1,11 @@ -import React, { useState, Fragment, useEffect, useRef } from "react"; -import { Listbox, Popover, Combobox, Transition } from "@headlessui/react"; +import React, { useState, useEffect, useRef } from "react"; import { DayPicker } from "react-day-picker"; import fr from "date-fns/locale/fr"; -import { RiSubtractLine, RiAddLine, RiArrowDownSLine, RiCheckboxFill, RiCheckboxBlankLine, RiRadioButtonLine, RiCircleLine, RiMapPin2Fill, RiCloseFill } from "react-icons/ri"; +import { RiArrowUpSLine, RiArrowDownSLine, RiCheckboxFill, RiCheckboxBlankLine, RiRadioButtonLine, RiCircleLine, RiMapPin2Fill, RiCloseFill } from "react-icons/ri"; import "react-day-picker/dist/style.css"; -const hexToRgb = (hex) => { - const bigint = parseInt(hex.replace("#", ""), 16); - const r = (bigint >> 16) & 255; - const g = (bigint >> 8) & 255; - const b = bigint & 255; - - return { r, g, b }; -}; - -const getTextColor = (color) => { - const { r, g, b } = hexToRgb(color); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - return brightness > 125 ? "black" : "white"; -}; - -export const MobileFilters = ({ options, filters, setFilters, color, showFilters, setShowFilters, disabledLocation = false, carousel }) => { +export const MobileFilters = ({ options, filters, setFilters, showFilters, setShowFilters, disabledLocation = false, carousel }) => { if (!Object.keys(options).length) return null; const handleReset = () => { @@ -42,91 +26,88 @@ export const MobileFilters = ({ options, filters, setFilters, color, showFilters return ( <> -
- setFilters({ ...filters, location: l })} disabled={disabledLocation} color={color} /> +
+ setFilters({ ...filters, location: l })} disabled={disabledLocation} />
-
+
{showFilters && ( -
-
- setFilters({ ...filters, start: f })} color={color} width="w-full" /> +
+
+ setFilters({ ...filters, start: f })} width="w-full" />
-
- setFilters({ ...filters, duration: v })} color={color} width="w-full" /> +
+ setFilters({ ...filters, duration: v })} width="w-full" />
-
+
setFilters({ ...filters, domain: v })} placeholder="Domaines" - color={color} width="w-full" />
-
+
setFilters({ ...filters, schedule: v })} placeholder="Horaires" - color={color} width="w-full" />
-
+
setFilters({ ...filters, accessibility: v })} placeholder="Accessibilité" - color={color} width="w-full" />
-
+
setFilters({ ...filters, beneficiary: v })} placeholder="Public bénéficiaire" - color={color} width="w-full" />
-
+
setFilters({ ...filters, action: v })} placeholder="Actions clés" - color={color} width="w-full" />
-
+
setFilters({ ...filters, country: v })} placeholder="France / Etranger" position="right-0" - color={color} />
+
@@ -136,7 +117,7 @@ export const MobileFilters = ({ options, filters, setFilters, color, showFilters ); }; -export const Filters = ({ options, filters, setFilters, color, disabledLocation = false }) => { +export const Filters = ({ options, filters, setFilters, disabledLocation = false }) => { const [moreFilters, setMoreFilters] = useState(false); const missionsAbroad = useRef(null); @@ -150,11 +131,11 @@ export const Filters = ({ options, filters, setFilters, color, disabledLocation return (
-
- setFilters({ ...filters, location: l })} disabled={disabledLocation} color={color} /> - setFilters({ ...filters, start: f })} color={color} /> - setFilters({ ...filters, duration: v })} color={color} /> - setFilters({ ...filters, domain: v })} placeholder="Thèmes" color={color} /> +
+ setFilters({ ...filters, location: l })} disabled={disabledLocation} /> + setFilters({ ...filters, start: f })} /> + setFilters({ ...filters, duration: v })} /> + setFilters({ ...filters, domain: v })} placeholder="Thèmes" /> {moreFilters ? ( setFilters({ ...filters, minor: v })} placeholder="Accès aux mineurs" position="right-0" - color={color} /> ) : ( -
- -
- - )} - - -
- + {isOpen && ( +
+
+

Je suis disponible à partir du

+
+ { + onChange({ label: date.toLocaleDateString("fr"), value: date }); + setIsOpen(false); + }} + className="w-full flex justify-center border-none" + modifiers={{ + selected: (date) => selected && date.toLocaleDateString("fr") === selected.value.toLocaleDateString("fr"), + }} + modifiersStyles={{ + selected: { + backgroundColor: "black", + }, + }} + /> +
+ +
+
+ )} +
); }; @@ -307,163 +288,197 @@ const DURATION_OPTIONS = [ { label: "12 mois", value: 12 }, ]; -const DurationFilter = ({ selected, onChange, color, position = "left-0", width = "w-80" }) => { +const DurationFilter = ({ selected, onChange, position = "left-0", width = "w-80" }) => { + const [isOpen, setIsOpen] = useState(false); const [keyboardNav, setKeyboardNav] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + const handleKeyDown = () => { setKeyboardNav(true); }; + const handleMouseOver = () => { setKeyboardNav(false); }; return ( - -
- - {({ open }) => ( - <> - - {selected ? selected.label : "Durée"} - - {open ? : } - - )} - - - -
-
-

Durée maximale de la mission

-
- {DURATION_OPTIONS.map((o) => { - return ( - - {({ active, selected }) => ( -
-
-
- {selected ? : } -
- - {o.label} -
-
- )} -
- ); - })} +
+ + + + {isOpen && ( +
+
+
+

Durée maximale de la mission

- - {({ active }) => ( -
- + {DURATION_OPTIONS.map((o) => { + return ( +
{ + onChange(o); + setIsOpen(false); + setKeyboardNav(false); + }} + > +
+
+ {selected?.value === o.value ? : } +
+ {o.label} +
- )} - - - -
- + ); + })} +
+
+ +
+
+ )} +
); }; -const SelectFilter = ({ options, selectedOptions, onChange, color, placeholder = "Choissiez une option", position = "left-0", width = "w-80" }) => { - const [keyboardNav, setKeyboardNav] = useState(false); +const SelectFilter = ({ options, selectedOptions, onChange, placeholder = "Choissiez une option", position = "left-0", width = "w-80" }) => { + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); - const handleKeyDown = () => { - setKeyboardNav(true); - }; - const handleMouseOver = () => { - setKeyboardNav(false); + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const toggleOption = (option) => { + if (!selectedOptions) return onChange([option]); + if (selectedOptions.some((o) => o.value === option.value)) { + onChange(selectedOptions.filter((o) => o.value !== option.value)); + } else { + onChange([...selectedOptions, option]); + } }; return ( - -
- - {({ open }) => ( - <> - 0 ? color : "black" }}> - {!selectedOptions || selectedOptions.some((o) => o === undefined) - ? (onChange([]), placeholder) - : selectedOptions.length > 0 - ? `${selectedOptions[0].label}${selectedOptions.length > 1 ? ` +${selectedOptions.length - 1}` : ""}` - : placeholder} - - - {open ? : } - - )} - - - -
- {options?.length === 0 ? ( -
Aucune option disponible
- ) : ( - options.map((o) => { - return ( - - {({ active, selected }) => ( -
-
-
- {selected ? : } -
- {o.label} -
- {o.count && {o.count}} -
- )} -
- ); - }) - )} -
+
+ + - - {({ active }) => ( -
- -
- )} -
- - -
- + {isOpen && ( +
+
+ {options?.length === 0 ? ( +
Aucune option disponible
+ ) : ( + options.map((o) => { + const isSelected = selectedOptions?.some((so) => so.value === o.value); + return ( +
toggleOption(o)}> +
+
{isSelected ? : }
+ {o.label} +
+ {o.count && {o.count}} +
+ ); + }) + )} +
+
+ +
+
+ )} +
); }; -const LocationFilter = ({ selected, onChange, color, disabled = false, width = "w-80" }) => { +const LocationFilter = ({ selected, onChange, disabled = false, width = "w-80" }) => { + const [isOpen, setIsOpen] = useState(false); const [options, setOptions] = useState([]); + const [inputValue, setInputValue] = useState(selected?.label || ""); + const ref = useRef(null); + + useEffect(() => { + setInputValue(selected?.label || ""); + }, [selected]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsOpen(false); + setOptions([]); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); const handleInputChange = async (e) => { - e.preventDefault(); const search = e.target.value; + setInputValue(search); + if (search?.length > 3) { const res = await fetch(`https://api-adresse.data.gouv.fr/search?q=${search}&type=municipality&autocomplete=1&limit=6`).then((r) => r.json()); if (!res.features) return; @@ -478,63 +493,64 @@ const LocationFilter = ({ selected, onChange, color, disabled = false, width = " name: f.properties.name, })) ); - } else if (search?.length === 0) { + setIsOpen(true); + } else { setOptions([]); + setIsOpen(false); } }; return ( - - {({ disabled }) => ( -
-
- - {disabled ? ( - - ) : ( - <> - location?.label} - placeholder="Localisation" - onChange={handleInputChange} - /> - {selected && ( - - )} - +
+ +
+ + {disabled ? ( + + ) : ( + <> + + {selected && ( + )} -
+ + )} +
- {options.length > 0 && ( - - - {options.map((option) => ( - - {({ active }) => ( -
- {option.label} -
- )} -
- ))} -
-
- )} + {options.length > 0 && isOpen && ( +
+ {options.map((option) => ( +
{ + onChange(option); + setInputValue(option.label); + setIsOpen(false); + }} + > + {option.label} +
+ ))}
)} - +
); }; diff --git a/widget-volontariat/components/grid.js b/widget-volontariat/components/grid.js index 2f7176f..b8d5c55 100644 --- a/widget-volontariat/components/grid.js +++ b/widget-volontariat/components/grid.js @@ -15,10 +15,10 @@ export const Grid = ({ widget, missions, color, total, page, request, handlePage } return ( -
-
+
+
{missions.map((mission, i) => ( -
+
))} @@ -33,7 +33,7 @@ export const Grid = ({ widget, missions, color, total, page, request, handlePage ); }; -const Pagination = ({ page, setPage, end, color }) => { +const Pagination = ({ page, setPage, end }) => { const [pages, setPages] = useState([...Array(end).keys()].map((i) => i + 1)); useEffect(() => { @@ -58,18 +58,18 @@ const Pagination = ({ page, setPage, end, color }) => { {pages.slice(0, 4).map((p) => ( ))} - - {pages.slice(end - 4, end).map((p) => ( - {pages.slice(page - 2, page + 1).map((p) => ( ))}