diff --git a/webapp/CHANGELOG.md b/webapp/CHANGELOG.md index f3a3d05a..fd5ad4b2 100644 --- a/webapp/CHANGELOG.md +++ b/webapp/CHANGELOG.md @@ -1,10 +1,9 @@ ## [0.73.20](https://github.com/SocialGouv/carte-jeune-engage/compare/v0.73.19...v0.73.20) (2024-12-05) - ### Bug Fixes -* desactivate reminder auchan and offer user preferences notifications and group offer activated reminder by user instead by offer ([d52879d](https://github.com/SocialGouv/carte-jeune-engage/commit/d52879d1f07bcbbd4711457fac499ba02691e44e)) -* remove unused import ([fe1bb94](https://github.com/SocialGouv/carte-jeune-engage/commit/fe1bb94cc13ee929b93fd8270523e6cbd0403518)) +- desactivate reminder auchan and offer user preferences notifications and group offer activated reminder by user instead by offer ([d52879d](https://github.com/SocialGouv/carte-jeune-engage/commit/d52879d1f07bcbbd4711457fac499ba02691e44e)) +- remove unused import ([fe1bb94](https://github.com/SocialGouv/carte-jeune-engage/commit/fe1bb94cc13ee929b93fd8270523e6cbd0403518)) ## [0.73.19](https://github.com/SocialGouv/carte-jeune-engage/compare/v0.73.18...v0.73.19) (2024-12-04) diff --git a/webapp/src/components/forms/payload/Form.tsx b/webapp/src/components/forms/payload/Form.tsx index 69caf02e..5d0d4f3b 100644 --- a/webapp/src/components/forms/payload/Form.tsx +++ b/webapp/src/components/forms/payload/Form.tsx @@ -2,8 +2,9 @@ import React, { useState, useCallback } from "react"; import { buildInitialFormState } from "./buildInitialFormState"; import { fields } from "./fields"; import { - CountryField, Form as FormType, + CountryField, + SelectField, TextAreaField, } from "@payloadcms/plugin-form-builder/dist/types"; import { useForm } from "react-hook-form"; @@ -33,7 +34,9 @@ export type FormBlockType = { blockName?: string; blockType?: "formBlock"; enableIntro: Boolean; - form: Omit & { fields: (CountryField | TextAreaField)[] }; + form: Omit & { + fields: (CountryField | TextAreaField | SelectField)[]; + }; }; export const FormBlock: React.FC< diff --git a/webapp/src/components/forms/payload/Ladder/index.tsx b/webapp/src/components/forms/payload/Ladder/index.tsx index 4f528b9f..6dcde166 100644 --- a/webapp/src/components/forms/payload/Ladder/index.tsx +++ b/webapp/src/components/forms/payload/Ladder/index.tsx @@ -6,13 +6,7 @@ import { FieldErrorsImpl, Controller, } from "react-hook-form"; -import { - Flex, - FormControl, - FormLabel, - IconButton, - Text, -} from "@chakra-ui/react"; +import { Flex, FormControl, IconButton, Text } from "@chakra-ui/react"; export const Ladder: React.FC<{ register: UseFormRegister; @@ -53,7 +47,7 @@ export const Ladder: React.FC<{ {ladderArr.map((item) => ( {item}} h={10} diff --git a/webapp/src/components/forms/payload/Select/index.tsx b/webapp/src/components/forms/payload/Select/index.tsx new file mode 100644 index 00000000..5604a6be --- /dev/null +++ b/webapp/src/components/forms/payload/Select/index.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { TextField } from "@payloadcms/plugin-form-builder/dist/types"; +import { + UseFormRegister, + FieldValues, + FieldErrorsImpl, + Controller, +} from "react-hook-form"; +import { Center, Flex, FormControl, Text } from "@chakra-ui/react"; +import ReactIcon from "~/utils/dynamicIcon"; + +export const Select: React.FC<{ + register: UseFormRegister; + control: any; + errors: Partial< + FieldErrorsImpl<{ + [x: string]: any; + }> + >; + field: TextField & { + options: { id: string; label: string; value: string; icon: string }[]; + }; +}> = ({ field: currentField, control }) => { + const options = currentField.options; + + return ( + + { + return ( +
+ {options.map((item) => ( + onChange(item.value)} + > +
+ +
+ + {item.label} + +
+ ))} +
+ ); + }} + /> +
+ ); +}; diff --git a/webapp/src/components/forms/payload/fields.tsx b/webapp/src/components/forms/payload/fields.tsx index eef9c00f..6ec78c63 100644 --- a/webapp/src/components/forms/payload/fields.tsx +++ b/webapp/src/components/forms/payload/fields.tsx @@ -1,7 +1,9 @@ import { Ladder } from "./Ladder"; import { Textarea } from "./Textarea"; +import { Select } from "./Select"; export const fields = { country: Ladder, textarea: Textarea, + select: Select, }; diff --git a/webapp/src/components/modals/IssueModal.tsx b/webapp/src/components/modals/IssueModal.tsx index ed4e5ea7..6f56b638 100644 --- a/webapp/src/components/modals/IssueModal.tsx +++ b/webapp/src/components/modals/IssueModal.tsx @@ -178,10 +178,7 @@ type IssueModalProps = IssueModalOrder | IssueModalCoupon; const IssueModal = (props: IssueModalProps) => { const { isOpen, onClose, kind } = props; - const { user } = useAuth(); - const CrispWithNoSSR = dynamic(() => import("../support/Crisp")); - - const [isOpenCrisp, setIsOpenCrisp] = useState(false); + const { showCrispModal, setShowCrispModal } = useAuth(); const id = kind === "order" ? props.order_id : props.coupon_id; @@ -203,74 +200,59 @@ const IssueModal = (props: IssueModalProps) => { ]; return ( - <> - - - {!isOpenCrisp && ( - - - - - - - setIsOpenCrisp(true)} - icon={HiMiniChatBubbleOvalLeftEllipsis} - text="Discutez avec nous en direct" - /> - {kind === "order" && ( - <> - - ou + + + {!showCrispModal && ( + + + + + + + setShowCrispModal(true)} + icon={HiMiniChatBubbleOvalLeftEllipsis} + text="Discutez avec nous en direct" + /> + {kind === "order" && ( + <> + + ou + + + + + + Disponible du lundi au vendredi de +
+ 09h à 12h30 puis de 14h à 17h30
- - - - - Disponible du lundi au vendredi de -
- 09h à 12h30 puis de 14h à 17h30 -
-
- - )} -
-
-
- )} -
- {isOpenCrisp && user && ( - { - setIsOpenCrisp(false); - }} - /> +
+ + )} + +
+
)} - +
); }; diff --git a/webapp/src/components/modals/CouponUsedFeedbackModal.tsx b/webapp/src/components/modals/UsedFeedbackModal.tsx similarity index 90% rename from webapp/src/components/modals/CouponUsedFeedbackModal.tsx rename to webapp/src/components/modals/UsedFeedbackModal.tsx index dd0aa4cf..744b0201 100644 --- a/webapp/src/components/modals/CouponUsedFeedbackModal.tsx +++ b/webapp/src/components/modals/UsedFeedbackModal.tsx @@ -16,14 +16,14 @@ import { FormBlock } from "../forms/payload/Form"; import { Form } from "~/payload/payload-types"; import { useAuth } from "~/providers/Auth"; import { UserIncluded } from "~/server/api/routers/user"; -import { OfferIncluded } from "~/server/api/routers/offer"; import { CouponIncluded } from "~/server/api/routers/coupon"; type CouponUsedFeedbackModalProps = { isOpen: boolean; onClose: () => void; onConfirm: () => void; - offer: CouponIncluded["offer"]; + kind: "coupon" | "order"; + offer_id: number; }; const CouponUsedFeedbackModalContent = ({ @@ -32,14 +32,14 @@ const CouponUsedFeedbackModalContent = ({ setCurrentStep, onClose, user, - offer, + offer_id, }: { couponUsedFeedbackForm: Form | undefined; currentStep: "form" | "finish" | undefined; setCurrentStep: (step: "form" | "finish" | undefined) => void; user: UserIncluded | null; onClose: () => void; - offer: CouponIncluded["offer"]; + offer_id: number; }) => { switch (currentStep) { case "form": @@ -50,7 +50,7 @@ const CouponUsedFeedbackModalContent = ({ form={couponUsedFeedbackForm as any} afterOnSubmit={() => setCurrentStep("finish")} enableIntro={true} - offer_id={offer.id} + offer_id={offer_id} /> )} @@ -121,8 +121,8 @@ const CouponUsedFeedbackModalContent = ({ } }; -const CouponUsedFeedbackModal = (props: CouponUsedFeedbackModalProps) => { - const { isOpen, onClose, onConfirm, offer } = props; +const UsedFeedbackModal = (props: CouponUsedFeedbackModalProps) => { + const { isOpen, onClose, onConfirm, offer_id, kind } = props; const { user } = useAuth(); const [currentStep, setCurrentStep] = useState<"form" | "finish" | undefined>( @@ -130,7 +130,7 @@ const CouponUsedFeedbackModal = (props: CouponUsedFeedbackModalProps) => { ); const { data: resultForm } = api.form.getFormBySlug.useQuery({ - slug: "coupon-used-feedback-form", + slug: `${kind}-used-feedback-form`, }); const { data: form } = resultForm || {}; @@ -158,7 +158,7 @@ const CouponUsedFeedbackModal = (props: CouponUsedFeedbackModalProps) => { currentStep={currentStep} setCurrentStep={setCurrentStep} onClose={closeModal} - offer={offer} + offer_id={offer_id} /> @@ -166,4 +166,4 @@ const CouponUsedFeedbackModal = (props: CouponUsedFeedbackModalProps) => { ); }; -export default CouponUsedFeedbackModal; +export default UsedFeedbackModal; diff --git a/webapp/src/components/offer/page/CouponContent.tsx b/webapp/src/components/offer/page/CouponContent.tsx index 10d02577..f0d6578c 100644 --- a/webapp/src/components/offer/page/CouponContent.tsx +++ b/webapp/src/components/offer/page/CouponContent.tsx @@ -20,7 +20,7 @@ import { CouponIncluded } from "~/server/api/routers/coupon"; import { OfferIncluded } from "~/server/api/routers/offer"; import { getExpiryObject } from "../ExpiryTag"; import InStoreSection from "../InStoreSection"; -import CouponUsedBox from "./CouponUsedBox"; +import OfferUsedBox from "./OfferUsedBox"; import IssueModal from "~/components/modals/IssueModal"; type CouponContentProps = { @@ -53,7 +53,13 @@ const CouponContent = (props: CouponContentProps) => { return ( {!coupon.used && ( - + + + )} void; +type OfferUsedBoxDefaultProps = { + onConfirm: () => void; }; -const CouponUsedBox = (props: CouponUsedBoxProps) => { - const { coupon, confirmCouponUsed } = props; +interface CouponUsedBoxProps extends OfferUsedBoxDefaultProps { + kind: "coupon"; + coupon: CouponIncluded; +} + +interface OrderUsedBoxProps extends OfferUsedBoxDefaultProps { + kind: "order"; + order: OrderIncluded; +} + +type OfferUsedBoxProps = CouponUsedBoxProps | OrderUsedBoxProps; + +const OfferUsedBox = (props: OfferUsedBoxProps) => { + const { kind, onConfirm } = props; const [showUsedBox, setShowUsedBox] = useState(true); const [isSwitched, setIsSwitched] = useState(false); @@ -27,8 +39,9 @@ const CouponUsedBox = (props: CouponUsedBoxProps) => { onClose: onCloseCouponUsedFeedbackModal, } = useDisclosure(); - const { mutateAsync: mutateCouponUsed } = - api.coupon.usedFromUser.useMutation(); + const { mutate: mutateCouponUsed } = api.coupon.usedFromUser.useMutation(); + + const { mutate: mutateOrderUsed } = api.order.usedFromUser.useMutation(); const handleCouponUsed = (used: boolean) => { if (!used) { @@ -39,21 +52,27 @@ const CouponUsedBox = (props: CouponUsedBoxProps) => { }; const confirmUsed = () => { - mutateCouponUsed({ coupon_id: coupon.id }); + if (kind === "coupon") { + const { coupon } = props; + mutateCouponUsed({ coupon_id: coupon.id }); + } else { + const { order } = props; + mutateOrderUsed({ order_id: order.id }); + } setTimeout(() => { setIsSwitched(true); }, 500); setTimeout(() => { - confirmCouponUsed(); + onConfirm(); }, 1000); }; if (!showUsedBox) return; return ( - + { - ); }; -export default CouponUsedBox; +export default OfferUsedBox; diff --git a/webapp/src/components/ui/BackButton.tsx b/webapp/src/components/ui/BackButton.tsx index 76038640..fbfe6379 100644 --- a/webapp/src/components/ui/BackButton.tsx +++ b/webapp/src/components/ui/BackButton.tsx @@ -1,5 +1,6 @@ import { Icon, IconButton } from "@chakra-ui/react"; import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; import { IconType } from "react-icons/lib"; import { TbChevronLeft } from "react-icons/tb"; @@ -13,6 +14,22 @@ const BackButton = (props: BackButtonProps) => { const router = useRouter(); const { variant, onClick, icon } = props; + const [canGoBack, setCanGoBack] = useState(false); + + useEffect(() => { + setCanGoBack(window.history.length > 1); + }, []); + + const handleBack = () => { + if (onClick) { + onClick(); + } else if (canGoBack) { + router.back(); + } else { + router.push("/dashboard"); + } + }; + return ( { aria-label="Retour" colorScheme="whiteBtn" bgColor="white" - onClick={onClick ? onClick : () => router.back()} + onClick={handleBack} borderRadius="2.25xl" size="md" icon={} diff --git a/webapp/src/layouts/DefaultLayout.tsx b/webapp/src/layouts/DefaultLayout.tsx index 79481e4f..cc7ce86c 100644 --- a/webapp/src/layouts/DefaultLayout.tsx +++ b/webapp/src/layouts/DefaultLayout.tsx @@ -11,11 +11,16 @@ import { push } from "@socialgouv/matomo-next"; import SplashScreenModal from "~/components/modals/SplashScreenModal"; import { isIOS } from "~/utils/tools"; import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; + +const CRISP_TOKEN = process.env.NEXT_PUBLIC_CRISP_TOKEN as string; export default function DefaultLayout({ children }: { children: ReactNode }) { const router = useRouter(); const pathname = usePathname(); + const CrispWithNoSSR = dynamic(() => import("../components/support/Crisp")); + const { setDeferredEvent, setShowing, @@ -26,6 +31,8 @@ export default function DefaultLayout({ children }: { children: ReactNode }) { showSplashScreenModal, setShowSplashScreenModal, setServiceWorkerRegistration, + showCrispModal, + setShowCrispModal, } = useAuth(); const { isOpen: isNotificationModalOpen, onClose: onNotificationModalClose } = @@ -193,6 +200,13 @@ export default function DefaultLayout({ children }: { children: ReactNode }) { onClose={onNotificationModalClose} /> )} + {showCrispModal && user && ( + setShowCrispModal(false)} + /> + )} {isLanding &&