@public-ui/sample-react 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.eslintignore +2 -1
  2. package/dist/1474.js +1 -1
  3. package/dist/1531.js +2 -0
  4. package/dist/183.js +1 -1
  5. package/dist/1932.js +2 -0
  6. package/dist/2337.js +1 -1
  7. package/dist/2412.js +1 -1
  8. package/dist/3059.js +2 -0
  9. package/dist/3303.js +1 -1
  10. package/dist/3325.js +1 -1
  11. package/dist/3459.js +1 -1
  12. package/dist/3537.js +1 -1
  13. package/dist/4021.js +1 -1
  14. package/dist/4291.js +1 -1
  15. package/dist/4323.js +1 -1
  16. package/dist/4355.js +1 -1
  17. package/dist/4477.js +1 -1
  18. package/dist/4564.js +1 -1
  19. package/dist/4891.js +1 -1
  20. package/dist/5183.js +1 -1
  21. package/dist/5369.js +1 -1
  22. package/dist/5390.js +1 -1
  23. package/dist/540.js +1 -1
  24. package/dist/5866.js +1 -1
  25. package/dist/6012.js +1 -1
  26. package/dist/6068.js +1 -1
  27. package/dist/6210.js +1 -1
  28. package/dist/6320.js +1 -1
  29. package/dist/6558.js +1 -1
  30. package/dist/6655.js +1 -1
  31. package/dist/6908.js +1 -1
  32. package/dist/7029.js +1 -1
  33. package/dist/7255.js +1 -1
  34. package/dist/7447.js +1 -1
  35. package/dist/7715.js +1 -1
  36. package/dist/7722.js +1 -1
  37. package/dist/7801.js +1 -1
  38. package/dist/7955.js +1 -1
  39. package/dist/7995.js +1 -1
  40. package/dist/8065.js +1 -1
  41. package/dist/8099.js +1 -1
  42. package/dist/8111.js +1 -1
  43. package/dist/8255.js +1 -1
  44. package/dist/8291.js +1 -1
  45. package/dist/8709.js +1 -1
  46. package/dist/8761.js +1 -1
  47. package/dist/9106.js +1 -1
  48. package/dist/9118.js +2 -0
  49. package/dist/9734.js +1 -1
  50. package/dist/9747.js +1 -1
  51. package/dist/9792.js +1 -1
  52. package/dist/9963.js +1 -1
  53. package/dist/main.css +1 -1
  54. package/dist/main.js +8653 -39
  55. package/dist/main.js.LICENSE.txt +9 -0
  56. package/package.json +7 -5
  57. package/src/components/FormWrap.tsx +9 -0
  58. package/src/components/modal/basic.tsx +1 -1
  59. package/src/scenarios/appointment-form/AppointmentForm.tsx +127 -0
  60. package/src/scenarios/appointment-form/AvailableAppointmentsForm.tsx +118 -0
  61. package/src/scenarios/appointment-form/DistrictForm.tsx +80 -0
  62. package/src/scenarios/appointment-form/ErrorList.tsx +35 -0
  63. package/src/scenarios/appointment-form/PersonalInformationForm.tsx +157 -0
  64. package/src/scenarios/appointment-form/Summary.tsx +55 -0
  65. package/src/scenarios/appointment-form/appointmentService.ts +37 -0
  66. package/src/scenarios/complex-form/common/form/component.tsx +25 -0
  67. package/src/scenarios/complex-form/common/form/types.ts +13 -0
  68. package/src/scenarios/complex-form/component.tsx +163 -0
  69. package/src/scenarios/complex-form/kopfdaten/component.tsx +50 -0
  70. package/src/scenarios/complex-form/location/component.tsx +16 -0
  71. package/src/scenarios/complex-form/location/location.form.ts +22 -0
  72. package/src/scenarios/complex-form/schedule/component.tsx +16 -0
  73. package/src/scenarios/complex-form/schedule/schedule.form.ts +34 -0
  74. package/src/scenarios/routes.ts +10 -0
  75. package/src/shares/routes.ts +8 -6
  76. package/dist/3153.js +0 -2
  77. package/dist/4436.js +0 -2
  78. package/dist/4864.js +0 -2
  79. package/dist/5293.js +0 -2
  80. /package/dist/{3153.js.LICENSE.txt → 1531.js.LICENSE.txt} +0 -0
  81. /package/dist/{4436.js.LICENSE.txt → 1932.js.LICENSE.txt} +0 -0
  82. /package/dist/{4864.js.LICENSE.txt → 3059.js.LICENSE.txt} +0 -0
  83. /package/dist/{5293.js.LICENSE.txt → 9118.js.LICENSE.txt} +0 -0
