From 35bfa0061bdec29f9b4426b10e1cd96c9c4c6a4f Mon Sep 17 00:00:00 2001 From: RaviAnand Mohabir Date: Tue, 27 Aug 2024 10:53:35 +0200 Subject: [PATCH] feat: :sparkles: implement contact and reservation forms with skeleton server actions --- src/actions/contact.ts | 12 ++ src/actions/reservation.ts | 12 ++ .../[locale]/contact/contact-form.tsx | 38 ++++ .../[locale]/contact/date-time-picker.tsx | 171 ++++++++++++++++++ src/app/(frontend)/[locale]/contact/page.tsx | 131 ++++++++------ .../[locale]/contact/reservation-form.tsx | 45 +++++ src/components/ui/date-picker.tsx | 1 + src/components/ui/field.tsx | 1 + src/components/ui/form-label.tsx | 1 + src/components/ui/input.tsx | 1 + src/components/ui/number-input.tsx | 52 ++++++ src/components/ui/styled/date-picker.tsx | 137 ++++++++++++++ src/components/ui/styled/field.tsx | 49 +++++ src/components/ui/styled/form-label.tsx | 7 + src/components/ui/styled/input.tsx | 7 + src/components/ui/styled/number-input.tsx | 57 ++++++ src/i18n/de.ts | 8 + src/i18n/en.ts | 11 ++ 18 files changed, 690 insertions(+), 51 deletions(-) create mode 100644 src/actions/contact.ts create mode 100644 src/actions/reservation.ts create mode 100644 src/app/(frontend)/[locale]/contact/contact-form.tsx create mode 100644 src/app/(frontend)/[locale]/contact/date-time-picker.tsx create mode 100644 src/app/(frontend)/[locale]/contact/reservation-form.tsx create mode 100644 src/components/ui/date-picker.tsx create mode 100644 src/components/ui/field.tsx create mode 100644 src/components/ui/form-label.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/number-input.tsx create mode 100644 src/components/ui/styled/date-picker.tsx create mode 100644 src/components/ui/styled/field.tsx create mode 100644 src/components/ui/styled/form-label.tsx create mode 100644 src/components/ui/styled/input.tsx create mode 100644 src/components/ui/styled/number-input.tsx diff --git a/src/actions/contact.ts b/src/actions/contact.ts new file mode 100644 index 0000000..119e192 --- /dev/null +++ b/src/actions/contact.ts @@ -0,0 +1,12 @@ +"use server"; + +export const submitContactFormAction = (formData: FormData) => { + console.log(JSON.stringify(formData, null, 2)); + console.log(formData.get("message")); + console.log(formData.get("subject")); + console.log(formData.get("date")); + console.log(formData.get("time")); + console.log(formData.get("name")); + console.log(formData.get("email")); + console.log(formData.get("phone")); +}; diff --git a/src/actions/reservation.ts b/src/actions/reservation.ts new file mode 100644 index 0000000..83e2edd --- /dev/null +++ b/src/actions/reservation.ts @@ -0,0 +1,12 @@ +"use server"; + +export const submitReservationFormAction = (formData: FormData) => { + console.log(JSON.stringify(formData, null, 2)); + console.log(formData.get("message")); + console.log(formData.get("subject")); + console.log(formData.get("date")); + console.log(formData.get("time")); + console.log(formData.get("name")); + console.log(formData.get("email")); + console.log(formData.get("phone")); +}; diff --git a/src/app/(frontend)/[locale]/contact/contact-form.tsx b/src/app/(frontend)/[locale]/contact/contact-form.tsx new file mode 100644 index 0000000..d025660 --- /dev/null +++ b/src/app/(frontend)/[locale]/contact/contact-form.tsx @@ -0,0 +1,38 @@ +import { HStack, styled } from "@styled-system/jsx"; + +import { Button } from "@/components/ui/button"; +import { Field } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { getI18n } from "@/i18n/server"; +import { stack } from "@styled-system/patterns"; +import { submitContactFormAction } from "@/actions/contact"; + +export default async function ContactForm() { + const t = await getI18n(); + + return ( + + + {t("contact.name")} + + + + {t("general.email")} + + + + + + {t("contact.subject")} + + + + {t("contact.message")} + + + + + + + ); +} diff --git a/src/app/(frontend)/[locale]/contact/date-time-picker.tsx b/src/app/(frontend)/[locale]/contact/date-time-picker.tsx new file mode 100644 index 0000000..3460e31 --- /dev/null +++ b/src/app/(frontend)/[locale]/contact/date-time-picker.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { HStack, Stack } from "@styled-system/jsx"; + +import { Button } from "@/components/ui/button"; +import { DatePicker } from "@/components/ui/date-picker"; +import { Field } from "@/components/ui/field"; +import { FormLabel } from "@/components/ui/form-label"; +import { IconButton } from "@/components/ui/icon-button"; +import { Input } from "@/components/ui/input"; +import { NumberInput } from "@/components/ui/number-input"; +import { useI18n } from "@/i18n/client"; + +export default function DateTimePicker() { + const t = useI18n(); + + return ( + + + {t("general.date")} + + + + + + + + + + + + {(api) => ( + <> + + + + + + + + + + + + + + + + + + + {api.weekDays.map((weekDay, id) => ( + + {weekDay.narrow} + + ))} + + + + {api.weeks.map((week, id) => ( + + {week.map((day, id) => ( + + + {day.day} + + + ))} + + ))} + + + + )} + + + + + {(api) => ( + <> + + + + + + + + + + + + + + + + + + {api.getMonthsGrid({ columns: 4, format: "short" }).map((months, id) => ( + + {months.map((month, id) => ( + + + + + + ))} + + ))} + + + + )} + + + + + {(api) => ( + <> + + + + + + + + + + + + + + + + + + {api.getYearsGrid({ columns: 4 }).map((years, id) => ( + + {years.map((year, id) => ( + + + + + + ))} + + ))} + + + + )} + + + + + + + {t("general.time")} + + + + + + ); +} diff --git a/src/app/(frontend)/[locale]/contact/page.tsx b/src/app/(frontend)/[locale]/contact/page.tsx index 0b45961..72952a0 100644 --- a/src/app/(frontend)/[locale]/contact/page.tsx +++ b/src/app/(frontend)/[locale]/contact/page.tsx @@ -1,8 +1,13 @@ import { Box, HStack, Stack } from "@styled-system/jsx"; import { getAbout, getContact } from "@/api"; +import ContactForm from "@/app/(frontend)/[locale]/contact/contact-form"; +import { Field } from "@/components/ui/field"; import { Heading } from "@/components/ui/heading"; +import { Input } from "@/components/ui/input"; import { Params } from "../shared"; +import ReservationForm from "@/app/(frontend)/[locale]/contact/reservation-form"; +import { Tabs } from "@/components/ui/tabs"; import { Text } from "@/components/ui/text"; import { getI18n } from "@/i18n/server"; import { getOpeningTimes } from "@/api/openingTimes"; @@ -15,65 +20,89 @@ export default async function Contact({ params: { locale } }: { params: Params } const openingTimes = await getOpeningTimes({ locale }); return ( - + {t("general.contact")} - - - {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("contact.reservation")} + {t("general.contact")} + + + + + + + + + - - - {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} + + {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" })} + - )} - {contact.email && ( - - {t("general.email")} - {contact.email} - - )} + ))} + + + + + {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} + + )} + + - - + + ); } diff --git a/src/app/(frontend)/[locale]/contact/reservation-form.tsx b/src/app/(frontend)/[locale]/contact/reservation-form.tsx new file mode 100644 index 0000000..36e642b --- /dev/null +++ b/src/app/(frontend)/[locale]/contact/reservation-form.tsx @@ -0,0 +1,45 @@ +import { HStack, Stack } from "@styled-system/jsx"; + +import { Button } from "@/components/ui/button"; +import DateTimePicker from "./date-time-picker"; +import { Field } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { getI18n } from "@/i18n/server"; +import { stack } from "@styled-system/patterns"; +import { styled } from "@styled-system/jsx"; +import { submitReservationFormAction } from "@/actions/reservation"; + +export default async function ReservationForm() { + const t = await getI18n(); + + return ( + + + {t("contact.name")} + + + + {t("general.email")} + + + + + + {t("general.phoneNumber")} + + + + + {t("reservation.guests")} + + + + {t("contact.message")} + + + + + + + ); +} diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx new file mode 100644 index 0000000..cb55f2c --- /dev/null +++ b/src/components/ui/date-picker.tsx @@ -0,0 +1 @@ +export * as DatePicker from './styled/date-picker' diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx new file mode 100644 index 0000000..fdff2b6 --- /dev/null +++ b/src/components/ui/field.tsx @@ -0,0 +1 @@ +export * as Field from './styled/field' diff --git a/src/components/ui/form-label.tsx b/src/components/ui/form-label.tsx new file mode 100644 index 0000000..ff63600 --- /dev/null +++ b/src/components/ui/form-label.tsx @@ -0,0 +1 @@ +export { FormLabel, type FormLabelProps } from './styled/form-label' diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..e11a0a4 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1 @@ +export { Input, type InputProps } from './styled/input' diff --git a/src/components/ui/number-input.tsx b/src/components/ui/number-input.tsx new file mode 100644 index 0000000..e3112d9 --- /dev/null +++ b/src/components/ui/number-input.tsx @@ -0,0 +1,52 @@ +import { forwardRef } from 'react' +import * as StyledNumberInput from './styled/number-input' + +export interface NumberInputProps extends StyledNumberInput.RootProps {} + +export const NumberInput = forwardRef((props, ref) => { + const { children, ...rootProps } = props + return ( + + {children && {children}} + + + + + + + + + + + ) +}) + +NumberInput.displayName = 'NumberInput' + +const ChevronUpIcon = () => ( + + Chevron Up Icon + + +) + +const ChevronDownIcon = () => ( + + Chevron Down Icon + + +) diff --git a/src/components/ui/styled/date-picker.tsx b/src/components/ui/styled/date-picker.tsx new file mode 100644 index 0000000..431f45f --- /dev/null +++ b/src/components/ui/styled/date-picker.tsx @@ -0,0 +1,137 @@ +'use client' +import type { Assign } from '@ark-ui/react' +import { DatePicker } from '@ark-ui/react/date-picker' +import { type DatePickerVariantProps, datePicker } from 'styled-system/recipes' +import type { ComponentProps, HTMLStyledProps } from 'styled-system/types' +import { createStyleContext } from './utils/create-style-context' + +const { withProvider, withContext } = createStyleContext(datePicker) + +export type RootProviderProps = ComponentProps +export const RootProvider = withProvider< + HTMLDivElement, + Assign, DatePicker.RootProviderBaseProps>, DatePickerVariantProps> +>(DatePicker.RootProvider, 'root') + +export type RootProps = ComponentProps +export const Root = withProvider< + HTMLDivElement, + Assign, DatePicker.RootBaseProps>, DatePickerVariantProps> +>(DatePicker.Root, 'root') + +export const ClearTrigger = withContext< + HTMLButtonElement, + Assign, DatePicker.ClearTriggerBaseProps> +>(DatePicker.ClearTrigger, 'clearTrigger') + +export const Content = withContext< + HTMLDivElement, + Assign, DatePicker.ContentBaseProps> +>(DatePicker.Content, 'content') + +export const Control = withContext< + HTMLDivElement, + Assign, DatePicker.ControlBaseProps> +>(DatePicker.Control, 'control') + +export const Input = withContext< + HTMLInputElement, + Assign, DatePicker.InputBaseProps> +>(DatePicker.Input, 'input') + +export const Label = withContext< + HTMLLabelElement, + Assign, DatePicker.LabelBaseProps> +>(DatePicker.Label, 'label') + +export const MonthSelect = withContext< + HTMLSelectElement, + Assign, DatePicker.MonthSelectBaseProps> +>(DatePicker.MonthSelect, 'monthSelect') + +export const NextTrigger = withContext< + HTMLButtonElement, + Assign, DatePicker.NextTriggerBaseProps> +>(DatePicker.NextTrigger, 'nextTrigger') + +export const Positioner = withContext< + HTMLDivElement, + Assign, DatePicker.PositionerBaseProps> +>(DatePicker.Positioner, 'positioner') + +export const PresetTrigger = withContext< + HTMLButtonElement, + Assign, DatePicker.PresetTriggerBaseProps> +>(DatePicker.PresetTrigger, 'presetTrigger') + +export const PrevTrigger = withContext< + HTMLButtonElement, + Assign, DatePicker.PrevTriggerBaseProps> +>(DatePicker.PrevTrigger, 'prevTrigger') + +export const RangeText = withContext< + HTMLDivElement, + Assign, DatePicker.RangeTextBaseProps> +>(DatePicker.RangeText, 'rangeText') + +export const TableBody = withContext< + HTMLTableSectionElement, + Assign, DatePicker.TableBodyBaseProps> +>(DatePicker.TableBody, 'tableBody') + +export const TableCell = withContext< + HTMLTableCellElement, + Assign, DatePicker.TableCellBaseProps> +>(DatePicker.TableCell, 'tableCell') + +export const TableCellTrigger = withContext< + HTMLDivElement, + Assign, DatePicker.TableCellTriggerBaseProps> +>(DatePicker.TableCellTrigger, 'tableCellTrigger') + +export const TableHead = withContext< + HTMLTableSectionElement, + Assign, DatePicker.TableHeadBaseProps> +>(DatePicker.TableHead, 'tableHead') + +export const TableHeader = withContext< + HTMLTableCellElement, + Assign, DatePicker.TableHeaderBaseProps> +>(DatePicker.TableHeader, 'tableHeader') + +export const Table = withContext< + HTMLTableElement, + Assign, DatePicker.TableBaseProps> +>(DatePicker.Table, 'table') + +export const TableRow = withContext< + HTMLTableRowElement, + Assign, DatePicker.TableRowBaseProps> +>(DatePicker.TableRow, 'tableRow') + +export const Trigger = withContext< + HTMLButtonElement, + Assign, DatePicker.TriggerBaseProps> +>(DatePicker.Trigger, 'trigger') + +export const ViewControl = withContext< + HTMLDivElement, + Assign, DatePicker.ViewControlBaseProps> +>(DatePicker.ViewControl, 'viewControl') + +export const View = withContext< + HTMLDivElement, + Assign, DatePicker.ViewBaseProps> +>(DatePicker.View, 'view') + +export const ViewTrigger = withContext< + HTMLButtonElement, + Assign, DatePicker.ViewTriggerBaseProps> +>(DatePicker.ViewTrigger, 'viewTrigger') + +export const YearSelect = withContext< + HTMLSelectElement, + Assign, DatePicker.YearSelectBaseProps> +>(DatePicker.YearSelect, 'yearSelect') + +export { DatePickerContext as Context } from '@ark-ui/react/date-picker' diff --git a/src/components/ui/styled/field.tsx b/src/components/ui/styled/field.tsx new file mode 100644 index 0000000..bad505b --- /dev/null +++ b/src/components/ui/styled/field.tsx @@ -0,0 +1,49 @@ +'use client' +import type { Assign } from '@ark-ui/react' +import { Field } from '@ark-ui/react/field' +import { styled } from 'styled-system/jsx' +import { type FieldVariantProps, field, input, textarea } from 'styled-system/recipes' +import type { ComponentProps, HTMLStyledProps } from 'styled-system/types' +import { createStyleContext } from './utils/create-style-context' + +const { withProvider, withContext } = createStyleContext(field) + +export type RootProviderProps = ComponentProps +export const RootProvider = withProvider< + HTMLDivElement, + Assign, Field.RootProviderBaseProps>, FieldVariantProps> +>(Field.RootProvider, 'root') + +export type RootProps = ComponentProps +export const Root = withProvider< + HTMLDivElement, + Assign, Field.RootBaseProps>, FieldVariantProps> +>(Field.Root, 'root') + +export const ErrorText = withContext< + HTMLSpanElement, + Assign, Field.ErrorTextBaseProps> +>(Field.ErrorText, 'errorText') + +export const HelperText = withContext< + HTMLSpanElement, + Assign, Field.HelperTextBaseProps> +>(Field.HelperText, 'helperText') + +export const Label = withContext< + HTMLLabelElement, + Assign, Field.LabelBaseProps> +>(Field.Label, 'label') + +export const Select = withContext< + HTMLSelectElement, + Assign, Field.SelectBaseProps> +>(Field.Select, 'select') + +export type InputProps = ComponentProps +export const Input = styled(Field.Input, input) + +export type TextareaProps = ComponentProps +export const Textarea = styled(Field.Textarea, textarea) + +export { FieldContext as Context } from '@ark-ui/react/field' diff --git a/src/components/ui/styled/form-label.tsx b/src/components/ui/styled/form-label.tsx new file mode 100644 index 0000000..cf1afdc --- /dev/null +++ b/src/components/ui/styled/form-label.tsx @@ -0,0 +1,7 @@ +import { ark } from '@ark-ui/react/factory' +import { styled } from 'styled-system/jsx' +import { formLabel } from 'styled-system/recipes' +import type { ComponentProps } from 'styled-system/types' + +export type FormLabelProps = ComponentProps +export const FormLabel = styled(ark.label, formLabel) diff --git a/src/components/ui/styled/input.tsx b/src/components/ui/styled/input.tsx new file mode 100644 index 0000000..1994157 --- /dev/null +++ b/src/components/ui/styled/input.tsx @@ -0,0 +1,7 @@ +import { ark } from '@ark-ui/react/factory' +import { styled } from 'styled-system/jsx' +import { input } from 'styled-system/recipes' +import type { ComponentProps } from 'styled-system/types' + +export type InputProps = ComponentProps +export const Input = styled(ark.input, input) diff --git a/src/components/ui/styled/number-input.tsx b/src/components/ui/styled/number-input.tsx new file mode 100644 index 0000000..2fe74df --- /dev/null +++ b/src/components/ui/styled/number-input.tsx @@ -0,0 +1,57 @@ +'use client' +import type { Assign } from '@ark-ui/react' +import { NumberInput } from '@ark-ui/react/number-input' +import { type NumberInputVariantProps, numberInput } from 'styled-system/recipes' +import type { ComponentProps, HTMLStyledProps } from 'styled-system/types' +import { createStyleContext } from './utils/create-style-context' + +const { withProvider, withContext } = createStyleContext(numberInput) + +export type RootProviderProps = ComponentProps +export const RootProvider = withProvider< + HTMLDivElement, + Assign, NumberInput.RootProviderBaseProps>, NumberInputVariantProps> +>(NumberInput.RootProvider, 'root') + +export type RootProps = ComponentProps +export const Root = withProvider< + HTMLDivElement, + Assign, NumberInput.RootBaseProps>, NumberInputVariantProps> +>(NumberInput.Root, 'root') + +export const Control = withContext< + HTMLDivElement, + Assign, NumberInput.ControlBaseProps> +>(NumberInput.Control, 'control') + +export const DecrementTrigger = withContext< + HTMLButtonElement, + Assign, NumberInput.DecrementTriggerBaseProps> +>(NumberInput.DecrementTrigger, 'decrementTrigger') + +export const IncrementTrigger = withContext< + HTMLButtonElement, + Assign, NumberInput.IncrementTriggerBaseProps> +>(NumberInput.IncrementTrigger, 'incrementTrigger') + +export const Input = withContext< + HTMLInputElement, + Assign, NumberInput.InputBaseProps> +>(NumberInput.Input, 'input') + +export const Label = withContext< + HTMLLabelElement, + Assign, NumberInput.LabelBaseProps> +>(NumberInput.Label, 'label') + +export const Scrubber = withContext< + HTMLDivElement, + Assign, NumberInput.ScrubberBaseProps> +>(NumberInput.Scrubber, 'scrubber') + +export const ValueText = withContext< + HTMLSpanElement, + Assign, NumberInput.ValueTextBaseProps> +>(NumberInput.ValueText, 'valueText') + +export { NumberInputContext as Context } from '@ark-ui/react/number-input' diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 60b6fa9..828963b 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -8,6 +8,8 @@ export default { phoneNumber: "Telefonnummer", email: "E-Mail", openingTimes: "Öffnungszeiten", + date: "Datum", + time: "Zeit", en: "Englisch", de: "Deutsch", fr: "Französisch", @@ -26,4 +28,10 @@ export default { dogsAllowed: "Hunde erlaubt", fumoire: "Fumoire verfügbar", }, + contact: { + name: "Name", + subject: "Betreff", + message: "Nachricht", + reservation: "Reservation", + }, } as const; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 91f7973..758368b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -8,6 +8,8 @@ export default { email: "Email", openingTimes: "Opening Times", phoneNumber: "Phone", + date: "Date", + time: "Time", en: "English", de: "German", fr: "French", @@ -26,4 +28,13 @@ export default { dogsAllowed: "Dogs allowed", fumoire: "Fumoire available", }, + contact: { + name: "Name", + subject: "Subject", + message: "Message", + reservation: "Reservation", + }, + reservation: { + guests: "Guests", + }, } as const;