diff --git a/app/lib/models/lead.server.ts b/app/lib/models/lead.server.ts index d2314c76..26c3bda5 100644 --- a/app/lib/models/lead.server.ts +++ b/app/lib/models/lead.server.ts @@ -30,3 +30,32 @@ export async function registerChallengeLead( }; } } + +export async function registerLead(request: Request, email: string) { + return axios + .post( + `${environment().API_HOST}/leads`, + { + email, + tags: ["lead-assine"], + }, + {}, + ) + .then((res) => { + return { success: "Cadastro realizado com sucesse!" }; + }) + .catch((error) => { + let errorMsg = + "Não foi possível realizar o cadastro. Por favor, tente novamente."; + + if (error.response.status === 409) { + errorMsg = "Esse email já está cadastrado em nossa lista."; + } else if (error.response.status === 422) { + errorMsg = "Por favor, insira um email válido."; + } + + return { + error: errorMsg, + }; + }); +} diff --git a/app/routes/_api/leads/index.tsx b/app/routes/_api/leads/index.tsx new file mode 100644 index 00000000..f29b3c6c --- /dev/null +++ b/app/routes/_api/leads/index.tsx @@ -0,0 +1,42 @@ +import { abort404 } from "~/lib/utils/responses.server"; +import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; +import NotFound from "~/components/features/error-handling/not-found"; +import { Error500 } from "~/components/features/error-handling/500"; +import { registerChallengeLead, registerLead } from "~/lib/models/lead.server"; + +export async function action({ request }: { request: Request }) { + const formData = await request.formData(); + const intent = formData.get("intent") as string; + + const email = formData.get("email") as string; + // const tag = formData.get("tag") as string | undefined; + + switch (intent) { + case "register-lead-assine": + return registerLead(request, email); + case "register-lead-challenge": + return registerChallengeLead(request, email); + } +} + +export async function loader() { + return abort404(); +} + +export function ErrorBoundary() { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + return ( +
+ +
+ ); + } + + return ; +} + +export default function Leads() { + return null; +} diff --git a/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/_layout.tsx b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/_layout.tsx index 9cd34aed..97dc1912 100644 --- a/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/_layout.tsx +++ b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/_layout.tsx @@ -47,8 +47,7 @@ import { SelectValue, } from "~/components/ui/select"; -import MobileSignupForm from "~/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/mobile-signup-form"; -import { registerChallengeLead } from "~/lib/models/lead.server"; +import MobileSignupForm from "./components/mobile-signup-form"; export const meta = ({ data, params }: any) => { // para não quebrar se não houver challenge ainda. @@ -123,10 +122,6 @@ export async function action({ request }: ActionFunctionArgs) { case "requestCertificate": const certifiableId = formData.get("certifiable_id") as string; return requestCertificate(request, "ChallengeUser", certifiableId); - case "register-lead": - const email = formData.get("email") as string; - - return registerChallengeLead(request, email); default: return null; } diff --git a/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/mobile-signup-form.tsx b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/mobile-signup-form.tsx index 2445d19b..080c3545 100644 --- a/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/mobile-signup-form.tsx +++ b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/mobile-signup-form.tsx @@ -1,15 +1,11 @@ -import { useLocation } from "@remix-run/react"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; -import ResponsiveEmailSignup from "~/components/features/auth/responsive-email-signup"; +import ResponsiveEmailSignup from "./responsive-email-signup"; import { useMediaQuery } from "~/lib/hooks/use-media-query"; import type { User } from "~/lib/models/user.server"; function MobileSignupForm({ user }: { user: User | null }) { const isMobile = useMediaQuery("(max-width: 768px)"); - const location = useLocation(); - - const slug = location.pathname.split("mini-projetos/")[1]; const [hideOnMobile, setHideOnMobile] = useState(false); useEffect(() => { @@ -29,7 +25,7 @@ function MobileSignupForm({ user }: { user: User | null }) { }} className="w-full bottom-0 h-20 z-20 bg-gradient-to-tr animate-bg from-background-50 to-background-100 border-background-100 dark:from-background-700 dark:to-background-900 fixed shadow-lg border-t dark:border-background-700 flex items-center justify-center" > - + ) ); diff --git a/app/components/features/auth/responsive-email-signup/index.tsx b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/responsive-email-signup.tsx similarity index 58% rename from app/components/features/auth/responsive-email-signup/index.tsx rename to app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/responsive-email-signup.tsx index 5979f06e..3c921b76 100644 --- a/app/components/features/auth/responsive-email-signup/index.tsx +++ b/app/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/responsive-email-signup.tsx @@ -1,27 +1,38 @@ -import { - Form, - useActionData, - useNavigation, - useSubmit, -} from "@remix-run/react"; +import { useActionData, useFetcher } from "@remix-run/react"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { ResponsiveDialog } from "~/components/ui/responsive-dialog"; import LoadingButton from "~/components/features/form/loading-button"; +import { useEffect } from "react"; +import { useToasterWithSound } from "~/lib/hooks/useToasterWithSound"; -export default function ResponsiveEmailSignup({ slug }: { slug: string }) { - const submit = useSubmit(); +interface FetcherData { + error?: string; + success?: string; +} + +export default function ResponsiveEmailSignup() { const errors = useActionData(); - const transition = useNavigation(); + const fetcher = useFetcher(); + const { showSuccessToast, showErrorToast } = useToasterWithSound(); - const status = transition.state; + const status = fetcher.state; let isSuccessfulSubmission = status === "idle" && errors === null; + const fetcherData = fetcher.data as FetcherData; + const errorMsg = fetcherData && fetcherData.error ? fetcherData.error : null; + const successMsg = + fetcherData && fetcherData.success ? fetcherData.success : null; + + useEffect(() => { + if (successMsg) showSuccessToast(successMsg); + if (errorMsg) showErrorToast(errorMsg); + }, [successMsg, errorMsg, showErrorToast, showSuccessToast]); + async function handleSubmit(event: React.FormEvent) { - submit(event.currentTarget, { + fetcher.submit(event.currentTarget.form, { method: "post", - action: `/mini-projetos/${slug}`, }); } @@ -33,7 +44,12 @@ export default function ResponsiveEmailSignup({ slug }: { slug: string }) { triggerButtonSize="lg" > <> -
+ @@ -51,12 +67,12 @@ export default function ResponsiveEmailSignup({ slug }: { slug: string }) { status={status} isSuccessfulSubmission={isSuccessfulSubmission} name="intent" - value="register-lead" + value="register-lead-challenge" > Receber instruções - +
); diff --git a/app/routes/_layout-raw/assine.index/components/responsive-dialog-assine.tsx b/app/routes/_layout-raw/assine.index/components/responsive-dialog-assine.tsx new file mode 100644 index 00000000..57c7eb43 --- /dev/null +++ b/app/routes/_layout-raw/assine.index/components/responsive-dialog-assine.tsx @@ -0,0 +1,126 @@ +import * as React from "react"; +import { Button } from "~/components/ui/button"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "~/components/ui/drawer"; +import { useMediaQuery } from "~/lib/hooks/use-media-query"; + +interface ResponsiveDialogProps { + children: React.ReactNode; + triggerLabel?: string; + title: string; + description: string; + drawerCancelLabel?: string; + open?: boolean; + triggerClassName?: string; + onOpenChange?: (open: boolean) => void; +} + +export function ResponsiveDialog({ + children, + title, + description, + drawerCancelLabel = "Cancelar", + open, + onOpenChange, +}: ResponsiveDialogProps) { + const isDesktop = useMediaQuery("(min-width: 768px)"); + + const scroll = () => { + const section = document.querySelector("#price-card"); + section?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + if (isDesktop) { + return ( + { + if (!isOpen) { + scroll(); + } + if (onOpenChange) { + onOpenChange(isOpen); + } + }} + > + + + + + + {title} + {description} + + {children} + + + ); + } + + return ( + { + if (!isOpen) { + scroll(); + } + if (onOpenChange) { + onOpenChange(isOpen); + } + }} + > + + + + + + {title} + {description} + +
{children}
+ + + + {" "} + +
+
+ ); +} diff --git a/app/routes/_layout-raw/assine.index/components/responsive-email-signup.tsx b/app/routes/_layout-raw/assine.index/components/responsive-email-signup.tsx new file mode 100644 index 00000000..87013ca9 --- /dev/null +++ b/app/routes/_layout-raw/assine.index/components/responsive-email-signup.tsx @@ -0,0 +1,89 @@ +import { useActionData, useFetcher } from "@remix-run/react"; + +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { ResponsiveDialog } from "./responsive-dialog-assine"; +import LoadingButton from "~/components/features/form/loading-button"; +import { useEffect } from "react"; +import { useToasterWithSound } from "~/lib/hooks/useToasterWithSound"; + +interface FetcherData { + error?: string; + success?: string; +} + +export default function ResponsiveEmailSignup() { + const errors = useActionData(); + const fetcher = useFetcher(); + const { showSuccessToast, showErrorToast } = useToasterWithSound(); + + const status = fetcher.state; + let isSuccessfulSubmission = status === "idle" && errors === null; + + const fetcherData = fetcher.data as FetcherData; + const errorMsg = fetcherData && fetcherData.error ? fetcherData.error : null; + const successMsg = + fetcherData && fetcherData.success ? fetcherData.success : null; + + // const scrollFunction = () => { + // const section = document.querySelector("#price-card"); + // section?.scrollIntoView({ behavior: "smooth", block: "start" }); + // }; + + useEffect(() => { + if (successMsg) showSuccessToast(successMsg); + if (errorMsg) showErrorToast(errorMsg); + }, [successMsg, errorMsg, showErrorToast, showSuccessToast]); + + async function handleSubmit(event: React.FormEvent) { + fetcher.submit(event.currentTarget.form, { + method: "post", + }); + } + + return ( + +
+ + + + +
+
+ Ao cadastrar você receberá informações sobre o Codante pelo email. + Não se preocupe, não vamos enviar spam. +
+
+
+ + Cadastrar + +
+
+
+ {/* */} +
+ ); +} diff --git a/app/routes/_layout-raw/assine.index/index.tsx b/app/routes/_layout-raw/assine.index/index.tsx index 062a1449..beba0e91 100644 --- a/app/routes/_layout-raw/assine.index/index.tsx +++ b/app/routes/_layout-raw/assine.index/index.tsx @@ -2,6 +2,7 @@ import { json, redirect } from "@remix-run/node"; import { Link, isRouteErrorResponse, + useFetcher, useLoaderData, useRouteError, } from "@remix-run/react"; @@ -19,7 +20,7 @@ import { } from "~/components/ui/cards/pricing/pricing-data"; import { useColorMode } from "~/lib/contexts/color-mode-context"; import UserAvatar from "~/components/ui/user-avatar"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"; import SubmissionCard from "~/routes/_layout-app/_mini-projetos/mini-projetos_.$slug_/components/submission-card"; import { ProgressivePracticeContent } from "./components/progressive-practice"; @@ -38,6 +39,11 @@ import axios from "axios"; import type { AxiosError } from "axios"; import type { Subscription } from "~/lib/models/subscription.server"; import { environment } from "~/lib/models/environment"; +import { Card, CardContent, CardTitle } from "~/components/ui/cards/card"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import toast from "react-hot-toast"; +import ResponsiveEmailSignup from "./components/responsive-email-signup"; export const loader = async () => { return json({ @@ -126,25 +132,75 @@ export function ErrorBoundary() { } function CodanteProButton() { - const scroll = () => { - const section = document.querySelector("#pricing"); - section?.scrollIntoView({ behavior: "smooth", block: "start" }); - }; + // const scroll = () => { + // const section = document.querySelector("#price-card"); + // section?.scrollIntoView({ behavior: "smooth", block: "start" }); + // }; + + return ; +} + +interface FetcherData { + error?: string; + success?: string; +} + +function LeadForm() { + const fetcher = useFetcher(); + const emailRef = useRef(null); + + const isSubmittingOrLoading = + fetcher.state === "submitting" || fetcher.state === "loading"; + + function handleLeadClickButton() { + if (emailRef?.current?.value) { + fetcher.submit( + { intent: "register-lead", email: emailRef.current.value }, + { + method: "post", + action: "/leads?index", + }, + ); + } + } + + const fetcherData = fetcher.data as FetcherData; + const errorMsg = fetcherData && fetcherData.error ? fetcherData.error : null; + const successMsg = + fetcherData && fetcherData.success ? fetcherData.success : null; + // if (successMsg) toast.success("E-mail cadastrado com sucesso!"); + + useEffect(() => { + if (successMsg) toast.success("E-mail cadastrado com sucesso!"); + }, [successMsg]); return ( - + + Quero receber novidades! + +

+ Cadastre seu e-mail e fique por dentro das nossas últimas + atualizações.{" "} +

+ +
+ {

{errorMsg || " "}

} +
+ +
+
); } @@ -390,6 +446,7 @@ function Headline() { ))} + ); @@ -1819,6 +1876,7 @@ function Pricing() { hidden: { opacity: 0.5, y: -10 }, }} className="text-4xl font-light font-lexend text-center" + id="price-card" > Preço{" "} atual