diff --git a/webapp/package.json b/webapp/package.json
index 35995564..f20fd5a9 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -51,6 +51,7 @@
"react-easy-crop": "^5.0.5",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.0",
+ "react-qrcode-logo": "^2.9.0",
"sharp": "^0.33.2",
"superjson": "^1.13.3",
"tsx": "^4.7.0",
diff --git a/webapp/public/images/cje-logo-white.svg b/webapp/public/images/cje-logo-white.svg
new file mode 100644
index 00000000..64b68cac
--- /dev/null
+++ b/webapp/public/images/cje-logo-white.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/public/images/cje-logo.png b/webapp/public/images/cje-logo.png
new file mode 100644
index 00000000..e3c4eef9
Binary files /dev/null and b/webapp/public/images/cje-logo.png differ
diff --git a/webapp/public/images/landing/main.png b/webapp/public/images/landing/main.png
new file mode 100644
index 00000000..c58b31df
Binary files /dev/null and b/webapp/public/images/landing/main.png differ
diff --git a/webapp/public/images/landing/map-mobile.png b/webapp/public/images/landing/map-mobile.png
new file mode 100644
index 00000000..9a712e6c
Binary files /dev/null and b/webapp/public/images/landing/map-mobile.png differ
diff --git a/webapp/public/images/landing/map.png b/webapp/public/images/landing/map.png
new file mode 100644
index 00000000..0de37dc3
Binary files /dev/null and b/webapp/public/images/landing/map.png differ
diff --git a/webapp/public/images/landing/mobile-showcase.png b/webapp/public/images/landing/mobile-showcase.png
new file mode 100644
index 00000000..3f5a4c7e
Binary files /dev/null and b/webapp/public/images/landing/mobile-showcase.png differ
diff --git a/webapp/public/images/landing/section-1.png b/webapp/public/images/landing/section-1.png
new file mode 100644
index 00000000..c61f2fa1
Binary files /dev/null and b/webapp/public/images/landing/section-1.png differ
diff --git a/webapp/public/images/landing/section-2.png b/webapp/public/images/landing/section-2.png
new file mode 100644
index 00000000..b040ae5e
Binary files /dev/null and b/webapp/public/images/landing/section-2.png differ
diff --git a/webapp/public/images/landing/section-3.png b/webapp/public/images/landing/section-3.png
new file mode 100644
index 00000000..1a1f61e2
Binary files /dev/null and b/webapp/public/images/landing/section-3.png differ
diff --git a/webapp/public/images/landing/section-4.png b/webapp/public/images/landing/section-4.png
new file mode 100644
index 00000000..c456a818
Binary files /dev/null and b/webapp/public/images/landing/section-4.png differ
diff --git a/webapp/public/images/marianne-white.svg b/webapp/public/images/marianne-white.svg
new file mode 100644
index 00000000..09974a9d
--- /dev/null
+++ b/webapp/public/images/marianne-white.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/src/components/forms/FormBlock.tsx b/webapp/src/components/forms/FormBlock.tsx
index 5468630a..b2064a16 100644
--- a/webapp/src/components/forms/FormBlock.tsx
+++ b/webapp/src/components/forms/FormBlock.tsx
@@ -39,6 +39,7 @@ const FormBlock = ({
}}
border="2px solid"
borderColor={isSelected ? "blackLight" : "transparent"}
+ cursor="pointer"
>
{iconSrc && }
;
- fieldError: FieldError | undefined;
+ field: FieldProps;
+ register: UseFormRegister;
+ fieldError: FieldError | undefined;
+ wrapperProps?: ChakraProps;
+ inputProps?: InputProps;
}
const FormInput = ({
- field: { name, kind, rules, label, placeholder, prefix },
- register,
- fieldError,
+ field: { name, kind, rules, label, placeholder, prefix },
+ register,
+ fieldError,
+ wrapperProps,
+ inputProps,
}: Props) => {
- return (
-
-
- {label && (
-
- {label}
-
- )}
- {fieldError?.message}
-
- );
+ const { autoFocus = true, ...restInputProps } = inputProps || {};
+
+ return (
+
+
+ {label && (
+
+ {label}
+
+ )}
+ {fieldError?.message}
+
+ );
};
export default FormInput;
diff --git a/webapp/src/components/landing/FAQSectionAccordionItem.tsx b/webapp/src/components/landing/FAQSectionAccordionItem.tsx
new file mode 100644
index 00000000..6855427b
--- /dev/null
+++ b/webapp/src/components/landing/FAQSectionAccordionItem.tsx
@@ -0,0 +1,106 @@
+import {
+ AccordionButton,
+ AccordionItem,
+ AccordionPanel,
+ Box,
+ Icon,
+ IconButton,
+ Text,
+ useBreakpointValue,
+} from "@chakra-ui/react";
+import { HiPlus, HiXMark } from "react-icons/hi2";
+
+type FAQSectionAccordionItemProps = {
+ title: string;
+ content: string;
+ index: number;
+ currentIndex: number | null;
+ setCurrentIndex: React.Dispatch>;
+ total: number;
+};
+
+const FAQSectionAccordionItem = ({
+ title,
+ content,
+ index,
+ currentIndex,
+ setCurrentIndex,
+ total,
+}: FAQSectionAccordionItemProps) => {
+ const accordionBtnSize = useBreakpointValue({
+ base: "sm",
+ lg: "md",
+ });
+
+ return (
+
+ {({ isExpanded }) => (
+
+ setCurrentIndex(!isExpanded ? index : null)}
+ pt={{ base: 2, lg: 8 }}
+ px={{ base: 4, lg: 8 }}
+ >
+
+ {title}
+
+ {!isExpanded ? (
+ }
+ onClick={() => setCurrentIndex(index)}
+ colorScheme="gray"
+ px={{ lg: 0 }}
+ size={accordionBtnSize}
+ borderRadius="full"
+ aria-label="Ouvrir l'accordéon"
+ />
+ ) : (
+ }
+ onClick={() => setCurrentIndex(null)}
+ px={{ lg: 0 }}
+ size={accordionBtnSize}
+ borderRadius="full"
+ aria-label="Fermer l'accordéon"
+ />
+ )}
+
+
+
+ {content}
+
+
+
+ )}
+
+ );
+};
+
+export default FAQSectionAccordionItem;
diff --git a/webapp/src/components/landing/Footer.tsx b/webapp/src/components/landing/Footer.tsx
new file mode 100644
index 00000000..7725ec94
--- /dev/null
+++ b/webapp/src/components/landing/Footer.tsx
@@ -0,0 +1,114 @@
+import { Box, Container, Divider, Flex, Image, Text } from "@chakra-ui/react";
+import { menuItems } from "./Header";
+import Link from "next/link";
+
+const Footer = () => {
+ const handleIsEligibleClick = () => {
+ const element = document.querySelector(".phone-number-footer");
+ if (element) (element as HTMLElement).focus();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Des remises exclusives pour les jeunes en
+
+ insertion.
+
+ Découvrez la Carte Jeune Engagé !
+
+
+
+ Vérifier mon éligibilité
+
+ {menuItems.map((item) => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+
+
+
+ CGU
+
+
+ Mentions légales
+
+
+ Politique de confidentialité
+
+
+
+
+
+ );
+};
+
+export default Footer;
diff --git a/webapp/src/components/landing/Header.tsx b/webapp/src/components/landing/Header.tsx
new file mode 100644
index 00000000..fb68753a
--- /dev/null
+++ b/webapp/src/components/landing/Header.tsx
@@ -0,0 +1,176 @@
+import {
+ Box,
+ Button,
+ Collapse,
+ Flex,
+ Icon,
+ Portal,
+ Stack,
+ Text,
+ useBreakpointValue,
+ useDisclosure,
+} from "@chakra-ui/react";
+import ChakraNextImage from "../ChakraNextImage";
+import { HiMiniBars3, HiXMark } from "react-icons/hi2";
+import Link from "next/link";
+import { useRef } from "react";
+import useActiveSection from "~/hooks/useActiveSection";
+
+export const menuItems = [
+ { title: "Qu'est-ce que c'est ?", slug: "what-is-it" },
+ { title: "Qui peut en profiter ?", slug: "who-can-benefit" },
+ { title: "Comment ça marche ?", slug: "how-does-it-work" },
+ { title: "FAQ", slug: "faq" },
+];
+
+const Header = () => {
+ const isDesktop = useBreakpointValue({ base: false, lg: true });
+
+ const { isOpen, onToggle } = useDisclosure();
+
+ const headerRef = useRef(null);
+
+ const activeSection = useActiveSection(
+ menuItems.map((item) => `${item.slug}-section`)
+ );
+
+ const handleIsEligibleClick = () => {
+ const element = document.querySelector(".phone-number-cta");
+ if (element) (element as HTMLElement).focus();
+ onToggle();
+ };
+
+ return (
+
+
+
+
+
+
+ {!isDesktop ? (
+
+
+
+
+
+ {menuItems.map(({ slug, title }, index) => (
+
+
+
+ {title}
+
+
+
+ ))}
+
+
+ Vérifier mon éligibilité
+
+
+
+
+
+
+ ) : (
+ <>
+
+ {menuItems.map(({ slug, title }) => (
+
+
+ {title}
+
+
+
+ ))}
+
+
+ Vérifier mon éligibilité
+
+ >
+ )}
+
+
+ );
+};
+
+export default Header;
diff --git a/webapp/src/components/landing/HowItWorkSectionCard.tsx b/webapp/src/components/landing/HowItWorkSectionCard.tsx
new file mode 100644
index 00000000..5ef07da3
--- /dev/null
+++ b/webapp/src/components/landing/HowItWorkSectionCard.tsx
@@ -0,0 +1,44 @@
+import { Box, Center, Flex, Text } from "@chakra-ui/react";
+
+type HowItWorksSectionCardProps = {
+ title: string;
+ description: string;
+ number: number;
+};
+
+const HowItWorksSectionCard = ({
+ title,
+ description,
+ number,
+}: HowItWorksSectionCardProps) => {
+ return (
+
+
+
+ {number}
+
+
+
+ {title}
+
+
+ {description}
+
+
+ );
+};
+
+export default HowItWorksSectionCard;
diff --git a/webapp/src/components/landing/MapSectionCard.tsx b/webapp/src/components/landing/MapSectionCard.tsx
new file mode 100644
index 00000000..ba13508b
--- /dev/null
+++ b/webapp/src/components/landing/MapSectionCard.tsx
@@ -0,0 +1,34 @@
+import { Box, Flex, Icon, Text } from "@chakra-ui/react";
+import { IconType } from "react-icons/lib";
+
+type MapSectionCardProps = {
+ text: string;
+ icon: IconType;
+};
+
+const MapSectionCard = ({ text, icon }: MapSectionCardProps) => {
+ return (
+
+
+
+
+
+ {text}
+
+
+ );
+};
+
+export default MapSectionCard;
diff --git a/webapp/src/components/landing/NotEligibleForm.tsx b/webapp/src/components/landing/NotEligibleForm.tsx
new file mode 100644
index 00000000..e269ed82
--- /dev/null
+++ b/webapp/src/components/landing/NotEligibleForm.tsx
@@ -0,0 +1,205 @@
+import {
+ Box,
+ Button,
+ Center,
+ Divider,
+ Flex,
+ FormControl,
+ FormLabel,
+ Heading,
+ Icon,
+ Text,
+ useBreakpointValue,
+} from "@chakra-ui/react";
+import { useState } from "react";
+import { Controller, SubmitHandler, useForm } from "react-hook-form";
+import { HiCheckCircle, HiFaceFrown, HiIdentification } from "react-icons/hi2";
+import FormInput from "../forms/FormInput";
+import FormBlock from "../forms/FormBlock";
+
+type NeedingHelpForm = {
+ registered: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+};
+
+const NotEligibleForm = () => {
+ const isDesktop = useBreakpointValue({ base: false, lg: true });
+
+ const [displayForm, setDisplayForm] = useState(false);
+ const [requestSent, setRequestSent] = useState(false);
+
+ const {
+ handleSubmit,
+ register,
+ control,
+ formState: { errors },
+ watch,
+ } = useForm({
+ mode: "onSubmit",
+ });
+
+ const formValues = watch();
+
+ const onSubmit: SubmitHandler = (data) => {
+ console.log(data);
+ setRequestSent(true);
+ };
+
+ if (displayForm && requestSent) {
+ return (
+
+
+
+ Notre équipe traite votre demande au plus vite.
+
+
+ Nous avons bien reçu votre demande, nous allons vous envoyer un mail
+ pour vous aider à vous connecter à l’application.
+
+
+
+
+
+
+ );
+ }
+
+ if (displayForm) {
+ return (
+
+
+
+ Nous allons vous aider à accéder à l’application.
+
+
+ Êtes-vous inscrit à la Mission locale de Sarcelles ?
+
+
+ (
+ <>
+
+ Oui
+
+
+ Non
+
+ >
+ )}
+ />
+
+ {formValues.registered === "no" && (
+
+ L’application est en phase d’expérimentation. Elle est réservée
+ aux jeunes inscrits en contrat d’engagement jeune à la Mission
+ locale de Sarcelles.
+
+ )}
+ {formValues.registered === "yes" && (
+
+
+
+ Vos informations personnelles
+
+
+
+
+
+
+
+
+ Envoyer ma demande
+
+
+ )}
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ On dirait que vous n’êtes pas encore inscrit.
+
+
+ Vérifiez que votre numéro de téléphone correspond à celui avec lequel
+ votre conseiller à la Mission locale de Sarcelles vous a inscrit.
+
+
+
+ Vous avez déjà été inscrit par votre conseiller ?
+
+ setDisplayForm(true)}
+ >
+ J’ai besoin d’aide pour me connecter
+
+
+
+
+
+
+ );
+};
+
+export default NotEligibleForm;
diff --git a/webapp/src/components/landing/PhoneNumberCTA.tsx b/webapp/src/components/landing/PhoneNumberCTA.tsx
new file mode 100644
index 00000000..5555eb0a
--- /dev/null
+++ b/webapp/src/components/landing/PhoneNumberCTA.tsx
@@ -0,0 +1,108 @@
+import { Button, Flex, Icon } from "@chakra-ui/react";
+import { ErrorOption, SubmitHandler, useForm } from "react-hook-form";
+import FormInput from "../forms/FormInput";
+import { frenchPhoneNumber } from "~/utils/tools";
+import { HiArrowRight } from "react-icons/hi2";
+
+export type LoginForm = {
+ phone_number: string;
+};
+
+export type ComponentPhoneNumberKeys =
+ | "phone-number-cta"
+ | "phone-number-footer";
+
+const PhoneNumberCTA = ({
+ onSubmit,
+ currentKey,
+ setCurrentPhoneNumberKey,
+ error,
+ isLoadingOtp,
+}: {
+ onSubmit: SubmitHandler;
+ currentKey: ComponentPhoneNumberKeys;
+ setCurrentPhoneNumberKey: React.Dispatch<
+ React.SetStateAction
+ >;
+ error: {
+ name: ComponentPhoneNumberKeys;
+ error: ErrorOption;
+ } | null;
+ isLoadingOtp: boolean;
+}) => {
+ const {
+ handleSubmit,
+ register,
+ setError,
+ formState: { errors },
+ } = useForm({
+ mode: "onSubmit",
+ });
+
+ if (currentKey === error?.name && errors.phone_number === undefined) {
+ setError("phone_number", {
+ type: error.error.type,
+ message: error.error.message,
+ });
+ }
+
+ return (
+ {
+ e.preventDefault();
+ setCurrentPhoneNumberKey(currentKey);
+ handleSubmit(onSubmit)();
+ }}
+ >
+
+ }
+ >
+ Vérifier mon éligibilité
+
+
+ );
+};
+
+export default PhoneNumberCTA;
diff --git a/webapp/src/components/landing/QRCode.tsx b/webapp/src/components/landing/QRCode.tsx
new file mode 100644
index 00000000..034789a5
--- /dev/null
+++ b/webapp/src/components/landing/QRCode.tsx
@@ -0,0 +1,28 @@
+import { Box, ChakraProps } from "@chakra-ui/react";
+import { QRCode } from "react-qrcode-logo";
+
+type QRCodeProps = {
+ value: string;
+ size?: number;
+ wrapperProps?: ChakraProps;
+};
+
+const QRCodeWrapper = ({ value, size, wrapperProps }: QRCodeProps) => {
+ return (
+
+
+
+ );
+};
+
+export default QRCodeWrapper;
diff --git a/webapp/src/components/landing/SimpleSection.tsx b/webapp/src/components/landing/SimpleSection.tsx
new file mode 100644
index 00000000..c02ef3b8
--- /dev/null
+++ b/webapp/src/components/landing/SimpleSection.tsx
@@ -0,0 +1,37 @@
+import { Box, Flex, Heading, Image, Text } from "@chakra-ui/react";
+
+type SimpleSectionProps = {
+ title: string;
+ description: string;
+ image: string;
+};
+
+const SimpleSection = ({ title, description, image }: SimpleSectionProps) => {
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ );
+};
+
+export default SimpleSection;
diff --git a/webapp/src/components/modals/BaseModal.tsx b/webapp/src/components/modals/BaseModal.tsx
index 779176ea..811c7523 100644
--- a/webapp/src/components/modals/BaseModal.tsx
+++ b/webapp/src/components/modals/BaseModal.tsx
@@ -1,55 +1,68 @@
import {
- Icon,
- IconButton,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalHeader,
- ModalOverlay,
+ Box,
+ Icon,
+ IconButton,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ useBreakpointValue,
} from "@chakra-ui/react";
import { HiMiniXMark } from "react-icons/hi2";
type BaseModalProps = {
- children: React.ReactNode;
- onClose: () => void;
- isOpen: boolean;
- title?: string;
- hideCloseBtn?: boolean;
+ children: React.ReactNode;
+ onClose: () => void;
+ isOpen: boolean;
+ title?: string;
+ hideCloseBtn?: boolean;
};
const BaseModal = ({
- children,
- onClose,
- isOpen,
- hideCloseBtn,
- title,
+ children,
+ onClose,
+ isOpen,
+ hideCloseBtn,
+ title,
}: BaseModalProps) => {
- return (
-
-
-
- {!hideCloseBtn && (
- }
- onClick={onClose}
- aria-label="Fermer la modal"
- />
- )}
- {title && (
-
- {title}
-
- )}
- {children}
-
-
- );
+ const isMobile = useBreakpointValue({ base: true, lg: false });
+
+ return (
+
+
+
+ {!hideCloseBtn && (
+ }
+ onClick={onClose}
+ aria-label="Fermer la modal"
+ />
+ )}
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+
+ );
};
export default BaseModal;
diff --git a/webapp/src/components/theme/modal.ts b/webapp/src/components/theme/modal.ts
new file mode 100644
index 00000000..8fe67a14
--- /dev/null
+++ b/webapp/src/components/theme/modal.ts
@@ -0,0 +1,28 @@
+import { modalAnatomy as parts } from "@chakra-ui/anatomy";
+import { useBreakpoint } from "@chakra-ui/react";
+import {
+ createMultiStyleConfigHelpers,
+ defineStyle,
+} from "@chakra-ui/styled-system";
+
+const { definePartsStyle, defineMultiStyleConfig } =
+ createMultiStyleConfigHelpers(parts.keys);
+
+const dialogPart = defineStyle({
+ maxWidth: "calc(100% - 150px)",
+ paddingBottom: 0,
+ borderRadius: "3xl",
+});
+
+const bodyPart = defineStyle({
+ px: 12,
+ pb: 12,
+});
+
+const sizes = {
+ desktop: definePartsStyle({ dialog: dialogPart, body: bodyPart }),
+};
+
+export const modalTheme = defineMultiStyleConfig({
+ sizes,
+});
diff --git a/webapp/src/hooks/useActiveSection.ts b/webapp/src/hooks/useActiveSection.ts
new file mode 100644
index 00000000..14159ea0
--- /dev/null
+++ b/webapp/src/hooks/useActiveSection.ts
@@ -0,0 +1,31 @@
+import { useState, useEffect } from "react";
+
+export default function useActiveSection(sectionIds: string[]) {
+ const [activeSection, setActiveSection] = useState("");
+
+ useEffect(() => {
+ const handleScroll = () => {
+ let currentSection = "";
+
+ sectionIds.forEach((id) => {
+ const section = document.getElementById(id);
+ const scrollPosition =
+ document.body.scrollTop + document.body.clientHeight / 2; // Adjust this to change when a section becomes "active"
+ if (
+ section &&
+ section.offsetTop <= scrollPosition &&
+ section.offsetTop + section.offsetHeight + 150 > scrollPosition
+ ) {
+ currentSection = id;
+ }
+ });
+
+ setActiveSection(currentSection);
+ };
+
+ document.body.addEventListener("scroll", handleScroll);
+ return () => document.body.removeEventListener("scroll", handleScroll);
+ }, [sectionIds]); // Only re-run the effect if sectionIds changes
+
+ return activeSection;
+}
diff --git a/webapp/src/layouts/DefaultLayout.tsx b/webapp/src/layouts/DefaultLayout.tsx
index 2735a174..936f06cf 100644
--- a/webapp/src/layouts/DefaultLayout.tsx
+++ b/webapp/src/layouts/DefaultLayout.tsx
@@ -1,20 +1,23 @@
-import { Box, Container } from "@chakra-ui/react";
+import { Box, Container, Flex } from "@chakra-ui/react";
import Head from "next/head";
import { usePathname } from "next/navigation";
import { ReactNode, useEffect } from "react";
import BottomNavigation from "~/components/BottomNavigation";
+import Footer from "~/components/landing/Footer";
import { BeforeInstallPromptEvent, useAuth } from "~/providers/Auth";
export default function DefaultLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
- const { setDeferredEvent, setShowing, user } = useAuth();
+ const { setDeferredEvent, setShowing, user, isOtpGenerated } = useAuth();
+
+ const isLanding = pathname === "/" && !isOtpGenerated;
const handleBeforeInstallPrompt = (event: Event) => {
// Prevent the default behavior to keep the event available for later use
event.preventDefault();
- // // Save the event for later use
+ // Save the event for later use
setDeferredEvent(event as BeforeInstallPromptEvent);
setShowing(true);
@@ -57,14 +60,22 @@ export default function DefaultLayout({ children }: { children: ReactNode }) {
/>
-
+
{children}
+ {isLanding && }
{(pathname === "/dashboard" ||
pathname === "/dashboard/wallet" ||
pathname === "/dashboard/categories" ||
diff --git a/webapp/src/pages/dashboard/index.tsx b/webapp/src/pages/dashboard/index.tsx
index aa1e26d1..e06e4106 100644
--- a/webapp/src/pages/dashboard/index.tsx
+++ b/webapp/src/pages/dashboard/index.tsx
@@ -24,8 +24,8 @@ export default function Dashboard() {
sort: "createdAt",
});
- const { data: resultQuickAccess, isLoading: isLoadingQuickAccess } =
- api.quickAccess.getAll.useQuery();
+ const { data: resultQuickAccess, isLoading: isLoadingQuickAccess } =
+ api.globals.quickAccessGetAll.useQuery();
const { data: resultOffers, isLoading: isLoadingOffers } =
api.offer.getListOfAvailables.useQuery({
diff --git a/webapp/src/pages/index.tsx b/webapp/src/pages/index.tsx
index aac515b4..9a4eb052 100644
--- a/webapp/src/pages/index.tsx
+++ b/webapp/src/pages/index.tsx
@@ -1,312 +1,661 @@
import {
- Box,
- Button,
- Divider,
- Flex,
- FormErrorMessage,
- HStack,
- Heading,
- Icon,
- Link,
- PinInput,
- PinInputField,
- Text,
+ Accordion,
+ AspectRatio,
+ Box,
+ Flex,
+ HStack,
+ Heading,
+ Icon,
+ Image,
+ Link,
+ PinInput,
+ PinInputField,
+ Text,
+ chakra,
+ shouldForwardProp,
+ useBreakpointValue,
+ useDisclosure,
} from "@chakra-ui/react";
+import { useGSAP } from "@gsap/react";
import { setCookie } from "cookies-next";
+import { isValidMotionProp, motion } from "framer-motion";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
-import { useForm, type SubmitHandler } from "react-hook-form";
-import { HiArrowRight, HiChevronLeft } from "react-icons/hi2";
+import { type SubmitHandler, ErrorOption } from "react-hook-form";
+import {
+ HiCalendarDays,
+ HiChevronLeft,
+ HiMapPin,
+ HiMiniClipboardDocumentCheck,
+} from "react-icons/hi2";
import BigLoader from "~/components/BigLoader";
import ChakraNextImage from "~/components/ChakraNextImage";
-import FormInput from "~/components/forms/FormInput";
+import Header from "~/components/landing/Header";
import { loginAnimation } from "~/utils/animations";
import { api } from "~/utils/api";
-import { addSpaceToTwoCharacters, frenchPhoneNumber } from "~/utils/tools";
+import { addSpaceToTwoCharacters } from "~/utils/tools";
+import SectionContent from "~/components/landing/SimpleSection";
+import MapSectionCard from "~/components/landing/MapSectionCard";
+import HowItWorksSectionCard from "~/components/landing/HowItWorkSectionCard";
+import FAQSectionAccordionItem from "~/components/landing/FAQSectionAccordionItem";
+import BaseModal from "~/components/modals/BaseModal";
+import PhoneNumberCTA, {
+ ComponentPhoneNumberKeys,
+ LoginForm,
+} from "~/components/landing/PhoneNumberCTA";
+import QRCodeWrapper from "~/components/landing/QRCode";
+import NotEligibleForm from "~/components/landing/NotEligibleForm";
+import { useAuth } from "~/providers/Auth";
-type LoginForm = {
- phone_number: string;
-};
+const ChakraBox = chakra(motion.div, {
+ shouldForwardProp: (prop) =>
+ isValidMotionProp(prop) || shouldForwardProp(prop),
+});
const pinProps = {
- w: 12,
- h: 12,
- borderColor: "transparent",
- _hover: { borderColor: "transparent" },
- _focus: { borderColor: "blackLight", borderWidth: "2px" },
- _focusVisible: { boxShadow: "none" },
+ w: 12,
+ h: 12,
+ borderColor: "transparent",
+ _hover: { borderColor: "transparent" },
+ _focus: { borderColor: "blackLight", borderWidth: "2px" },
+ _focusVisible: { boxShadow: "none" },
};
const defaultTimeToResend = 30;
+const sectionItems = [
+ {
+ title:
+ "Bénéficiez d’un statut privilégié qui vous offre des remises avantageuses",
+ description:
+ "Vous n’êtes pas encore en formation ni en emploi, vous bénéficiez d’un statut de jeune engagé en étant inscrit à la Mission locale, avec la carte “jeune engagé”, vous accédez à toutes les réductions disponibles pour vous !",
+ image: "/images/landing/section-1.png",
+ },
+ {
+ title:
+ "Tout ce qu’il faut pour bien démarrer dans la vie active, à prix réduit grâce aux partenaires",
+ description:
+ "La carte “jeune engagé” vous fait économisez pour tout grâce aux nombreux partenaires participants. Bénéficiez de prix instantanément réduits pour faire vos courses, pour équiper votre logement, pour le matériel informatique mais aussi pour vos assurances et vos abonnements. ",
+ image: "/images/landing/section-2.png",
+ },
+ {
+ title: "Des réductions à utiliser en ligne ou en magasin",
+ description:
+ "Profitez d’une flexibilité totale avec la carte “jeune engagé” ! L’application vous offre des réductions à utiliser en ligne mais aussi directement en magasin : plus pratique pour faire vos courses par exemple. ",
+ image: "/images/landing/section-3.png",
+ },
+ {
+ title: "Suivez toutes vos économies",
+ description:
+ "Gardez un œil sur vos économies grâce à notre fonction de suivi intégrée. Consultez facilement l'historique de vos économies et suivez les au fil du temps. ",
+ image: "/images/landing/section-4.png",
+ },
+];
+
export default function Home() {
- const router = useRouter();
+ const router = useRouter();
+
+ const { isOtpGenerated, setIsOtpGenerated } = useAuth();
+
+ const isDesktop = useBreakpointValue({ base: false, lg: true });
+
+ const {
+ isOpen: isOpenDesktopLoginSuccessful,
+ onOpen: onOpenDesktopLoginSuccessful,
+ onClose: onCloseDesktopLoginSuccessful,
+ } = useDisclosure({
+ onClose: () => setCurrentPhoneNumber(""),
+ });
+
+ const {
+ isOpen: isOpenDesktopLoginError,
+ onOpen: onOpenDesktopLoginError,
+ onClose: onCloseDesktopLoginError,
+ } = useDisclosure();
+
+ const [hasOtpError, setHasOtpError] = useState(false);
+ const [hasOtpExpired, setHasOtpExpired] = useState(false);
+ const [forceLoader, setForceLoader] = useState(false);
+
+ const [timeToResend, setTimeToResend] = useState(defaultTimeToResend);
+ const [intervalId, setIntervalId] = useState(null);
+ const [faqCurrentIndex, setFaqCurrentIndex] = useState(null);
+
+ const [currentPhoneNumberKey, setCurrentPhoneNumberKey] =
+ useState("phone-number-cta");
+ const [currentPhoneNumber, setCurrentPhoneNumber] = useState("");
+ const [phoneNumberError, setPhoneNumberError] = useState<{
+ name: ComponentPhoneNumberKeys;
+ error: ErrorOption;
+ } | null>(null);
+
+ const resetTimer = () => {
+ if (intervalId) clearInterval(intervalId);
+ setTimeToResend(defaultTimeToResend);
+ const id = setInterval(() => {
+ setTimeToResend((prevTime) => prevTime - 1);
+ }, 1000);
+ setIntervalId(id);
+ };
- const [isOtpGenerated, setIsOtpGenerated] = useState(false);
- const [hasOtpError, setHasOtpError] = useState(false);
- const [hasOtpExpired, setHasOtpExpired] = useState(false);
- const [forceLoader, setForceLoader] = useState(false);
+ const { data: resultLogoPartners, isLoading: isLoadingLogoPartners } =
+ api.globals.landingPartnersGetLogos.useQuery();
- const [timeToResend, setTimeToResend] = useState(defaultTimeToResend);
- const [intervalId, setIntervalId] = useState(null);
+ const logoPartners = resultLogoPartners?.data || [];
- const {
- handleSubmit,
- register,
- setError,
- setValue,
- watch,
- formState: { errors },
- } = useForm({
- mode: "onSubmit",
- });
+ const { data: resultFAQ, isLoading: isLoadingFAQ } =
+ api.globals.landingFAQGetAll.useQuery();
- const formValues = watch();
+ const landingFAQ = resultFAQ?.data || [];
- const resetTimer = () => {
- if (intervalId) clearInterval(intervalId);
- setTimeToResend(defaultTimeToResend);
- const id = setInterval(() => {
- setTimeToResend((prevTime) => prevTime - 1);
- }, 1000);
- setIntervalId(id);
- };
+ const { mutate: generateOtp, isLoading: isLoadingOtp } =
+ api.user.generateOTP.useMutation({
+ onSuccess: () => {
+ if (isDesktop) {
+ onOpenDesktopLoginSuccessful();
+ } else {
+ setIsOtpGenerated(true);
+ resetTimer();
+ }
+ },
+ onError: async ({ data }) => {
+ if (data?.httpStatus === 401) {
+ onOpenDesktopLoginError();
+ // setPhoneNumberError({
+ // name: currentPhoneNumberKey,
+ // error: {
+ // type: "conflict",
+ // message:
+ // "Votre numéro de téléphone n'est pas autorisé à accéder à l'application",
+ // },
+ // });
+ } else {
+ setPhoneNumberError({
+ name: currentPhoneNumberKey,
+ error: {
+ type: "internal",
+ message: "Erreur coté serveur, veuillez contacter le support",
+ },
+ });
+ }
+ },
+ });
- const { mutate: generateOtp, isLoading: isLoadingOtp } =
- api.user.generateOTP.useMutation({
- onSuccess: async ({ data }) => {
- setIsOtpGenerated(true);
- resetTimer();
- },
- onError: async ({ data }) => {
- console.log(data?.httpStatus)
- if (data?.httpStatus === 401) {
- setError("phone_number", {
- type: "conflict",
- message:
- "Votre numéro de téléphone n'est pas autorisé à accéder à l'application",
- });
- } else {
- setError("phone_number", {
- type: "internal",
- message:
- "Erreur coté serveur, veuillez contacter le support",
- });
- }
- },
- });
+ const { mutate: loginUser, isLoading: isLoadingLogin } =
+ api.user.loginUser.useMutation({
+ onSuccess: async ({ data }) => {
+ setCookie(
+ process.env.NEXT_PUBLIC_JWT_NAME ?? "cje-jwt",
+ data.token || ""
+ );
+ router.reload();
+ router.push("/dashboard");
+ },
+ onError: async ({ data }) => {
+ if (data?.httpStatus === 401) {
+ setHasOtpError(true);
+ } else if (data?.httpStatus === 408) {
+ setHasOtpExpired(true);
+ }
+ setForceLoader(false);
+ },
+ });
- const { mutate: loginUser, isLoading: isLoadingLogin } =
- api.user.loginUser.useMutation({
- onSuccess: async ({ data }) => {
- setCookie(
- process.env.NEXT_PUBLIC_JWT_NAME ?? "cje-jwt",
- data.token || ""
- );
- router.reload();
- router.push("/dashboard");
- },
- onError: async ({ data }) => {
- if (data?.httpStatus === 401) {
- setHasOtpError(true);
- } else if (data?.httpStatus === 408) {
- setHasOtpExpired(true);
- }
- setForceLoader(false);
- },
- });
+ const logoAnimationBreakpoint = useBreakpointValue({
+ base: {
+ x: ["0px", `-${57 * logoPartners.length + 32 * logoPartners.length}px`],
+ transition: {
+ repeat: Infinity,
+ duration: 4,
+ ease: "linear",
+ },
+ },
+ lg: undefined,
+ });
- const handleGenerateOtp: SubmitHandler = async (values) => {
- generateOtp({ phone_number: values.phone_number });
- };
+ const handleGenerateOtp: SubmitHandler = async (values) => {
+ setCurrentPhoneNumber(values.phone_number);
+ generateOtp({ phone_number: values.phone_number });
+ };
- const handleLoginUser = async (otp: string) => {
- setForceLoader(true);
- loginUser({
- phone_number: formValues.phone_number,
- otp,
- });
- };
+ const handleLoginUser = async (otp: string) => {
+ setForceLoader(true);
+ loginUser({
+ phone_number: currentPhoneNumber,
+ otp,
+ });
+ };
- useEffect(() => {
- loginAnimation();
- }, []);
+ useGSAP(() => {
+ // loginAnimation();
+ }, []);
- useEffect(() => {
- const id = setInterval(() => {
- setTimeToResend((prevTime) => prevTime - 1);
- }, 1000);
+ useEffect(() => {
+ const id = setInterval(() => {
+ setTimeToResend((prevTime) => prevTime - 1);
+ }, 1000);
- setIntervalId(id);
+ setIntervalId(id);
- return () => clearInterval(id);
- }, []);
+ return () => clearInterval(id);
+ }, []);
- if (isLoadingOtp || isLoadingLogin || forceLoader) return ;
+ if (isLoadingLogin || forceLoader || isLoadingLogoPartners || isLoadingFAQ)
+ return ;
- if (isOtpGenerated) {
- return (
- <>
-
- {
- setIsOtpGenerated(false);
- setValue("phone_number", "");
- }}
- cursor="pointer"
- position="absolute"
- left={6}
- />
-
- Connexion
-
-
-
-
- Vous avez reçu un code à 4 chiffres par SMS
-
-
- Saisissez le code envoyé au {addSpaceToTwoCharacters(formValues.phone_number)} pour pouvoir créer votre
- compte
-
-
-
- {
- setHasOtpError(false);
- setHasOtpExpired(false);
- }}
- >
-
-
-
-
-
-
- {
- hasOtpExpired && (
-
- Le code n'est plus valide, cliquez sur le lien ci-dessous pour recevoir un nouveau SMS
-
- )
- }
- {hasOtpError && (
-
- On dirait que ce code n’est pas le bon
-
- )}
-
- {
- if (timeToResend <= 0)
- handleGenerateOtp({ phone_number: formValues.phone_number });
- }}
- >
- Me renvoyer un code par SMS{" "}
- {timeToResend <= 0 ? "" : `(${timeToResend}s)`}
-
-
- >
- );
- }
+ if (isOtpGenerated) {
+ return (
+ <>
+
+ {
+ setIsOtpGenerated(false);
+ setCurrentPhoneNumber("");
+ }}
+ cursor="pointer"
+ position="absolute"
+ left={6}
+ />
+
+ Connexion
+
+
+
+
+ Vous avez reçu un code à 4 chiffres par SMS
+
+
+ Saisissez le code envoyé au{" "}
+ {addSpaceToTwoCharacters(currentPhoneNumber)} pour pouvoir créer
+ votre compte
+
+
+
+ {
+ setHasOtpError(false);
+ setHasOtpExpired(false);
+ }}
+ >
+
+
+
+
+
+
+ {hasOtpExpired && (
+
+ Le code n'est plus valide, cliquez sur le lien ci-dessous pour
+ recevoir un nouveau SMS
+
+ )}
+ {hasOtpError && (
+
+ On dirait que ce code n’est pas le bon
+
+ )}
+
+ {
+ if (timeToResend <= 0)
+ handleGenerateOtp({ phone_number: currentPhoneNumber });
+ }}
+ >
+ Me renvoyer un code par SMS{" "}
+ {timeToResend <= 0 ? "" : `(${timeToResend}s)`}
+
+
+ >
+ );
+ }
- return (
-
-
-
-
-
- Ma carte
-
- jeune engagé
-
-
-
- Connectez-vous avec votre n° de téléphone
-
-
-
-
- J'ai changé de numéro de téléphone
-
-
-
- );
+ return (
+ <>
+
+
+
+
+
+ Des remises exclusives pour les jeunes qui vont commencer la vie
+ active. Avec la carte “jeune engagé”
+
+
+ Les économies pensées pour bien démarrer dans la vie
+
+ et pour toutes ses dépenses quotidiennes.
+
+
+
+
+
+
+
+ Ils vous offrent{" "}
+
+ des remises
+
+
+
+ {logoPartners.map((logo, index) => (
+
+ ))}
+ {logoPartners.map((logo, index) => (
+
+ ))}
+
+
+
+ Et encore plein d’autres...
+
+
+
+ {sectionItems.map((section, index) => (
+
+ ))}
+
+
+
+ Qui peut en profiter ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Comment ça marche ?
+
+
+ Rappel : Vous devez être inscrit en Missions locale dans le
+ “Contrat d’engagement jeune”.
+
+
+
+
+
+
+
+
+
+ Questions fréquentes
+
+
+ {landingFAQ.map(({ title, content }, index) => (
+
+ ))}
+
+
+
+
+ Je profite des réductions{" "}
+
+ dès maintenant
+
+
+ Accédez aux réductions et aux offres des entreprises qui aident
+ les jeunes à se lancer dans la vie active.
+
+
+
+
+
+
+
+
+
+
+
+ Vous êtes éligible !
+
+
+ Téléchargez l’application sur votre téléphone pour continuer.
+
+
+ Scannez le QR code avec l’appareil photo de votre téléphone pour
+ accéder à l’application carte “jeune engagé” sur votre mobile.
+
+
+
+
+
+
+
+
+
+
+
+ {isDesktop && (
+
+
+ Accéder à l’application
+
+
+
+ )}
+ >
+ );
}
diff --git a/webapp/src/payload/globals/LandingFAQ.ts b/webapp/src/payload/globals/LandingFAQ.ts
new file mode 100644
index 00000000..19e08419
--- /dev/null
+++ b/webapp/src/payload/globals/LandingFAQ.ts
@@ -0,0 +1,31 @@
+import { GlobalConfig } from "payload/types";
+
+export const LandingFAQ: GlobalConfig = {
+ slug: "landingFAQ",
+ label: "[Accueil] Foire aux questions",
+ fields: [
+ {
+ name: "items",
+ label: "Questions",
+ labels: {
+ singular: "Question",
+ plural: "Questions",
+ },
+ type: "array",
+ fields: [
+ {
+ name: "title",
+ type: "text",
+ label: "Titre",
+ required: true,
+ },
+ {
+ name: "content",
+ type: "textarea",
+ label: "Contenu",
+ required: true,
+ },
+ ],
+ },
+ ],
+};
diff --git a/webapp/src/payload/globals/LandingPartners.ts b/webapp/src/payload/globals/LandingPartners.ts
new file mode 100644
index 00000000..f7ec3a25
--- /dev/null
+++ b/webapp/src/payload/globals/LandingPartners.ts
@@ -0,0 +1,26 @@
+import { GlobalConfig } from "payload/types";
+
+export const LandingPartners: GlobalConfig = {
+ slug: "landingPartners",
+ label: "[Accueil] Logos des partenaires",
+ fields: [
+ {
+ name: "items",
+ label: "Partenaires",
+ labels: {
+ singular: "Partenaire",
+ plural: "Partenaires",
+ },
+ type: "array",
+ fields: [
+ {
+ name: "partner",
+ type: "relationship",
+ relationTo: "partners",
+ label: "Partenaire",
+ required: true,
+ },
+ ],
+ },
+ ],
+};
diff --git a/webapp/src/payload/migrations/20240308_105148.json b/webapp/src/payload/migrations/20240308_105148.json
new file mode 100644
index 00000000..2d4d2a2c
--- /dev/null
+++ b/webapp/src/payload/migrations/20240308_105148.json
@@ -0,0 +1,2284 @@
+{
+ "id": "820a9856-bffe-437a-a768-c4fdd9a3d400",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "5",
+ "dialect": "pg",
+ "tables": {
+ "admins": {
+ "name": "admins",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "email_idx": {
+ "name": "email_idx",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "civility": {
+ "name": "civility",
+ "type": "enum_users_civility",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "birth_date": {
+ "name": "birth_date",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address": {
+ "name": "address",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "timeAtCEJ": {
+ "name": "timeAtCEJ",
+ "type": "enum_users_time_at_c_e_j",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_email": {
+ "name": "user_email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_image": {
+ "name": "status_image",
+ "type": "enum_users_status_image",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "otp_request_token": {
+ "name": "otp_request_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "phone_number_idx": {
+ "name": "phone_number_idx",
+ "columns": [
+ "phone_number"
+ ],
+ "isUnique": true
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "email_idx": {
+ "name": "email_idx",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "users_rels": {
+ "name": "users_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "categories_id": {
+ "name": "categories_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "users_rels_parent_id_users_id_fk": {
+ "name": "users_rels_parent_id_users_id_fk",
+ "tableFrom": "users_rels",
+ "tableTo": "users",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "users_rels_media_id_media_id_fk": {
+ "name": "users_rels_media_id_media_id_fk",
+ "tableFrom": "users_rels",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "users_rels_categories_id_categories_id_fk": {
+ "name": "users_rels_categories_id_categories_id_fk",
+ "tableFrom": "users_rels",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "categories_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "supervisors": {
+ "name": "supervisors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "email_idx": {
+ "name": "email_idx",
+ "columns": [
+ "email"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "phone_number": {
+ "name": "phone_number",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "phone_number_idx": {
+ "name": "phone_number_idx",
+ "columns": [
+ "phone_number"
+ ],
+ "isUnique": true
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "media": {
+ "name": "media",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "alt": {
+ "name": "alt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filesize": {
+ "name": "filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "filename_idx": {
+ "name": "filename_idx",
+ "columns": [
+ "filename"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "categories": {
+ "name": "categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slug_idx": {
+ "name": "slug_idx",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "categories_rels": {
+ "name": "categories_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "categories_rels_parent_id_categories_id_fk": {
+ "name": "categories_rels_parent_id_categories_id_fk",
+ "tableFrom": "categories_rels",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "categories_rels_media_id_media_id_fk": {
+ "name": "categories_rels_media_id_media_id_fk",
+ "tableFrom": "categories_rels",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "partners": {
+ "name": "partners",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "name_idx": {
+ "name": "name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "partners_rels": {
+ "name": "partners_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "partners_rels_parent_id_partners_id_fk": {
+ "name": "partners_rels_parent_id_partners_id_fk",
+ "tableFrom": "partners_rels",
+ "tableTo": "partners",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "partners_rels_media_id_media_id_fk": {
+ "name": "partners_rels_media_id_media_id_fk",
+ "tableFrom": "partners_rels",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offers_terms_of_use": {
+ "name": "offers_terms_of_use",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_highlighted": {
+ "name": "is_highlighted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "_order_idx": {
+ "name": "_order_idx",
+ "columns": [
+ "_order"
+ ],
+ "isUnique": false
+ },
+ "_parent_id_idx": {
+ "name": "_parent_id_idx",
+ "columns": [
+ "_parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "offers_terms_of_use__parent_id_offers_id_fk": {
+ "name": "offers_terms_of_use__parent_id_offers_id_fk",
+ "tableFrom": "offers_terms_of_use",
+ "tableTo": "offers",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offers_conditions": {
+ "name": "offers_conditions",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "_order_idx": {
+ "name": "_order_idx",
+ "columns": [
+ "_order"
+ ],
+ "isUnique": false
+ },
+ "_parent_id_idx": {
+ "name": "_parent_id_idx",
+ "columns": [
+ "_parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "offers_conditions__parent_id_offers_id_fk": {
+ "name": "offers_conditions__parent_id_offers_id_fk",
+ "tableFrom": "offers_conditions",
+ "tableTo": "offers",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offers": {
+ "name": "offers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "validity_from": {
+ "name": "validity_from",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "validity_to": {
+ "name": "validity_to",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "enum_offers_kind",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "nb_of_eligible_stores": {
+ "name": "nb_of_eligible_stores",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "link_of_eligible_stores": {
+ "name": "link_of_eligible_stores",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "offers_rels": {
+ "name": "offers_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "partners_id": {
+ "name": "partners_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "categories_id": {
+ "name": "categories_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "offers_rels_parent_id_offers_id_fk": {
+ "name": "offers_rels_parent_id_offers_id_fk",
+ "tableFrom": "offers_rels",
+ "tableTo": "offers",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "offers_rels_partners_id_partners_id_fk": {
+ "name": "offers_rels_partners_id_partners_id_fk",
+ "tableFrom": "offers_rels",
+ "tableTo": "partners",
+ "columnsFrom": [
+ "partners_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "offers_rels_categories_id_categories_id_fk": {
+ "name": "offers_rels_categories_id_categories_id_fk",
+ "tableFrom": "offers_rels",
+ "tableTo": "categories",
+ "columnsFrom": [
+ "categories_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "offers_rels_media_id_media_id_fk": {
+ "name": "offers_rels_media_id_media_id_fk",
+ "tableFrom": "offers_rels",
+ "tableTo": "media",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "coupons": {
+ "name": "coupons",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used": {
+ "name": "used",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "assign_user_at": {
+ "name": "assign_user_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "coupons_rels": {
+ "name": "coupons_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "offers_id": {
+ "name": "offers_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "coupons_rels_parent_id_coupons_id_fk": {
+ "name": "coupons_rels_parent_id_coupons_id_fk",
+ "tableFrom": "coupons_rels",
+ "tableTo": "coupons",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "coupons_rels_users_id_users_id_fk": {
+ "name": "coupons_rels_users_id_users_id_fk",
+ "tableFrom": "coupons_rels",
+ "tableTo": "users",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "coupons_rels_offers_id_offers_id_fk": {
+ "name": "coupons_rels_offers_id_offers_id_fk",
+ "tableFrom": "coupons_rels",
+ "tableTo": "offers",
+ "columnsFrom": [
+ "offers_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "savings": {
+ "name": "savings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "amount": {
+ "name": "amount",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "savings_rels": {
+ "name": "savings_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "coupons_id": {
+ "name": "coupons_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "savings_rels_parent_id_savings_id_fk": {
+ "name": "savings_rels_parent_id_savings_id_fk",
+ "tableFrom": "savings_rels",
+ "tableTo": "savings",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "savings_rels_coupons_id_coupons_id_fk": {
+ "name": "savings_rels_coupons_id_coupons_id_fk",
+ "tableFrom": "savings_rels",
+ "tableTo": "coupons",
+ "columnsFrom": [
+ "coupons_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "payload_preferences": {
+ "name": "payload_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "key_idx": {
+ "name": "key_idx",
+ "columns": [
+ "key"
+ ],
+ "isUnique": false
+ },
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "payload_preferences_rels": {
+ "name": "payload_preferences_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "admins_id": {
+ "name": "admins_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supervisors_id": {
+ "name": "supervisors_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "payload_preferences_rels_parent_id_payload_preferences_id_fk": {
+ "name": "payload_preferences_rels_parent_id_payload_preferences_id_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "payload_preferences",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_admins_id_admins_id_fk": {
+ "name": "payload_preferences_rels_admins_id_admins_id_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "admins",
+ "columnsFrom": [
+ "admins_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_users_id_users_id_fk": {
+ "name": "payload_preferences_rels_users_id_users_id_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "users",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_supervisors_id_supervisors_id_fk": {
+ "name": "payload_preferences_rels_supervisors_id_supervisors_id_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "supervisors",
+ "columnsFrom": [
+ "supervisors_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "payload_migrations": {
+ "name": "payload_migrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch": {
+ "name": "batch",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "created_at_idx": {
+ "name": "created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "quick_access_items": {
+ "name": "quick_access_items",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "_order_idx": {
+ "name": "_order_idx",
+ "columns": [
+ "_order"
+ ],
+ "isUnique": false
+ },
+ "_parent_id_idx": {
+ "name": "_parent_id_idx",
+ "columns": [
+ "_parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "quick_access_items__parent_id_quick_access_id_fk": {
+ "name": "quick_access_items__parent_id_quick_access_id_fk",
+ "tableFrom": "quick_access_items",
+ "tableTo": "quick_access",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "quick_access": {
+ "name": "quick_access",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "quick_access_rels": {
+ "name": "quick_access_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "partners_id": {
+ "name": "partners_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "offers_id": {
+ "name": "offers_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "quick_access_rels_parent_id_quick_access_id_fk": {
+ "name": "quick_access_rels_parent_id_quick_access_id_fk",
+ "tableFrom": "quick_access_rels",
+ "tableTo": "quick_access",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quick_access_rels_partners_id_partners_id_fk": {
+ "name": "quick_access_rels_partners_id_partners_id_fk",
+ "tableFrom": "quick_access_rels",
+ "tableTo": "partners",
+ "columnsFrom": [
+ "partners_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quick_access_rels_offers_id_offers_id_fk": {
+ "name": "quick_access_rels_offers_id_offers_id_fk",
+ "tableFrom": "quick_access_rels",
+ "tableTo": "offers",
+ "columnsFrom": [
+ "offers_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "landing_partners_items": {
+ "name": "landing_partners_items",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "_order_idx": {
+ "name": "_order_idx",
+ "columns": [
+ "_order"
+ ],
+ "isUnique": false
+ },
+ "_parent_id_idx": {
+ "name": "_parent_id_idx",
+ "columns": [
+ "_parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "landing_partners_items__parent_id_landing_partners_id_fk": {
+ "name": "landing_partners_items__parent_id_landing_partners_id_fk",
+ "tableFrom": "landing_partners_items",
+ "tableTo": "landing_partners",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "landing_partners": {
+ "name": "landing_partners",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "landing_partners_rels": {
+ "name": "landing_partners_rels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "partners_id": {
+ "name": "partners_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "order_idx": {
+ "name": "order_idx",
+ "columns": [
+ "order"
+ ],
+ "isUnique": false
+ },
+ "parent_idx": {
+ "name": "parent_idx",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ },
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ "path"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "landing_partners_rels_parent_id_landing_partners_id_fk": {
+ "name": "landing_partners_rels_parent_id_landing_partners_id_fk",
+ "tableFrom": "landing_partners_rels",
+ "tableTo": "landing_partners",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "landing_partners_rels_partners_id_partners_id_fk": {
+ "name": "landing_partners_rels_partners_id_partners_id_fk",
+ "tableFrom": "landing_partners_rels",
+ "tableTo": "partners",
+ "columnsFrom": [
+ "partners_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "landing_f_a_q_items": {
+ "name": "landing_f_a_q_items",
+ "schema": "",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "_order_idx": {
+ "name": "_order_idx",
+ "columns": [
+ "_order"
+ ],
+ "isUnique": false
+ },
+ "_parent_id_idx": {
+ "name": "_parent_id_idx",
+ "columns": [
+ "_parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "landing_f_a_q_items__parent_id_landing_f_a_q_id_fk": {
+ "name": "landing_f_a_q_items__parent_id_landing_f_a_q_id_fk",
+ "tableFrom": "landing_f_a_q_items",
+ "tableTo": "landing_f_a_q",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "landing_f_a_q": {
+ "name": "landing_f_a_q",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {
+ "_locales": {
+ "name": "_locales",
+ "values": {
+ "fr": "fr"
+ }
+ },
+ "enum_users_civility": {
+ "name": "enum_users_civility",
+ "values": {
+ "man": "man",
+ "woman": "woman"
+ }
+ },
+ "enum_users_time_at_c_e_j": {
+ "name": "enum_users_time_at_c_e_j",
+ "values": {
+ "started": "started",
+ "lessThan3Months": "lessThan3Months",
+ "moreThan3Months": "moreThan3Months"
+ }
+ },
+ "enum_users_status_image": {
+ "name": "enum_users_status_image",
+ "values": {
+ "pending": "pending",
+ "approved": "approved",
+ "rejected": "rejected"
+ }
+ },
+ "enum_offers_kind": {
+ "name": "enum_offers_kind",
+ "values": {
+ "voucher": "voucher",
+ "voucher_pass": "voucher_pass",
+ "code": "code",
+ "code_space": "code_space"
+ }
+ }
+ },
+ "schemas": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/webapp/src/payload/migrations/20240308_105148.ts b/webapp/src/payload/migrations/20240308_105148.ts
new file mode 100644
index 00000000..60a0cb7c
--- /dev/null
+++ b/webapp/src/payload/migrations/20240308_105148.ts
@@ -0,0 +1,84 @@
+import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'
+import { sql } from 'drizzle-orm'
+
+export async function up({ payload }: MigrateUpArgs): Promise {
+await payload.db.drizzle.execute(sql`
+
+CREATE TABLE IF NOT EXISTS "landing_partners_items" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS "landing_partners" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "updated_at" timestamp(3) with time zone,
+ "created_at" timestamp(3) with time zone
+);
+
+CREATE TABLE IF NOT EXISTS "landing_partners_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "partners_id" integer
+);
+
+CREATE TABLE IF NOT EXISTS "landing_f_a_q_items" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL,
+ "title" varchar NOT NULL,
+ "content" varchar NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS "landing_f_a_q" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "updated_at" timestamp(3) with time zone,
+ "created_at" timestamp(3) with time zone
+);
+
+CREATE INDEX IF NOT EXISTS "_order_idx" ON "landing_partners_items" ("_order");
+CREATE INDEX IF NOT EXISTS "_parent_id_idx" ON "landing_partners_items" ("_parent_id");
+CREATE INDEX IF NOT EXISTS "order_idx" ON "landing_partners_rels" ("order");
+CREATE INDEX IF NOT EXISTS "parent_idx" ON "landing_partners_rels" ("parent_id");
+CREATE INDEX IF NOT EXISTS "path_idx" ON "landing_partners_rels" ("path");
+CREATE INDEX IF NOT EXISTS "_order_idx" ON "landing_f_a_q_items" ("_order");
+CREATE INDEX IF NOT EXISTS "_parent_id_idx" ON "landing_f_a_q_items" ("_parent_id");
+DO $$ BEGIN
+ ALTER TABLE "landing_partners_items" ADD CONSTRAINT "landing_partners_items__parent_id_landing_partners_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "landing_partners"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ ALTER TABLE "landing_partners_rels" ADD CONSTRAINT "landing_partners_rels_parent_id_landing_partners_id_fk" FOREIGN KEY ("parent_id") REFERENCES "landing_partners"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ ALTER TABLE "landing_partners_rels" ADD CONSTRAINT "landing_partners_rels_partners_id_partners_id_fk" FOREIGN KEY ("partners_id") REFERENCES "partners"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ ALTER TABLE "landing_f_a_q_items" ADD CONSTRAINT "landing_f_a_q_items__parent_id_landing_f_a_q_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "landing_f_a_q"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+`);
+
+};
+
+export async function down({ payload }: MigrateDownArgs): Promise {
+await payload.db.drizzle.execute(sql`
+
+DROP TABLE "landing_partners_items";
+DROP TABLE "landing_partners";
+DROP TABLE "landing_partners_rels";
+DROP TABLE "landing_f_a_q_items";
+DROP TABLE "landing_f_a_q";`);
+
+};
diff --git a/webapp/src/payload/payload-types.ts b/webapp/src/payload/payload-types.ts
index 66504a63..cb97c79c 100644
--- a/webapp/src/payload/payload-types.ts
+++ b/webapp/src/payload/payload-types.ts
@@ -23,6 +23,8 @@ export interface Config {
};
globals: {
quickAccess: QuickAccess;
+ landingPartners: LandingPartner;
+ landingFAQ: LandingFAQ;
};
}
/**
@@ -260,6 +262,37 @@ export interface QuickAccess {
updatedAt?: string | null;
createdAt?: string | null;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "landingPartners".
+ */
+export interface LandingPartner {
+ id: number;
+ items?:
+ | {
+ partner: number | Partner;
+ id?: string | null;
+ }[]
+ | null;
+ updatedAt?: string | null;
+ createdAt?: string | null;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "landingFAQ".
+ */
+export interface LandingFAQ {
+ id: number;
+ items?:
+ | {
+ title: string;
+ content: string;
+ id?: string | null;
+ }[]
+ | null;
+ updatedAt?: string | null;
+ createdAt?: string | null;
+}
declare module 'payload' {
diff --git a/webapp/src/payload/payload.config.ts b/webapp/src/payload/payload.config.ts
index 1a6227e1..d394a16c 100644
--- a/webapp/src/payload/payload.config.ts
+++ b/webapp/src/payload/payload.config.ts
@@ -7,6 +7,8 @@ import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";
import { QuickAccess } from "./globals/QuickAccess";
+import { LandingPartners } from "./globals/LandingPartners";
+import { LandingFAQ } from "./globals/LandingFAQ";
import { Admins } from "./collections/Admin";
import { Users } from "./collections/User";
@@ -74,7 +76,7 @@ export default buildConfig({
locales: ["fr"],
defaultLocale: "fr",
},
- globals: [QuickAccess],
+ globals: [QuickAccess, LandingPartners, LandingFAQ],
typescript: {
outputFile: path.resolve(__dirname, "./payload-types.ts"),
},
diff --git a/webapp/src/providers/Auth.tsx b/webapp/src/providers/Auth.tsx
index db0dc643..b7eb4113 100644
--- a/webapp/src/providers/Auth.tsx
+++ b/webapp/src/providers/Auth.tsx
@@ -16,6 +16,8 @@ export interface BeforeInstallPromptEvent extends Event {
type AuthContext = {
user: UserIncluded | null;
setUser: (user: UserIncluded | null) => void;
+ isOtpGenerated: boolean;
+ setIsOtpGenerated: (isOtpGenerated: boolean) => void;
showing: boolean;
setShowing: (showing: boolean) => void;
deferredEvent: BeforeInstallPromptEvent | null;
@@ -30,6 +32,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
}) => {
const [user, setUser] = useState(null);
+ const [isOtpGenerated, setIsOtpGenerated] = useState(false);
+
const [showing, setShowing] = useState(false);
const [deferredEvent, setDeferredEvent] =
useState(null);
@@ -66,6 +70,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
value={{
user,
setUser,
+ isOtpGenerated,
+ setIsOtpGenerated,
showing,
setShowing,
deferredEvent,
diff --git a/webapp/src/server/api/root.ts b/webapp/src/server/api/root.ts
index 868ae662..c9f2cd1f 100644
--- a/webapp/src/server/api/root.ts
+++ b/webapp/src/server/api/root.ts
@@ -5,7 +5,7 @@ import { categoryRouter } from "./routers/category";
import { offerRouter } from "./routers/offer";
import { couponRouter } from "./routers/coupon";
import { partnerRouter } from "./routers/partner";
-import { quickAccessRouter } from "./routers/quickAccess";
+import { globalsRouter } from "./routers/globals";
import { savingRouter } from "./routers/saving";
import { permissionRouter } from "./routers/permission";
@@ -17,10 +17,10 @@ import { permissionRouter } from "./routers/permission";
export const appRouter = createTRPCRouter({
user: userRouter,
category: categoryRouter,
+ globals: globalsRouter,
offer: offerRouter,
coupon: couponRouter,
partner: partnerRouter,
- quickAccess: quickAccessRouter,
saving: savingRouter,
permission: permissionRouter,
});
diff --git a/webapp/src/server/api/routers/globals.ts b/webapp/src/server/api/routers/globals.ts
new file mode 100644
index 00000000..25be0a7b
--- /dev/null
+++ b/webapp/src/server/api/routers/globals.ts
@@ -0,0 +1,55 @@
+import {
+ createTRPCRouter,
+ publicProcedure,
+ userProtectedProcedure,
+} from "~/server/api/trpc";
+import { PartnerIncluded } from "./partner";
+import { OfferIncluded } from "./offer";
+import { Media } from "~/payload/payload-types";
+
+export const globalsRouter = createTRPCRouter({
+ quickAccessGetAll: userProtectedProcedure.query(async ({ ctx }) => {
+ const quickAccess = await ctx.payload.findGlobal({
+ slug: "quickAccess",
+ depth: 3,
+ });
+
+ const partners = quickAccess.items as {
+ partner: PartnerIncluded;
+ offer: OfferIncluded;
+ id?: string;
+ }[];
+
+ return {
+ data: partners,
+ };
+ }),
+
+ landingPartnersGetLogos: publicProcedure.query(async ({ ctx }) => {
+ const landingPartners = await ctx.payload.findGlobal({
+ slug: "landingPartners",
+ depth: 3,
+ });
+
+ const partners = landingPartners.items?.map(
+ (item) => item.partner
+ ) as PartnerIncluded[];
+
+ const logoPartners = partners.map((partner) => partner.icon);
+
+ return {
+ data: logoPartners,
+ };
+ }),
+
+ landingFAQGetAll: publicProcedure.query(async ({ ctx }) => {
+ const landingFAQ = await ctx.payload.findGlobal({
+ slug: "landingFAQ",
+ depth: 3,
+ });
+
+ return {
+ data: landingFAQ.items,
+ };
+ }),
+});
diff --git a/webapp/src/server/api/routers/quickAccess.ts b/webapp/src/server/api/routers/quickAccess.ts
deleted file mode 100644
index f33d45c2..00000000
--- a/webapp/src/server/api/routers/quickAccess.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { createTRPCRouter, userProtectedProcedure } from "~/server/api/trpc";
-import { PartnerIncluded } from "./partner";
-import { OfferIncluded } from "./offer";
-
-export const quickAccessRouter = createTRPCRouter({
- getAll: userProtectedProcedure.query(async ({ ctx }) => {
- const quickAccess = await ctx.payload.findGlobal({
- slug: "quickAccess",
- depth: 3,
- });
-
- const partners = quickAccess.items as {
- partner: PartnerIncluded;
- offer: OfferIncluded;
- id?: string;
- }[];
-
- return {
- data: partners,
- };
- }),
-});
diff --git a/webapp/src/styles/globals.css b/webapp/src/styles/globals.css
index 05775560..b2dceab2 100644
--- a/webapp/src/styles/globals.css
+++ b/webapp/src/styles/globals.css
@@ -117,6 +117,7 @@ a {
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.05);
+ scroll-behavior: smooth;
}
/* override crisp */
diff --git a/webapp/src/utils/chakra-theme.ts b/webapp/src/utils/chakra-theme.ts
index c2b70543..dcace0c2 100644
--- a/webapp/src/utils/chakra-theme.ts
+++ b/webapp/src/utils/chakra-theme.ts
@@ -3,8 +3,10 @@ import {
StyleFunctionProps,
extendTheme,
theme as defaultTheme,
+ defineStyle,
} from "@chakra-ui/react";
import localFont from "next/font/local";
+import { modalTheme } from "~/components/theme/modal";
export const Marianne = localFont({
src: [
@@ -118,6 +120,7 @@ export const theme = extendTheme({
},
},
},
+ Modal: modalTheme,
},
styles: {
global: () => ({
@@ -125,10 +128,10 @@ export const theme = extendTheme({
height: "100%",
},
body: {
- bg: "bgWhite",
height: "100%",
},
main: {
+ bg: "bgWhite",
height: "100%",
},
"#__next": {
@@ -194,6 +197,11 @@ export const theme = extendTheme({
secondaryText: "#5C5C70",
blackLight: "#20202C",
},
+ shadows: {
+ "landing-phone-number-component":
+ "0px 4px 9.9px 0px rgba(177, 177, 177, 0.25)",
+ "landing-qr-code-desktop": "0px 0px 24.2px 0px rgba(145, 145, 145, 0.25)",
+ },
radii: {
"1.5xl": "1.25rem",
},
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index 25d8122e..019713fc 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -11864,6 +11864,13 @@ __metadata:
languageName: node
linkType: hard
+"lodash.isequal@npm:^4.5.0":
+ version: 4.5.0
+ resolution: "lodash.isequal@npm:4.5.0"
+ checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
+ languageName: node
+ linkType: hard
+
"lodash.isinteger@npm:^4.0.4":
version: 4.0.4
resolution: "lodash.isinteger@npm:4.0.4"
@@ -14411,6 +14418,13 @@ __metadata:
languageName: node
linkType: hard
+"qrcode-generator@npm:^1.4.1":
+ version: 1.4.4
+ resolution: "qrcode-generator@npm:1.4.4"
+ checksum: 10c0/3249fcff98cb9fa17c21329d3dfd895e294a2d6ea48161f7b377010779d41f0cd88668b7fb3478a659725061bb0a770b40a227c2f4853e8c4a6b947a9e8bf17a
+ languageName: node
+ linkType: hard
+
"qs-middleware@npm:1.0.3":
version: 1.0.3
resolution: "qs-middleware@npm:1.0.3"
@@ -14740,6 +14754,19 @@ __metadata:
languageName: node
linkType: hard
+"react-qrcode-logo@npm:^2.9.0":
+ version: 2.9.0
+ resolution: "react-qrcode-logo@npm:2.9.0"
+ dependencies:
+ lodash.isequal: "npm:^4.5.0"
+ qrcode-generator: "npm:^1.4.1"
+ peerDependencies:
+ react: ">=16.4.1"
+ react-dom: ">=16.4.1"
+ checksum: 10c0/d6411069e04f6f9a8433eda3a5f3538682906151858fe53a06d3d26ed05f29a72e88a307d4f37f8419dcf4def63a88fd8340bb56a06ad8b47f5d9c379eaed2d2
+ languageName: node
+ linkType: hard
+
"react-remove-scroll-bar@npm:^2.3.4":
version: 2.3.5
resolution: "react-remove-scroll-bar@npm:2.3.5"
@@ -17165,6 +17192,7 @@ __metadata:
react-easy-crop: "npm:^5.0.5"
react-hook-form: "npm:^7.50.1"
react-icons: "npm:^5.0.0"
+ react-qrcode-logo: "npm:^2.9.0"
sharp: "npm:^0.33.2"
superjson: "npm:^1.13.3"
tsx: "npm:^4.7.0"