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 = () => (
+
+)
+
+const ChevronDownIcon = () => (
+
+)
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;