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 "./announcements";
|
||||
export * from "./contact";
|
||||
export * from "./gallery";
|
||||
export * from "./holidays";
|
||||
export * from "./home";
|
||||
export * from "./menu";
|
||||
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 { importedLocales } from "./settings";
|
||||
import { Locale, importedLocales } from "./settings";
|
||||
import { createI18nServer, setStaticParamsLocale } from "next-international/server";
|
||||
|
||||
export const { getI18n, getScopedI18n, getStaticParams, getCurrentLocale } =
|
||||
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