feat: ✨ implement toast on contact form success, alerts for vacations, holidays and announcements
- Move address and contact methods to individual components.main
parent
520dd137d8
commit
45d53a9617
@ -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 };
|
||||||
|
};
|
@ -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,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
@ -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<Options<"announcement">, "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,
|
||||||
|
});
|
||||||
|
};
|
@ -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<Options<"holiday">, "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,
|
||||||
|
});
|
||||||
|
};
|
@ -1,7 +1,9 @@
|
|||||||
export * from "./about";
|
export * from "./about";
|
||||||
|
export * from "./announcements";
|
||||||
export * from "./contact";
|
export * from "./contact";
|
||||||
export * from "./gallery";
|
export * from "./gallery";
|
||||||
|
export * from "./holidays";
|
||||||
export * from "./home";
|
export * from "./home";
|
||||||
export * from "./menu";
|
export * from "./menu";
|
||||||
export * from "./settings";
|
export * from "./settings";
|
||||||
|
export * from "./vacations";
|
||||||
|
@ -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<Options<"vacation">, "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,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import { getAbout, getContact } from "@/api";
|
||||||
|
|
||||||
|
import { Box } from "@styled-system/jsx";
|
||||||
|
import { Locale } from "@/i18n/settings";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
|
||||||
|
export default async function Address({ locale }: { locale: Locale }) {
|
||||||
|
const about = await getAbout({ locale });
|
||||||
|
const contact = await getContact({ locale });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text fontWeight="bold">{about.name}</Text>
|
||||||
|
<Text>
|
||||||
|
{contact.address.street} {contact.address.number}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{contact.address.zip} {contact.address.area}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Grid } from "@styled-system/jsx";
|
||||||
|
import { Link } from "@/components/ui/link";
|
||||||
|
import { Locale } from "@/i18n/settings";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { getContact } from "@/api";
|
||||||
|
import { getI18n } from "@/i18n/server";
|
||||||
|
|
||||||
|
export default async function ContactMethods({ locale }: { locale: Locale }) {
|
||||||
|
const t = await getI18n();
|
||||||
|
const contact = await getContact({ locale });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gridTemplateColumns="min-content 1fr" columnGap={2} rowGap={0.5}>
|
||||||
|
{contact.phone && (
|
||||||
|
<>
|
||||||
|
<Text fontWeight="bold">{t("general.phoneNumber")}</Text>
|
||||||
|
<Link href={`tel:${contact.phone}`}>{contact.phone}</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{contact.email && (
|
||||||
|
<>
|
||||||
|
<Text fontWeight="bold">{t("general.email")}</Text>
|
||||||
|
<Link href={`mailto:${contact.email}`}>{contact.email}</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Box, HStack } from "@styled-system/jsx";
|
||||||
|
|
||||||
|
import { Locale } from "@/i18n/settings";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { getI18n } from "@/i18n/server";
|
||||||
|
import { getOpeningTimes } from "@/api/openingTimes";
|
||||||
|
|
||||||
|
export default async function OpeningTimes({ locale }: { locale: Locale }) {
|
||||||
|
const t = await getI18n();
|
||||||
|
const openingTimes = await getOpeningTimes({ locale });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{openingTimes.docs.map((ot) => (
|
||||||
|
<HStack key={ot.id}>
|
||||||
|
<Text w={250}>
|
||||||
|
{t(`days.${ot.from}`)}
|
||||||
|
{ot.to ? ` - ${t(`days.${ot.to}`)}` : ""}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{new Date(ot.timeOpen).toLocaleTimeString("de-CH", { timeStyle: "short" })} -{" "}
|
||||||
|
{new Date(ot.timeClose).toLocaleTimeString("de-CH", { timeStyle: "short" })}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * as Alert from './styled/alert'
|
@ -0,0 +1 @@
|
|||||||
|
export { Link, type LinkProps } from './styled/link'
|
@ -0,0 +1,34 @@
|
|||||||
|
'use client'
|
||||||
|
import type { Assign, PolymorphicProps } from '@ark-ui/react'
|
||||||
|
import { ark } from '@ark-ui/react/factory'
|
||||||
|
import { alert } from 'styled-system/recipes'
|
||||||
|
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
|
||||||
|
import { createStyleContext } from './utils/create-style-context'
|
||||||
|
|
||||||
|
const { withProvider, withContext } = createStyleContext(alert)
|
||||||
|
|
||||||
|
export type RootProps = ComponentProps<typeof Root>
|
||||||
|
export const Root = withProvider<HTMLDivElement, Assign<HTMLStyledProps<'div'>, PolymorphicProps>>(
|
||||||
|
ark.div,
|
||||||
|
'root',
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Content = withContext<
|
||||||
|
HTMLDivElement,
|
||||||
|
Assign<HTMLStyledProps<'div'>, PolymorphicProps>
|
||||||
|
>(ark.div, 'content')
|
||||||
|
|
||||||
|
export const Description = withContext<
|
||||||
|
HTMLDivElement,
|
||||||
|
Assign<HTMLStyledProps<'div'>, PolymorphicProps>
|
||||||
|
>(ark.div, 'description')
|
||||||
|
|
||||||
|
export const Icon = withContext<HTMLOrSVGElement, Assign<HTMLStyledProps<'svg'>, PolymorphicProps>>(
|
||||||
|
ark.svg,
|
||||||
|
'icon',
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Title = withContext<
|
||||||
|
HTMLHeadingElement,
|
||||||
|
Assign<HTMLStyledProps<'h5'>, PolymorphicProps>
|
||||||
|
>(ark.h5, 'title')
|
@ -0,0 +1,7 @@
|
|||||||
|
import { ark } from '@ark-ui/react/factory'
|
||||||
|
import { styled } from 'styled-system/jsx'
|
||||||
|
import { link } from 'styled-system/recipes'
|
||||||
|
import type { ComponentProps } from 'styled-system/types'
|
||||||
|
|
||||||
|
export type LinkProps = ComponentProps<typeof Link>
|
||||||
|
export const Link = styled(ark.a, link)
|
@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
import type { Assign } from '@ark-ui/react'
|
||||||
|
import { Toast } from '@ark-ui/react/toast'
|
||||||
|
import { toast } from 'styled-system/recipes'
|
||||||
|
import type { HTMLStyledProps } from 'styled-system/types'
|
||||||
|
import { createStyleContext } from './utils/create-style-context'
|
||||||
|
|
||||||
|
const { withProvider, withContext } = createStyleContext(toast)
|
||||||
|
|
||||||
|
export const Root = withProvider<
|
||||||
|
HTMLDivElement,
|
||||||
|
Assign<HTMLStyledProps<'div'>, Toast.ActionTriggerProps>
|
||||||
|
>(Toast.Root, 'root')
|
||||||
|
|
||||||
|
export const ActionTrigger = withContext<
|
||||||
|
HTMLButtonElement,
|
||||||
|
Assign<HTMLStyledProps<'button'>, Toast.ActionTriggerProps>
|
||||||
|
>(Toast.ActionTrigger, 'actionTrigger')
|
||||||
|
|
||||||
|
export const CloseTrigger = withContext<
|
||||||
|
HTMLDivElement,
|
||||||
|
Assign<HTMLStyledProps<'div'>, Toast.CloseTriggerProps>
|
||||||
|
>(Toast.CloseTrigger, 'closeTrigger')
|
||||||
|
|
||||||
|
export const Description = withContext<
|
||||||
|
HTMLDivElement,
|
||||||
|
Assign<HTMLStyledProps<'div'>, Toast.DescriptionProps>
|
||||||
|
>(Toast.Description, 'description')
|
||||||
|
|
||||||
|
export const Title = withContext<HTMLDivElement, Assign<HTMLStyledProps<'div'>, Toast.TitleProps>>(
|
||||||
|
Toast.Title,
|
||||||
|
'title',
|
||||||
|
)
|
||||||
|
|
||||||
|
export {
|
||||||
|
ToastContext as Context,
|
||||||
|
Toaster,
|
||||||
|
createToaster,
|
||||||
|
type ToastContextProps as ContextProps,
|
||||||
|
type ToasterProps,
|
||||||
|
} from '@ark-ui/react/toast'
|
@ -0,0 +1 @@
|
|||||||
|
export * as Toast from './styled/toast'
|
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { ReactNode, useContext } from "react";
|
||||||
|
|
||||||
|
import { IconButton } from "@/components/ui/icon-button";
|
||||||
|
import { Toast } from "@/components/ui/toast";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
const toaster = Toast.createToaster({
|
||||||
|
placement: "bottom-end",
|
||||||
|
overlap: true,
|
||||||
|
gap: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ToastContext = React.createContext({ toaster });
|
||||||
|
|
||||||
|
export function ToastContextProvider({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<ToastContext.Provider value={{ toaster }}>
|
||||||
|
<Toast.Toaster toaster={toaster}>
|
||||||
|
{(toast) => (
|
||||||
|
<Toast.Root key={toast.id}>
|
||||||
|
<Toast.Title>{toast.title}</Toast.Title>
|
||||||
|
<Toast.Description>{toast.description}</Toast.Description>
|
||||||
|
<Toast.CloseTrigger asChild>
|
||||||
|
<IconButton size="sm" variant="link">
|
||||||
|
<XIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Toast.CloseTrigger>
|
||||||
|
</Toast.Root>
|
||||||
|
)}
|
||||||
|
</Toast.Toaster>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { ToastContext } from "@/contexts/toast";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
return useContext(ToastContext).toaster;
|
||||||
|
}
|
@ -1,5 +1,13 @@
|
|||||||
import { createI18nServer } from "next-international/server";
|
import { Locale, importedLocales } from "./settings";
|
||||||
import { importedLocales } from "./settings";
|
import { createI18nServer, setStaticParamsLocale } from "next-international/server";
|
||||||
|
|
||||||
export const { getI18n, getScopedI18n, getStaticParams, getCurrentLocale } =
|
export const { getI18n, getScopedI18n, getStaticParams, getCurrentLocale } =
|
||||||
createI18nServer(importedLocales);
|
createI18nServer(importedLocales);
|
||||||
|
|
||||||
|
export const getLocalizedI18n = async (locale: Locale) => {
|
||||||
|
const prevLocale = getCurrentLocale();
|
||||||
|
|
||||||
|
setStaticParamsLocale(locale);
|
||||||
|
|
||||||
|
return [await getI18n(), () => setStaticParamsLocale(prevLocale)] as const;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const contactSchema = z.object({
|
||||||
|
name: z.string().min(4),
|
||||||
|
email: z.string().email(),
|
||||||
|
subject: z.string().min(4),
|
||||||
|
message: z.string().min(10),
|
||||||
|
});
|
@ -0,0 +1,36 @@
|
|||||||
|
import { I18nProviderClient, useI18n } from "@/i18n/client";
|
||||||
|
import { Options, render } from "@react-email/render";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { defaultLocale } from "@/i18n/settings";
|
||||||
|
import { getI18n } from "@/i18n/server";
|
||||||
|
|
||||||
|
type T = Awaited<ReturnType<typeof getI18n>>;
|
||||||
|
|
||||||
|
function I18nWrapper<P extends { t: T }>({
|
||||||
|
component: Component,
|
||||||
|
props,
|
||||||
|
}: {
|
||||||
|
component: React.ComponentType<P>;
|
||||||
|
props: P;
|
||||||
|
}) {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return <Component {...props} t={t} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function i18nProviderWrapper<P extends { t: T }>(Component: React.ComponentType<P>) {
|
||||||
|
return function I18nProviderWrapper(props: P & { locale?: string }) {
|
||||||
|
return (
|
||||||
|
<I18nProviderClient locale={props.locale ?? defaultLocale}>
|
||||||
|
<I18nWrapper component={Component} props={props} />
|
||||||
|
</I18nProviderClient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderEmail<P extends { t: T }>(Component: React.ComponentType<P>) {
|
||||||
|
return async function (props: P, opts?: Options) {
|
||||||
|
return await render(<Component {...props} />, opts);
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue