From 45d53a96173e22dad6c914ea574f6d8a1ce32219 Mon Sep 17 00:00:00 2001 From: RaviAnand Mohabir Date: Tue, 27 Aug 2024 22:49:44 +0200 Subject: [PATCH] feat: :sparkles: implement toast on contact form success, alerts for vacations, holidays and announcements - Move address and contact methods to individual components. --- panda.config.ts | 45 ++++++++++ src/actions/contact.ts | 45 ++++++++++ src/actions/contact.tsx | 53 ----------- .../{reservation.tsx => reservation.ts} | 0 src/api/announcements.ts | 23 +++++ src/api/holidays.ts | 23 +++++ src/api/index.ts | 4 +- src/api/vacations.ts | 23 +++++ .../[locale]/contact/contact-form.tsx | 49 ++++++++--- src/app/(frontend)/[locale]/contact/page.tsx | 87 +++++++------------ src/app/(frontend)/[locale]/layout.tsx | 17 ++-- src/app/(frontend)/[locale]/page.tsx | 85 +++++++++++++++--- src/collections/Announcement.ts | 2 + src/collections/Holiday.ts | 2 + src/collections/Vacation.ts | 2 + src/components/general/address.tsx | 22 +++++ src/components/general/contact-methods.tsx | 28 ++++++ src/components/general/opening-times.tsx | 28 ++++++ src/components/layout/footer.tsx | 34 ++++---- src/components/layout/mobile-nav.tsx | 5 +- src/components/layout/navbar.tsx | 7 +- src/components/ui/alert.tsx | 1 + src/components/ui/link.tsx | 1 + src/components/ui/styled/alert.tsx | 34 ++++++++ src/components/ui/styled/link.tsx | 7 ++ src/components/ui/styled/toast.tsx | 41 +++++++++ src/components/ui/toast.tsx | 1 + src/contexts/toast.tsx | 38 ++++++++ src/emails/contact-confirmation.tsx | 36 ++------ src/emails/contact.tsx | 36 ++++---- src/globals/Settings.ts | 2 + src/hooks/toast.ts | 6 ++ src/i18n/de.ts | 3 + src/i18n/en.ts | 3 + src/i18n/server.ts | 12 ++- src/payload-types.ts | 40 +++++++-- src/payload.config.ts | 2 + src/types/contact.ts | 8 ++ src/utils/email.tsx | 36 ++++++++ 39 files changed, 674 insertions(+), 217 deletions(-) create mode 100644 src/actions/contact.ts delete mode 100644 src/actions/contact.tsx rename src/actions/{reservation.tsx => reservation.ts} (100%) create mode 100644 src/api/announcements.ts create mode 100644 src/api/holidays.ts create mode 100644 src/api/vacations.ts create mode 100644 src/components/general/address.tsx create mode 100644 src/components/general/contact-methods.tsx create mode 100644 src/components/general/opening-times.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/link.tsx create mode 100644 src/components/ui/styled/alert.tsx create mode 100644 src/components/ui/styled/link.tsx create mode 100644 src/components/ui/styled/toast.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/contexts/toast.tsx create mode 100644 src/hooks/toast.ts create mode 100644 src/types/contact.ts create mode 100644 src/utils/email.tsx diff --git a/panda.config.ts b/panda.config.ts index b7fd848..cacce99 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -18,6 +18,51 @@ export default defineConfig({ // Files to exclude exclude: [], + patterns: { + extend: { + scrollable: { + description: "A container that allows for scrolling", + defaultValues: { + direction: "vertical", + hideScrollbar: false, + }, + properties: { + // The direction of the scroll + direction: { type: "enum", value: ["horizontal", "vertical"] }, + // Whether to hide the scrollbar + hideScrollbar: { type: "boolean" }, + }, + // disallow the `overflow` property (in TypeScript) + blocklist: ["overflow"], + transform(props) { + const { direction, hideScrollbar, ...rest } = props; + return { + overflow: "auto", + height: direction === "horizontal" ? "100%" : "auto", + width: direction === "vertical" ? "100%" : "auto", + scrollbarWidth: hideScrollbar ? "none" : "auto", + WebkitOverflowScrolling: "touch", + "&::-webkit-scrollbar": { + display: hideScrollbar ? "none" : "auto", + width: 2, + }, + "&::-webkit-scrollbar-track": { + background: "{colors.gray.a8}", + }, + "&::-webkit-scrollbar-thumb": { + background: "{colors.accent.a10}", + borderRadius: "md", + }, + "&::-webkit-scrollbar-thumb:hover": { + background: "{colors.accent.10}", + }, + ...rest, + }; + }, + }, + }, + }, + // Useful for theme customization theme: {}, diff --git a/src/actions/contact.ts b/src/actions/contact.ts new file mode 100644 index 0000000..4a16168 --- /dev/null +++ b/src/actions/contact.ts @@ -0,0 +1,45 @@ +"use server"; + +import { getI18n, getLocalizedI18n } from "@/i18n/server"; + +import { contactSchema } from "@/types/contact"; +import { getPayload } from "@/utils/payload"; +import { getSettings } from "@/api"; +import { renderContactConfirmationEmail } from "@/emails/contact-confirmation"; +import { renderContactEmail } from "@/emails/contact"; + +export const submitContactFormAction = async (_: any, formData: FormData) => { + const { success, error, data } = contactSchema.safeParse(Object.fromEntries(formData)); + + if (!success) { + return { success, error: error.flatten().fieldErrors }; + } + + const payload = await getPayload(); + const { adminLanguage, contactEmailsTo } = await getSettings(); + const [t, reset] = await getLocalizedI18n(adminLanguage); + + try { + let res = await payload.sendEmail({ + to: contactEmailsTo, + subject: t("email.contactSubject", { name: data.name }), + email: await renderContactEmail({ + t, + ...data, + }), + }); + + res = await payload.sendEmail({ + to: data.email, + subject: t("email.contactConfirmationSubject"), + email: await renderContactConfirmationEmail({ + t, + ...data, + }), + }); + } finally { + reset(); + } + + return { success: true }; +}; diff --git a/src/actions/contact.tsx b/src/actions/contact.tsx deleted file mode 100644 index a5f65db..0000000 --- a/src/actions/contact.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use server"; - -import { getI18n } from "@/i18n/server"; -import { getPayload } from "@/utils/payload"; -import { getSettings } from "@/api"; -import { renderContactConfirmationEmail } from "@/emails/contact-confirmation"; -import { renderContactEmail } from "@/emails/contact"; - -export const submitContactFormAction = async (formData: FormData) => { - const payload = await getPayload(); - const { adminLanguage, contactEmailsTo } = await getSettings(); - const t = await getI18n(); - - console.log( - await renderContactEmail( - { - name: formData.get("name"), - email: formData.get("email"), - subject: formData.get("subject"), - message: formData.get("message"), - locale: adminLanguage, - }, - { plainText: true }, - ), - ); - - await payload.sendEmail({ - to: contactEmailsTo, - subject: t("email.contactSubject", { name: formData.get("name") }), - email: await renderContactEmail( - { - name: formData.get("name"), - email: formData.get("email"), - subject: formData.get("subject"), - message: formData.get("message"), - locale: adminLanguage, - }, - { plainText: true }, - ), - }); - - await payload.sendEmail({ - to: formData.get("email"), - subject: `Bestätigung Ihrer Kontaktanfrage ${formData.get("name")}`, - email: await renderContactConfirmationEmail({ - name: formData.get("name"), - email: formData.get("email"), - subject: formData.get("subject"), - message: formData.get("message"), - locale: adminLanguage, - }), - }); -}; diff --git a/src/actions/reservation.tsx b/src/actions/reservation.ts similarity index 100% rename from src/actions/reservation.tsx rename to src/actions/reservation.ts diff --git a/src/api/announcements.ts b/src/api/announcements.ts new file mode 100644 index 0000000..50f3f2a --- /dev/null +++ b/src/api/announcements.ts @@ -0,0 +1,23 @@ +import type { Options } from "node_modules/payload/dist/collections/operations/local/find"; +import { getPayload } from "@/utils/payload"; + +export const getCurrentAnnouncements = async ( + opts: Omit, "collection" | "sort" | "pagination">, +) => { + const payload = await getPayload(); + + const today = new Date(); + + return await payload.find({ + collection: "announcement", + sort: "from", + where: { + or: [ + { from: { equals: today } }, + { and: [{ from: { less_than_equal: today } }, { to: { greater_than_equal: today } }] }, + ], + }, + pagination: false, + ...opts, + }); +}; diff --git a/src/api/holidays.ts b/src/api/holidays.ts new file mode 100644 index 0000000..570a928 --- /dev/null +++ b/src/api/holidays.ts @@ -0,0 +1,23 @@ +import type { Options } from "node_modules/payload/dist/collections/operations/local/find"; +import { getPayload } from "@/utils/payload"; + +export const getCurrentHolidays = async ( + opts: Omit, "collection" | "sort" | "pagination">, +) => { + const payload = await getPayload(); + + const today = new Date(); + + return await payload.find({ + collection: "holiday", + sort: "from", + where: { + or: [ + { from: { equals: today } }, + { and: [{ from: { less_than_equal: today } }, { to: { greater_than_equal: today } }] }, + ], + }, + pagination: false, + ...opts, + }); +}; diff --git a/src/api/index.ts b/src/api/index.ts index f6ffa84..968a009 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,9 @@ export * from "./about"; +export * from "./announcements"; export * from "./contact"; export * from "./gallery"; +export * from "./holidays"; export * from "./home"; export * from "./menu"; export * from "./settings"; - +export * from "./vacations"; diff --git a/src/api/vacations.ts b/src/api/vacations.ts new file mode 100644 index 0000000..1fc7153 --- /dev/null +++ b/src/api/vacations.ts @@ -0,0 +1,23 @@ +import type { Options } from "node_modules/payload/dist/collections/operations/local/find"; +import { getPayload } from "@/utils/payload"; + +export const getCurrentVacations = async ( + opts: Omit, "collection" | "sort" | "pagination">, +) => { + const payload = await getPayload(); + + const today = new Date(); + + return await payload.find({ + collection: "vacation", + sort: "from", + where: { + or: [ + { from: { equals: today } }, + { and: [{ from: { less_than_equal: today } }, { to: { greater_than_equal: today } }] }, + ], + }, + pagination: false, + ...opts, + }); +}; diff --git a/src/app/(frontend)/[locale]/contact/contact-form.tsx b/src/app/(frontend)/[locale]/contact/contact-form.tsx index d025660..e8f07e4 100644 --- a/src/app/(frontend)/[locale]/contact/contact-form.tsx +++ b/src/app/(frontend)/[locale]/contact/contact-form.tsx @@ -1,37 +1,64 @@ -import { HStack, styled } from "@styled-system/jsx"; +"use client"; + +import { HStack, Stack, styled } from "@styled-system/jsx"; +import { useActionState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Field } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; -import { getI18n } from "@/i18n/server"; +import { Text } from "@/components/ui/text"; import { stack } from "@styled-system/patterns"; import { submitContactFormAction } from "@/actions/contact"; +import { useI18n } from "@/i18n/client"; +import { useToast } from "@/hooks/toast"; + +const hasError = (error: string[] | undefined) => error !== undefined && error.length > 0; + +export default function ContactForm() { + const [state, formAction, pending] = useActionState(submitContactFormAction, null); + const t = useI18n(); + const toaster = useToast(); -export default async function ContactForm() { - const t = await getI18n(); + useEffect(() => { + if (state?.success) { + toaster.create({ + title: t("contact.messageSent"), + description: t("contact.messageSentDescription"), + }); + } + }, [state, toaster]); return ( - - + + {t("contact.name")} + {state?.error?.name?.[0]} - + {t("general.email")} + {state?.error?.email?.[0]} - + {t("contact.subject")} + {state?.error?.subject?.[0]} - + {t("contact.message")} - + + {state?.error?.message?.[0]} - + + + {state?.success && Nachricht gesendet} + ); diff --git a/src/app/(frontend)/[locale]/contact/page.tsx b/src/app/(frontend)/[locale]/contact/page.tsx index 0956ebe..504a4c8 100644 --- a/src/app/(frontend)/[locale]/contact/page.tsx +++ b/src/app/(frontend)/[locale]/contact/page.tsx @@ -1,21 +1,30 @@ -import { Box, HStack, Stack } from "@styled-system/jsx"; +import { Box, Grid, HStack, Stack } from "@styled-system/jsx"; import { getAbout, getContact } from "@/api"; +import Address from "@/components/general/address"; import ContactForm from "./contact-form"; +import ContactMethods from "@/components/general/contact-methods"; import { Heading } from "@/components/ui/heading"; +import { Link } from "@/components/ui/link"; +import OpeningTimes from "@/components/general/opening-times"; import { Params } from "../shared"; import ReservationForm from "./reservation-form"; import { Tabs } from "@/components/ui/tabs"; import { Text } from "@/components/ui/text"; import { getI18n } from "@/i18n/server"; -import { getOpeningTimes } from "@/api/openingTimes"; import { styled } from "@styled-system/jsx"; -export default async function Contact({ params: { locale } }: { params: Params }) { +export default async function Contact({ + params: { locale }, + searchParams: { tab = "contact" }, +}: { + params: Params; + searchParams: { tab?: "reservation" | "contact" }; +}) { + console.log(tab); const t = await getI18n(); const about = await getAbout({ locale }); const contact = await getContact({ locale }); - const openingTimes = await getOpeningTimes({ locale }); return ( @@ -24,80 +33,42 @@ export default async function Contact({ params: { locale } }: { params: Params } - + - {t("contact.reservation")} {t("general.contact")} + {t("contact.reservation")} - - - + + + - + {t("general.openingTimes")} - {openingTimes.docs.map((ot) => ( - - - {t(`days.${ot.from}`)} - {ot.to ? `- ${t(`days.${ot.to}`)}` : ""} - - - {new Date(ot.timeOpen).toLocaleTimeString("de-CH", { timeStyle: "short" })} -{" "} - {new Date(ot.timeClose).toLocaleTimeString("de-CH", { timeStyle: "short" })} - - - ))} + {t("general.address")} - - {contact.address.embeddedMaps && ( - - )} - - - {about.name} - - {contact.address.street} {contact.address.number} - - - {contact.address.zip} {contact.address.area} - - - {contact.phone && ( - - {t("general.phoneNumber")} - {contact.phone} - - )} - {contact.email && ( - - {t("general.email")} - {contact.email} - - )} - + +
+ + {contact.address.embeddedMaps && ( + + )} diff --git a/src/app/(frontend)/[locale]/layout.tsx b/src/app/(frontend)/[locale]/layout.tsx index 9706faf..f798d0b 100644 --- a/src/app/(frontend)/[locale]/layout.tsx +++ b/src/app/(frontend)/[locale]/layout.tsx @@ -7,6 +7,7 @@ import { I18nProviderClient } from "@/i18n/client"; import { Metadata } from "next"; import Navbar from "@/components/layout/navbar"; import { Params } from "./shared"; +import { ToastContextProvider } from "@/contexts/toast"; import { getAbout } from "@/api"; import localFont from "next/font/local"; import { styled } from "@styled-system/jsx"; @@ -26,13 +27,15 @@ export default async function RootLayout({ return ( - - - - {children} - -