@@ -55,3 +55,12 @@
55
55
  *
56
56
  * @license MIT
57
57
  */
58
+
59
+ /** @license React v16.13.1
60
+ * react-is.production.min.js
61
+ *
62
+ * Copyright (c) Facebook, Inc. and its affiliates.
63
+ *
64
+ * This source code is licensed under the MIT license found in the
65
+ * LICENSE file in the root directory of this source tree.
66
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@public-ui/sample-react",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "This app contains samples for the KoliBri/Public UI",
5
5
  "license": "EUPL-1.2",
6
6
  "dependencies": {
@@ -8,9 +8,9 @@
8
8
  "@leanup/stack-react": "1.3.48",
9
9
  "@leanup/stack-webpack": "1.3.48",
10
10
  "@public-oss/kolibri-themes": "0.0.3",
11
- "@public-ui/components": "1.7.1",
12
- "@public-ui/react": "1.7.1",
13
- "@public-ui/themes": "1.7.1",
11
+ "@public-ui/components": "1.7.2",
12
+ "@public-ui/react": "1.7.2",
13
+ "@public-ui/themes": "1.7.2",
14
14
  "@types/node": "20.8.0",
15
15
  "@types/react": "18.2.23",
16
16
  "@types/react-dom": "18.2.8",
@@ -21,6 +21,7 @@
21
21
  "cpy-cli": "5.0.0",
22
22
  "eslint-plugin-jsx-a11y": "6.7.1",
23
23
  "eslint-plugin-react": "7.33.2",
24
+ "formik": "2.4.5",
24
25
  "nightwatch-axe-verbose": "2.2.2",
25
26
  "npm-run-all": "4.1.5",
26
27
  "react": "18.2.0",
@@ -30,7 +31,8 @@
30
31
  "rimraf": "3.0.2",
31
32
  "ts-prune": "0.10.3",
32
33
  "typescript": "5.2.2",
33
- "world_countries_lists": "2.8.2"
34
+ "world_countries_lists": "2.8.2",
35
+ "yup": "1.3.1"
34
36
  },
35
37
  "files": [
36
38
  ".eslintignore",
@@ -13,6 +13,15 @@ export const FormWrap: FC<FormWrapProps> = (props) => (
13
13
  <FocusElement {...props} />
14
14
  <div className="flex gap-4">
15
15
  <KolButton _label="Submit" _icons="codicon codicon-arrow-right" _type="submit" _variant="primary" />
16
+ <KolButton
17
+ _label="Bunte Icons"
18
+ _icons={{
19
+ left: { icon: 'codicon codicon-heart-filled', style: { color: '#cc006e' } },
20
+ right: { icon: 'codicon codicon-squirrel', style: { color: '#b41b1b' } },
21
+ }}
22
+ _type="submit"
23
+ _variant="secondary"
24
+ />
16
25
  <KolButton _label="Reset" _type="reset" _variant="tertiary" />
17
26
  <KolButton _label="Help" _type="button" _variant="ghost" />
18
27
  </div>
@@ -6,7 +6,7 @@ export const ModalBasic: FC = () => {
6
6
 
7
7
  return (
8
8
  <div>
9
- <KolModal _ariaLabel="" _width="80%" ref={modalElement}>
9
+ <KolModal _ariaLabel="" _width="80%" ref={modalElement} _on={{ onClose: () => console.log('Modal closed') }}>
10
10
  <KolCard _heading="Ich bin ein Modal" style={{ width: '100%' }}>
11
11
  <div slot="content">
12
12
  <KolButton
@@ -0,0 +1,127 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { KolTabs } from '@public-ui/react';
3
+ import { DistrictForm } from './DistrictForm';
4
+ import { Summary } from './Summary';
5
+ import { PersonalInformationForm } from './PersonalInformationForm';
6
+ import { Formik, FormikHelpers } from 'formik';
7
+ import * as Yup from 'yup';
8
+ import { AvailableAppointmentsForm } from './AvailableAppointmentsForm';
9
+ import { Iso8601 } from '@public-ui/components';
10
+ import { checkAppointmentAvailability } from './appointmentService';
11
+
12
+ // export interface FormProps {}
13
+ export interface FormValues {
14
+ district: string;
15
+ date: Iso8601;
16
+ time: Iso8601;
17
+ salutation: string;
18
+ name: string;
19
+ company: string;
20
+ email: string;
21
+ phone: string;
22
+ }
23
+
24
+ enum FormSection {
25
+ DISTRICT,
26
+ AVAILABLE_APPOINTMENTS,
27
+ PERSONAL_INFORMATION,
28
+ SUMMARY,
29
+ }
30
+
31
+ const formSectionSequence = [FormSection.DISTRICT, FormSection.AVAILABLE_APPOINTMENTS, FormSection.PERSONAL_INFORMATION, FormSection.SUMMARY] as const;
32
+
33
+ const initialValues: FormValues = {
34
+ district: '',
35
+ date: '' as Iso8601,
36
+ time: '' as Iso8601,
37
+ salutation: '',
38
+ name: '',
39
+ company: '',
40
+ email: '',
41
+ phone: '',
42
+ };
43
+
44
+ const districtSchema = {
45
+ district: Yup.string().required('Bitte Stadtteil wählen.'),
46
+ };
47
+ const personalInformationSchema = {
48
+ salutation: Yup.string().required('Bitte Anrede auswählen.'),
49
+ name: Yup.string().required('Bitte Name eingeben.'),
50
+ company: Yup.string().when('salutation', {
51
+ is: (salutation: string) => salutation === 'Firma',
52
+ then: (schema) => schema.required('Bitte Firmenname angeben.'),
53
+ }),
54
+ email: Yup.string().required('Bitte E-Mail-Adresse eingeben.'),
55
+ };
56
+ const availableAppointmentsSchema = {
57
+ date: Yup.string().required('Bitte Datum eingeben.'),
58
+ time: Yup.string().when('date', {
59
+ is: (date: string) => Boolean(date), // only validate time when date is already set
60
+ then: (schema) => schema.test('checkTimeAvailability', 'Termin leider nicht mehr verfügbar.', checkAppointmentAvailability),
61
+ }),
62
+ };
63
+
64
+ export function AppointmentForm() {
65
+ const [activeFormSection, setActiveFormSection] = useState(FormSection.DISTRICT);
66
+ const [selectedTab, setSelectedTab] = useState(activeFormSection);
67
+
68
+ const validationSchema = Yup.object().shape({
69
+ ...(activeFormSection === FormSection.DISTRICT ? districtSchema : {}),
70
+ ...(activeFormSection === FormSection.AVAILABLE_APPOINTMENTS ? availableAppointmentsSchema : {}),
71
+ ...(activeFormSection === FormSection.PERSONAL_INFORMATION ? personalInformationSchema : {}),
72
+ });
73
+
74
+ useEffect(() => {
75
+ setSelectedTab(activeFormSection);
76
+ }, [activeFormSection]);
77
+
78
+ const handleSubmit = async (_values: FormValues, formik: FormikHelpers<FormValues>) => {
79
+ console.log(_values, formik);
80
+ const currentSectionIndex = formSectionSequence.indexOf(activeFormSection);
81
+ const nextSection = formSectionSequence[currentSectionIndex + 1];
82
+ if (nextSection !== undefined) {
83
+ await formik.setTouched({});
84
+ setTimeout(() => setActiveFormSection(nextSection), 1000);
85
+ }
86
+ };
87
+
88
+ return (
89
+ <Formik<FormValues> initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}>
90
+ <KolTabs
91
+ _tabs={[
92
+ {
93
+ _label: '1. Einwohnermeldeamt wählen',
94
+ },
95
+ {
96
+ _label: '2. Freie Termine',
97
+ _disabled: activeFormSection < FormSection.AVAILABLE_APPOINTMENTS,
98
+ },
99
+ {
100
+ _label: '3. Persönliche Daten',
101
+ _disabled: activeFormSection < FormSection.PERSONAL_INFORMATION,
102
+ },
103
+ {
104
+ _label: 'Zusammenfassung',
105
+ _disabled: activeFormSection < FormSection.SUMMARY,
106
+ },
107
+ ]}
108
+ _label="Formular-Navigation"
109
+ _selected={selectedTab}
110
+ _on={{ onSelect: (_event, selectedTab) => setActiveFormSection(selectedTab) }}
111
+ >
112
+ <div>
113
+ <DistrictForm />
114
+ </div>
115
+ <div>
116
+ <AvailableAppointmentsForm />
117
+ </div>
118
+ <div>
119
+ <PersonalInformationForm />
120
+ </div>
121
+ <div>
122
+ <Summary />
123
+ </div>
124
+ </KolTabs>
125
+ </Formik>
126
+ );
127
+ }
@@ -0,0 +1,118 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { KolButton, KolForm, KolHeading, KolInputDate, KolInputRadio, KolSpin } from '@public-ui/react';
3
+ import { FormValues } from './AppointmentForm';
4
+ import { ErrorList } from './ErrorList';
5
+ import { Field, FieldProps, useFormikContext } from 'formik';
6
+ import { fetchAvailableTimes } from './appointmentService';
7
+ import { Option } from '@public-ui/components/src';
8
+
9
+ export function AvailableAppointmentsForm() {
10
+ const form = useFormikContext<FormValues>();
11
+
12
+ const [sectionSubmitted, setSectionSubmitted] = useState(false);
13
+ const [availableTimes, setAvailableTimes] = useState<Option<string>[] | null>(null);
14
+
15
+ useEffect(() => {
16
+ let ignoreResponse = false;
17
+ setAvailableTimes(null);
18
+
19
+ if (form.values.date) {
20
+ fetchAvailableTimes().then(
21
+ (times) => {
22
+ if (!ignoreResponse) {
23
+ setAvailableTimes(times);
24
+ void form.setFieldValue('time', times[0].value);
25
+ void form.setFieldTouched('time');
26
+ }
27
+ },
28
+ () => {},
29
+ ); // ignore errors
30
+ }
31
+ return () => {
32
+ ignoreResponse = true;
33
+ };
34
+ }, [form.values.date]);
35
+
36
+ return (
37
+ <div className="p-2">
38
+ <KolHeading _level={2} _label="Wählen Sie einen Termin aus"></KolHeading>
39
+
40
+ {sectionSubmitted && Object.keys(form.errors).length ? (
41
+ <div className="mt-2">
42
+ <ErrorList errors={form.errors} />
43
+ </div>
44
+ ) : null}
45
+
46
+ <KolForm
47
+ _on={{
48
+ onSubmit: () => {
49
+ void form.submitForm();
50
+ setSectionSubmitted(true);
51
+ },
52
+ }}
53
+ >
54
+ <Field name="date">
55
+ {({ field }: FieldProps<FormValues['date']>) => (
56
+ <KolInputDate
57
+ id="field-date"
58
+ _label="Datum"
59
+ _value={field.value}
60
+ _error={form.errors.date || ''}
61
+ _touched={form.touched.date}
62
+ _required
63
+ _on={{
64
+ onChange: (event: Event, value: unknown): void => {
65
+ if (event.target) {
66
+ void form.setFieldValue('date', value, true);
67
+ }
68
+ },
69
+ onBlur: () => {
70
+ void form.setFieldTouched('date', true);
71
+ },
72
+ }}
73
+ />
74
+ )}
75
+ </Field>
76
+
77
+ {form.values.date && (
78
+ <div className="grid gap-4 mt-4">
79
+ {availableTimes ? (
80
+ <>
81
+ <Field name="time">
82
+ {({ field }: FieldProps<FormValues['time']>) => (
83
+ <KolInputRadio
84
+ id="field-date"
85
+ _label="Zeit"
86
+ _orientation="horizontal"
87
+ _options={availableTimes}
88
+ _value={field.value}
89
+ _error={form.errors.time || ''}
90
+ _touched={form.touched.time}
91
+ _required
92
+ _on={{
93
+ onChange: (event: Event, value: unknown): void => {
94
+ if (event.target) {
95
+ void form.setFieldTouched('time', true);
96
+ void form.setFieldValue('time', value, true);
97
+ }
98
+ },
99
+ }}
100
+ />
101
+ )}
102
+ </Field>
103
+ <p>
104
+ <em>Aus Testzwecken sind nur die Termine zu jeder halben Stunde verfügbar.</em>
105
+ </p>
106
+ </>
107
+ ) : (
108
+ <KolSpin _show className="block" aria-label="Termine werden geladen." _variant="cycle" />
109
+ )}
110
+ </div>
111
+ )}
112
+
113
+ <KolButton _label="Weiter" _type="submit" className="mt-2" _disabled={form.isValidating} />
114
+ {form.values.date && form.isValidating ? <KolSpin _show aria-label="Termin wird geprüft." /> : ''}
115
+ </KolForm>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,80 @@
1
+ import React, { useState } from 'react';
2
+ import { KolButton, KolForm, KolHeading, KolSelect } from '@public-ui/react';
3
+ import { Field, FieldProps, useFormikContext } from 'formik';
4
+ import { FormValues } from './AppointmentForm';
5
+ import { ErrorList } from './ErrorList';
6
+
7
+ const LOCATION_OPTIONS = [
8
+ {
9
+ value: 'Aplerbeck',
10
+ label: 'Aplerbeck',
11
+ },
12
+ {
13
+ value: 'Brackel',
14
+ label: 'Brackel',
15
+ },
16
+ {
17
+ value: 'Dorstfeld',
18
+ label: 'Dorstfeld',
19
+ },
20
+ {
21
+ value: 'Innenstadt Ost',
22
+ label: 'Innenstadt Ost',
23
+ },
24
+ {
25
+ value: 'Innenstadt West',
26
+ label: 'Innenstadt West',
27
+ },
28
+ ];
29
+
30
+ export function DistrictForm() {
31
+ const form = useFormikContext<FormValues>();
32
+ const [sectionSubmitted, setSectionSubmitted] = useState(false);
33
+
34
+ return (
35
+ <div className="p-2">
36
+ <KolHeading _level={2} _label="Wählen Sie einen Stadtteil aus"></KolHeading>
37
+
38
+ {sectionSubmitted && Object.keys(form.errors).length ? (
39
+ <div className="mt-2">
40
+ <ErrorList errors={form.errors} />
41
+ </div>
42
+ ) : null}
43
+
44
+ <KolForm
45
+ _on={{
46
+ onSubmit: () => {
47
+ void form.submitForm();
48
+ setSectionSubmitted(true);
49
+ },
50
+ }}
51
+ >
52
+ <Field name="district">
53
+ {({ field }: FieldProps<FormValues['district']>) => (
54
+ <KolSelect
55
+ id="field-district"
56
+ _label="Stadtteil"
57
+ _options={[{ label: 'Bitte wählen…', value: '' }, ...LOCATION_OPTIONS]}
58
+ _value={[field.value]}
59
+ _error={form.errors.district || ''}
60
+ _touched={form.touched.district}
61
+ _required
62
+ _on={{
63
+ onChange: (event, values: unknown) => {
64
+ // Select und Radio setzen den Wert immer initial.
65
+ if (event.target) {
66
+ const [value] = values as [FormValues['district']];
67
+ void form.setFieldTouched('district', true);
68
+ void form.setFieldValue('district', value, true);
69
+ }
70
+ },
71
+ }}
72
+ />
73
+ )}
74
+ </Field>
75
+
76
+ <KolButton _label="Weiter" _type="submit" className="mt-2" />
77
+ </KolForm>
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { KolAlert, KolLink } from '@public-ui/react';
3
+
4
+ type ErrorListPropType = {
5
+ errors: Record<string, string>;
6
+ };
7
+
8
+ export function ErrorList({ errors }: ErrorListPropType) {
9
+ const handleLinkClick = (event: Event) => {
10
+ const href = (event.target as HTMLAnchorElement | undefined)?.href;
11
+ if (href) {
12
+ const hrefUrl = new URL(href);
13
+
14
+ const targetElement = document.querySelector<HTMLElement>(hrefUrl.hash);
15
+ if (targetElement && typeof targetElement.focus === 'function') {
16
+ targetElement.focus();
17
+ }
18
+ }
19
+ };
20
+
21
+ return (
22
+ <KolAlert _type="error" _variant="msg">
23
+ Bitte korrigieren Sie folgende Fehler:
24
+ <nav aria-label="Fehlerliste">
25
+ <ul>
26
+ {Object.entries(errors).map(([field, error]) => (
27
+ <li key={field}>
28
+ <KolLink _href={`#field-${field}`} _label={error} _on={{ onClick: handleLinkClick }} />
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ </nav>
33
+ </KolAlert>
34
+ );
35
+ }
@@ -0,0 +1,157 @@
1
+ import React, { useState } from 'react';
2
+ import { KolButton, KolForm, KolHeading, KolInputEmail, KolInputText, KolSelect } from '@public-ui/react';
3
+ import { Field, FieldProps, useFormikContext } from 'formik';
4
+ import { FormValues } from './AppointmentForm';
5
+
6
+ const SALUTATION_OPTIONS = [
7
+ {
8
+ value: 'Firma',
9
+ label: 'Firma',
10
+ },
11
+ {
12
+ value: 'Frau',
13
+ label: 'Frau',
14
+ },
15
+ {
16
+ value: 'Herr',
17
+ label: 'Herr',
18
+ },
19
+ {
20
+ value: 'Hallo',
21
+ label: 'Hallo',
22
+ },
23
+ ];
24
+
25
+ export function PersonalInformationForm() {
26
+ const form = useFormikContext<FormValues>();
27
+ const [sectionSubmitted, setSectionSubmitted] = useState(false);
28
+
29
+ return (
30
+ <div className="p-2">
31
+ <KolHeading _level={2} _label="Geben Sie Ihre Kontaktdaten ein"></KolHeading>
32
+ <ul>{sectionSubmitted && Object.entries(form.errors).map(([field, error]) => <li key={field}>{error}</li>)}</ul>
33
+ <KolForm
34
+ _on={{
35
+ onSubmit: () => {
36
+ void form.submitForm();
37
+ setSectionSubmitted(true);
38
+ },
39
+ }}
40
+ >
41
+ <Field name="salutation">
42
+ {({ field }: FieldProps<FormValues['salutation']>) => (
43
+ <KolSelect
44
+ _label="Anrede"
45
+ _value={[field.value]}
46
+ _error={form.errors.salutation || ''}
47
+ _touched={form.touched.salutation}
48
+ _options={[{ label: 'Bitte wählen…', value: '' }, ...SALUTATION_OPTIONS]}
49
+ _required
50
+ _on={{
51
+ onChange: (event, values: unknown) => {
52
+ if (event.target) {
53
+ const [value] = values as [FormValues['salutation']];
54
+ void form.setFieldTouched('salutation', true);
55
+ void form.setFieldValue('salutation', value, true);
56
+ }
57
+ },
58
+ }}
59
+ />
60
+ )}
61
+ </Field>
62
+
63
+ {form.values.salutation === 'Firma' && (
64
+ <Field name="company">
65
+ {({ field }: FieldProps<FormValues['company']>) => (
66
+ <div className="block mt-2">
67
+ <KolInputText
68
+ _label="Firma"
69
+ _value={field.value}
70
+ _error={form.errors.company || ''}
71
+ _touched={form.touched.company}
72
+ _required
73
+ _on={{
74
+ onChange: (event, value: unknown) => {
75
+ if (event.target) {
76
+ void form.setFieldTouched('company', true);
77
+ void form.setFieldValue('company', value, true);
78
+ }
79
+ },
80
+ }}
81
+ />
82
+ </div>
83
+ )}
84
+ </Field>
85
+ )}
86
+
87
+ <Field name="name">
88
+ {({ field }: FieldProps<FormValues['name']>) => (
89
+ <div className="block mt-2">
90
+ <KolInputText
91
+ _label="Vor- und Zuname"
92
+ _value={field.value}
93
+ _error={form.errors.name || ''}
94
+ _touched={form.touched.name}
95
+ _required
96
+ _on={{
97
+ onChange: (event, value: unknown) => {
98
+ if (event.target) {
99
+ void form.setFieldTouched('name', true);
100
+ void form.setFieldValue('name', value, true);
101
+ }
102
+ },
103
+ }}
104
+ />
105
+ </div>
106
+ )}
107
+ </Field>
108
+
109
+ <Field name="email">
110
+ {({ field }: FieldProps<FormValues['email']>) => (
111
+ <div className="block mt-2">
112
+ <KolInputEmail
113
+ _label="E-Mail"
114
+ _value={field.value}
115
+ _error={form.errors.email || ''}
116
+ _touched={form.touched.email}
117
+ _required
118
+ _on={{
119
+ onChange: (event, value: unknown) => {
120
+ if (event.target) {
121
+ void form.setFieldTouched('email', true);
122
+ void form.setFieldValue('email', value, true);
123
+ }
124
+ },
125
+ }}
126
+ />
127
+ </div>
128
+ )}
129
+ </Field>
130
+
131
+ <Field name="phone">
132
+ {({ field }: FieldProps<FormValues['phone']>) => (
133
+ <div className="block mt-2">
134
+ <KolInputText
135
+ _type="tel"
136
+ _label="Telefonnumer"
137
+ _value={field.value}
138
+ _error={form.errors.phone || ''}
139
+ _touched={form.touched.phone}
140
+ _on={{
141
+ onChange: (event, value: unknown) => {
142
+ if (event.target) {
143
+ void form.setFieldTouched('phone', true);
144
+ void form.setFieldValue('phone', value, true);
145
+ }
146
+ },
147
+ }}
148
+ />
149
+ </div>
150
+ )}
151
+ </Field>
152
+
153
+ <KolButton _label="Weiter" _type="submit" className="mt-2" />
154
+ </KolForm>
155
+ </div>
156
+ );
157
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { useFormikContext } from 'formik';
3
+ import { KolHeading } from '@public-ui/react';
4
+ import { FormValues } from './AppointmentForm';
5
+
6
+ const ValueFallback = () => <i>Nicht angegeben</i>;
7
+ const ValueWithFallback = ({ value }: { value: string }) => (value ? value : <ValueFallback />);
8
+
9
+ export function Summary() {
10
+ const { values } = useFormikContext<FormValues>();
11
+
12
+ return (
13
+ <>
14
+ <KolHeading _level={2} _label="Zusammenfassung"></KolHeading>
15
+
16
+ <dl>
17
+ <dt>Stadtteil</dt>
18
+ <dd>
19
+ <ValueWithFallback value={values.district} />
20
+ </dd>
21
+ <dt>Termin</dt>
22
+ <dd>{values.date && values.time ? `${values.date} ${values.time} Uhr` : <ValueFallback />}</dd>
23
+
24
+ {values.salutation === 'Firma' ? (
25
+ <>
26
+ <dt>Firma</dt>
27
+ <dd>
28
+ <ValueWithFallback value={values.company} />
29
+ </dd>
30
+ </>
31
+ ) : (
32
+ <>
33
+ <dt>Anrede</dt>
34
+ <dd>
35
+ <ValueWithFallback value={values.salutation} />
36
+ </dd>
37
+ </>
38
+ )}
39
+
40
+ <dt>Name</dt>
41
+ <dd>
42
+ <ValueWithFallback value={values.name} />
43
+ </dd>
44
+ <dt>E-Mail</dt>
45
+ <dd>
46
+ <ValueWithFallback value={values.email} />
47
+ </dd>
48
+ <dt>Telefon</dt>
49
+ <dd>
50
+ <ValueWithFallback value={values.phone} />
51
+ </dd>
52
+ </dl>
53
+ </>
54
+ );
55
+ }
@@ -0,0 +1,37 @@
1
+ import { Option } from '@public-ui/components';
2
+
3
+ const getRandomIntInclusive = (min: number, max: number) => {
4
+ min = Math.ceil(min);
5
+ max = Math.floor(max);
6
+ return Math.floor(Math.random() * (max - min + 1) + min);
7
+ };
8
+
9
+ const padHours = (hours: number): string => `${hours < 10 ? '0' : ''}${hours}`;
10
+
11
+ const getRandomTimes = () => {
12
+ const earliest = 8;
13
+ const latest = 17;
14
+ const amount = getRandomIntInclusive(2, 9);
15
+
16
+ const times = new Set<number>();
17
+ while (times.size !== amount) {
18
+ times.add(getRandomIntInclusive(earliest, latest));
19
+ }
20
+ return [...times].sort((timeA, timeB) => timeA - timeB).flatMap((hours) => [`${padHours(hours)}:00`, `${padHours(hours)}:30`]);
21
+ };
22
+
23
+ const sleep = (timeout: number) => {
24
+ return new Promise((resolve) => setTimeout(resolve, timeout));
25
+ };
26
+ export const fetchAvailableTimes = async (): Promise<Option<string>[]> => {
27
+ await sleep(1000);
28
+ return getRandomTimes().map((time) => ({
29
+ label: time,
30
+ value: time,
31
+ }));
32
+ };
33
+
34
+ export const checkAppointmentAvailability = async (time?: string): Promise<boolean> => {
35
+ await sleep(500);
36
+ return time?.endsWith(':30') ?? false;
37
+ };