@dejwcake/craftable 0.1.10 → 2.0.1

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 (127) hide show
  1. package/README.md +117 -3
  2. package/dist/buildMatchPatternFn-BV4I8r8j.js +63 -0
  3. package/dist/buildMatchPatternFn-T5KjFTBQ.cjs +1 -0
  4. package/dist/craftable.css +2 -0
  5. package/dist/cs-B5QMxFrl.js +682 -0
  6. package/dist/cs-MAoetkhe.cjs +1 -0
  7. package/dist/de-BlQ2ZKmc.js +526 -0
  8. package/dist/de-BoVLAn6U.cjs +1 -0
  9. package/dist/en-US-BLvEnhs4.js +443 -0
  10. package/dist/en-US-Dz8vkDZ_.cjs +1 -0
  11. package/dist/en-US-X9gX759i.cjs +1 -0
  12. package/dist/es-DBHDeXHI.cjs +1 -0
  13. package/dist/es-YEqfkBY2.js +437 -0
  14. package/dist/fr-B7MHC2AN.js +407 -0
  15. package/dist/fr-DLs3QoNB.cjs +1 -0
  16. package/dist/hu-BKZPI9jH.cjs +1 -0
  17. package/dist/hu-reoJgNxD.js +425 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.mjs +3922 -0
  20. package/dist/isSameWeek-CC6GX_zp.js +40 -0
  21. package/dist/isSameWeek-Mctj6Tdj.cjs +1 -0
  22. package/dist/pl-B5A0Rjp7.js +628 -0
  23. package/dist/pl-BMyrOO-Q.cjs +1 -0
  24. package/dist/sk-B7JF2MoV.cjs +1 -0
  25. package/dist/sk-Btro3W4S.js +602 -0
  26. package/package.json +68 -40
  27. package/scripts/publish-components.js +118 -0
  28. package/scss/_images.scss +1 -1
  29. package/scss/_variables.scss +13 -6
  30. package/scss/app.scss +4 -4
  31. package/scss/overrides/_alerts.scss +0 -2
  32. package/scss/overrides/_badges.scss +1 -17
  33. package/scss/overrides/_buttons.scss +64 -7
  34. package/scss/overrides/_cards.scss +4 -51
  35. package/scss/overrides/_checkboxes.scss +54 -59
  36. package/scss/overrides/_forms.scss +270 -176
  37. package/scss/overrides/_index.scss +18 -18
  38. package/scss/overrides/_keyframes.scss +6 -4
  39. package/scss/overrides/_layout.scss +24 -30
  40. package/scss/overrides/_media-wysiwyg.scss +9 -111
  41. package/scss/overrides/_modal.scss +0 -18
  42. package/scss/overrides/_multiselect.scss +63 -7
  43. package/scss/overrides/_nav.scss +55 -33
  44. package/scss/overrides/_notifications.scss +6 -1
  45. package/scss/overrides/_pagination.scss +3 -3
  46. package/scss/overrides/_sidebar.scss +84 -0
  47. package/scss/overrides/_tables.scss +52 -37
  48. package/scss/overrides/_text.scss +1 -3
  49. package/scss/overrides/_wysiwyg.scss +151 -48
  50. package/scss/styles/_auth.scss +32 -37
  51. package/scss/styles/_custom.scss +44 -1
  52. package/scss/styles/_index.scss +4 -4
  53. package/scss/styles/_legacy.scss +5 -81
  54. package/scss/styles/_others.scss +232 -337
  55. package/scss/vendor/_index.scss +5 -4
  56. package/src/auth/ActivationError.vue +23 -0
  57. package/src/auth/ActivationForm.vue +69 -0
  58. package/src/auth/ForgotPasswordForm.vue +74 -0
  59. package/src/auth/LoginForm.vue +107 -0
  60. package/src/auth/ResetPasswordForm.vue +122 -0
  61. package/src/components/ConfirmModal.vue +65 -0
  62. package/src/components/UserDetailTooltip.vue +88 -0
  63. package/src/components/form/CkeditorEditor.vue +205 -0
  64. package/src/components/form/FormCheckbox.vue +29 -0
  65. package/src/components/form/FormDatePicker.vue +52 -0
  66. package/src/components/form/FormEmail.vue +17 -0
  67. package/src/components/form/FormInput.vue +31 -0
  68. package/src/components/form/FormLocalizedInput.vue +47 -0
  69. package/src/components/form/FormLocalizedWysiwyg.vue +55 -0
  70. package/src/components/form/FormLocalizedWysiwygSimple.vue +55 -0
  71. package/src/components/form/FormMultiSelect.vue +36 -0
  72. package/src/components/form/FormPasswordConfirm.vue +49 -0
  73. package/src/components/form/FormSelect.vue +35 -0
  74. package/src/components/form/FormSubmit.vue +13 -0
  75. package/src/components/form/FormTagInput.vue +36 -0
  76. package/src/components/form/FormTextarea.vue +31 -0
  77. package/src/components/form/FormWysiwyg.vue +31 -0
  78. package/src/components/form/FormWysiwygSimple.vue +31 -0
  79. package/src/components/form/LocalizationBar.vue +90 -0
  80. package/src/components/form/MediaUpload.vue +317 -0
  81. package/src/components/form/TiptapEditor.vue +238 -0
  82. package/src/components/listing/BulkCheckboxHeader.vue +18 -0
  83. package/src/components/listing/BulkCheckboxRow.vue +21 -0
  84. package/src/components/listing/BulkOperationsBar.vue +41 -0
  85. package/src/components/listing/DeleteButton.vue +46 -0
  86. package/src/components/listing/EditButton.vue +17 -0
  87. package/src/components/listing/EmptyState.vue +25 -0
  88. package/src/components/listing/ListingHeader.vue +24 -0
  89. package/src/components/listing/Pagination.vue +158 -0
  90. package/src/components/listing/PerPage.vue +15 -0
  91. package/src/components/listing/PublishedAtColumn.vue +226 -0
  92. package/src/components/listing/Search.vue +81 -0
  93. package/src/components/listing/Sortable.vue +28 -0
  94. package/src/components/listing/ToggleSwitch.vue +47 -0
  95. package/src/composables/useAdmin.js +39 -0
  96. package/src/composables/useBaseAuth.js +81 -0
  97. package/src/composables/useBaseForm.js +265 -0
  98. package/src/composables/useBaseListing.js +316 -0
  99. package/src/composables/useResponsiveColumns.js +62 -0
  100. package/src/index.js +63 -0
  101. package/src/translation/TranslationListing.vue +230 -0
  102. package/src/translation/modals/TranslationEditModal.vue +86 -0
  103. package/src/translation/modals/TranslationExportModal.vue +74 -0
  104. package/src/translation/modals/TranslationImportModal.vue +255 -0
  105. package/src/ui/index.js +99 -0
  106. package/src/utils/ckeditorHelpers.js +36 -0
  107. package/src/utils/dateFnsLocale.js +24 -0
  108. package/src/utils/dateFormatters.js +30 -0
  109. package/src/utils/mediaProps.js +14 -0
  110. package/src/utils/notify.js +22 -0
  111. package/dist/admin/Admin.js +0 -42
  112. package/dist/auth/Form.js +0 -15
  113. package/dist/base-components/Form/BaseForm.js +0 -395
  114. package/dist/base-components/Form/BaseUpload.js +0 -188
  115. package/dist/base-components/Form/bootstrap.js +0 -21
  116. package/dist/base-components/Listing/BaseListing.js +0 -454
  117. package/dist/base-components/Listing/components/Pagination.js +0 -105
  118. package/dist/base-components/Listing/components/Sortable.js +0 -48
  119. package/dist/base-components/Listing/components/UserDetailTooltip.js +0 -55
  120. package/dist/index.js +0 -70
  121. package/dist/overrides/trumbowyg.edit-embed-template.js +0 -72
  122. package/dist/overrides/trumbowyg.reupload.js +0 -146
  123. package/dist/overrides/trumbowyg.template.js +0 -67
  124. package/dist/translation/Form.js +0 -15
  125. package/dist/translation/Listing.js +0 -283
  126. package/dist/ui/index.js +0 -33
  127. package/scss/overrides/_switches.scss +0 -25
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <span v-if="item.published_at <= now">
3
+ {{ formatDatetime(item.published_at, datetimeFormat) }}
4
+ </span>
5
+ <span v-if="item.published_at > now">
6
+ <small>{{ translations.will_be_published }}</small
7
+ ><br />
8
+ {{ formatDatetime(item.published_at, datetimeFormat) }}
9
+ <span class="cursor-pointer" :title="translations.publish_later" role="button" @click="openPublishLater()">
10
+ <i class="fa fa-calendar"></i>
11
+ </span>
12
+ </span>
13
+ <div v-if="!item.published_at">
14
+ <span
15
+ class="btn btn-sm btn-info text-white mb-1"
16
+ :title="translations.publish_later"
17
+ role="button"
18
+ @click="openPublishLater()"
19
+ >
20
+ <i class="fa fa-calendar"></i>&nbsp;&nbsp;{{ translations.publish_later }}
21
+ </span>
22
+ </div>
23
+ <div v-if="!item.published_at || item.published_at > now">
24
+ <form class="d-inline" @submit.prevent="publishNow()">
25
+ <button
26
+ type="submit"
27
+ class="btn btn-sm btn-success text-white"
28
+ :title="translations.publish_now"
29
+ :disabled="submitting"
30
+ >
31
+ <i class="fa fa-paper-plane"></i>&nbsp;&nbsp;{{ translations.publish_now }}
32
+ </button>
33
+ </form>
34
+ </div>
35
+ <div v-if="item.published_at && item.published_at < now">
36
+ <form class="d-inline" @submit.prevent="unpublishNow()">
37
+ <button
38
+ type="submit"
39
+ class="btn btn-sm btn-danger"
40
+ :title="translations.unpublish_now"
41
+ :disabled="submitting"
42
+ >
43
+ <i class="fa fa-paper-plane"></i>&nbsp;&nbsp;{{ translations.unpublish_now }}
44
+ </button>
45
+ </form>
46
+ </div>
47
+ <div v-if="editing" class="publish-later-backdrop" @click.self="editing = false"></div>
48
+ <div v-if="editing" class="publish-later-modal">
49
+ <div class="d-flex justify-content-between align-items-center mb-2">
50
+ <strong>{{ translations.publish_later }}</strong>
51
+ <button type="button" class="btn-close btn-close-sm" aria-label="Close" @click="editing = false"></button>
52
+ </div>
53
+ <VueDatePicker
54
+ v-model="publishDate"
55
+ model-type="yyyy-MM-dd HH:mm:ss"
56
+ :format="'dd.MM.yyyy HH:mm'"
57
+ :format-locale="dateFnsLocale"
58
+ :enable-time-picker="true"
59
+ :enable-seconds="false"
60
+ :inline="true"
61
+ auto-apply
62
+ />
63
+ <div class="mt-2 text-end">
64
+ <button
65
+ class="btn btn-sm btn-primary text-white"
66
+ :disabled="submitting || !publishDate"
67
+ @click="savePublishLater()"
68
+ >
69
+ Save
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <ConfirmModal
74
+ :show="showConfirm"
75
+ :translations="confirmTranslations"
76
+ :variant="confirmVariant"
77
+ @confirm="onConfirm"
78
+ @cancel="onCancelConfirm"
79
+ />
80
+ </template>
81
+
82
+ <script setup>
83
+ import { ref } from 'vue';
84
+ import { VueDatePicker } from '@vuepic/vue-datepicker';
85
+ import { notifySuccess, notifyError } from '../../utils/notify.js';
86
+ // Use the globally configured axios instance (with CSRF token and X-Requested-With header)
87
+ const axios = window.axios;
88
+ import { formatDatetime } from '../../utils/dateFormatters.js';
89
+ import { getDateFnsLocale } from '../../utils/dateFnsLocale.js';
90
+ import ConfirmModal from '../ConfirmModal.vue';
91
+
92
+ const dateFnsLocale = getDateFnsLocale();
93
+
94
+ const props = defineProps({
95
+ item: { type: Object, required: true },
96
+ url: { type: String, required: true },
97
+ now: { type: String, required: true },
98
+ datetimeFormat: { type: String, default: 'DD.MM.YYYY HH:mm' },
99
+ translations: {
100
+ type: Object,
101
+ default: () => ({
102
+ publish_now: 'Publish now',
103
+ unpublish_now: 'Unpublish now',
104
+ publish_later: 'Publish later',
105
+ will_be_published: 'This item will be published at',
106
+ confirm_publish_now: 'Do you really want to publish this item now?',
107
+ confirm_unpublish_now: 'Do you really want to unpublish this item?',
108
+ }),
109
+ },
110
+ });
111
+
112
+ const emit = defineEmits(['update:item']);
113
+
114
+ const editing = ref(false);
115
+ const publishDate = ref(null);
116
+ const submitting = ref(false);
117
+ const showConfirm = ref(false);
118
+ const pendingAction = ref(null);
119
+ const confirmTranslations = ref({});
120
+ const confirmVariant = ref('danger');
121
+
122
+ function openPublishLater() {
123
+ publishDate.value = props.item.published_at || null;
124
+ editing.value = true;
125
+ }
126
+
127
+ function savePublishLater() {
128
+ if (!publishDate.value) return;
129
+ submitting.value = true;
130
+
131
+ axios
132
+ .post(props.url, { published_at: publishDate.value })
133
+ .then(
134
+ (response) => {
135
+ emit('update:item', { ...props.item, published_at: response.data.object.published_at });
136
+ editing.value = false;
137
+ notifySuccess(response.data.message);
138
+ },
139
+ (error) => {
140
+ notifyError(error.response?.data?.message);
141
+ },
142
+ )
143
+ .finally(() => {
144
+ submitting.value = false;
145
+ });
146
+ }
147
+
148
+ function publishNow() {
149
+ pendingAction.value = 'publish';
150
+ confirmVariant.value = 'success';
151
+ confirmTranslations.value = {
152
+ confirm_title: props.translations.publish_now,
153
+ confirm_text: props.translations.confirm_publish_now,
154
+ confirm_btn: props.translations.publish_now,
155
+ cancel_btn: 'Cancel',
156
+ };
157
+ showConfirm.value = true;
158
+ }
159
+
160
+ function unpublishNow() {
161
+ pendingAction.value = 'unpublish';
162
+ confirmVariant.value = 'danger';
163
+ confirmTranslations.value = {
164
+ confirm_title: props.translations.unpublish_now,
165
+ confirm_text: props.translations.confirm_unpublish_now,
166
+ confirm_btn: props.translations.unpublish_now,
167
+ cancel_btn: 'Cancel',
168
+ };
169
+ showConfirm.value = true;
170
+ }
171
+
172
+ function onConfirm() {
173
+ showConfirm.value = false;
174
+ if (pendingAction.value === 'publish') {
175
+ doPublishNow();
176
+ } else if (pendingAction.value === 'unpublish') {
177
+ doUnpublishNow();
178
+ }
179
+ pendingAction.value = null;
180
+ }
181
+
182
+ function onCancelConfirm() {
183
+ showConfirm.value = false;
184
+ pendingAction.value = null;
185
+ }
186
+
187
+ function doPublishNow() {
188
+ submitting.value = true;
189
+ axios
190
+ .post(props.url, { publish_now: true })
191
+ .then(
192
+ (response) => {
193
+ emit('update:item', { ...props.item, published_at: response.data.object.published_at });
194
+ notifySuccess(response.data.message);
195
+ },
196
+ (error) => {
197
+ notifyError(error.response?.data?.message);
198
+ },
199
+ )
200
+ .finally(() => {
201
+ submitting.value = false;
202
+ });
203
+ }
204
+
205
+ function doUnpublishNow() {
206
+ submitting.value = true;
207
+ axios
208
+ .post(props.url, { unpublish_now: true })
209
+ .then(
210
+ (response) => {
211
+ emit('update:item', {
212
+ ...props.item,
213
+ published_at: response.data.object.published_at,
214
+ published_to: response.data.object.published_to,
215
+ });
216
+ notifySuccess(response.data.message);
217
+ },
218
+ (error) => {
219
+ notifyError(error.response?.data?.message);
220
+ },
221
+ )
222
+ .finally(() => {
223
+ submitting.value = false;
224
+ });
225
+ }
226
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div class="input-group">
3
+ <slot name="prepend" />
4
+ <div class="form-control d-flex align-items-center p-0">
5
+ <input
6
+ ref="inputRef"
7
+ class="border-0 shadow-none ps-3 flex-grow-1"
8
+ style="outline: none; min-width: 0"
9
+ :placeholder="translations.search_placeholder"
10
+ :value="search"
11
+ @input="onInput"
12
+ @keyup.enter="onEnter"
13
+ />
14
+ <button
15
+ v-if="search"
16
+ type="button"
17
+ class="border-0 bg-transparent pe-1 ps-0 flex-shrink-0 text-danger"
18
+ @click="clearSearch"
19
+ >
20
+ <i class="fa fa-times"></i>
21
+ </button>
22
+ </div>
23
+ <button type="button" class="btn btn-primary" @click="doSearch">
24
+ <i class="fa fa-search"></i>&nbsp; {{ translations.search_btn }}
25
+ </button>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup>
30
+ import { ref, onBeforeUnmount } from 'vue';
31
+
32
+ const props = defineProps({
33
+ search: { type: String, default: '' },
34
+ debounce: { type: Number, default: 400 },
35
+ filterFn: { type: Function, required: true },
36
+ translations: {
37
+ type: Object,
38
+ default: () => ({ search_placeholder: 'Search...', search_btn: 'Search' }),
39
+ },
40
+ });
41
+
42
+ const emit = defineEmits(['update:search']);
43
+ const inputRef = ref(null);
44
+ let debounceTimer = null;
45
+
46
+ function cancelDebounce() {
47
+ if (debounceTimer) {
48
+ clearTimeout(debounceTimer);
49
+ debounceTimer = null;
50
+ }
51
+ }
52
+
53
+ function onInput(event) {
54
+ const value = event.target.value;
55
+ emit('update:search', value);
56
+ cancelDebounce();
57
+ debounceTimer = setTimeout(() => {
58
+ props.filterFn('search', value);
59
+ }, props.debounce);
60
+ }
61
+
62
+ function doSearch() {
63
+ cancelDebounce();
64
+ props.filterFn('search', props.search);
65
+ }
66
+
67
+ function onEnter() {
68
+ cancelDebounce();
69
+ const value = inputRef.value?.value ?? props.search;
70
+ emit('update:search', value);
71
+ props.filterFn('search', value);
72
+ }
73
+
74
+ function clearSearch() {
75
+ cancelDebounce();
76
+ emit('update:search', '');
77
+ props.filterFn('search', '');
78
+ }
79
+
80
+ onBeforeUnmount(cancelDebounce);
81
+ </script>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <th>
3
+ <a @click.stop="sort(column)">
4
+ <span
5
+ class="fa"
6
+ :class="{
7
+ 'fa-arrow-down-wide-short': orderBy.column === column && orderBy.direction === 'asc',
8
+ 'fa-arrow-down-short-wide': orderBy.column === column && orderBy.direction === 'desc',
9
+ }"
10
+ ></span>
11
+ <slot></slot>
12
+ </a>
13
+ </th>
14
+ </template>
15
+
16
+ <script setup>
17
+ const props = defineProps({
18
+ column: { type: String, required: true },
19
+ orderBy: { type: Object, required: true },
20
+ });
21
+
22
+ const emit = defineEmits(['sort']);
23
+
24
+ function sort(column) {
25
+ const direction = props.orderBy.column === column ? (props.orderBy.direction === 'asc' ? 'desc' : 'asc') : 'asc';
26
+ emit('sort', { column, direction });
27
+ }
28
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="form-check form-switch form-switch-inline">
3
+ <input
4
+ class="form-check-input"
5
+ :class="switchClass"
6
+ type="checkbox"
7
+ role="switch"
8
+ :checked="!!modelValue"
9
+ @change="toggle"
10
+ />
11
+ </div>
12
+ </template>
13
+
14
+ <script setup>
15
+ const axios = window.axios;
16
+ import { notifySuccess, notifyError } from '../../utils/notify.js';
17
+ import { computed } from 'vue';
18
+
19
+ const props = defineProps({
20
+ modelValue: { type: [Boolean, Number, String], required: true },
21
+ url: { type: String, required: true },
22
+ column: { type: String, required: true },
23
+ row: { type: Object, required: true },
24
+ variant: { type: String, default: 'success' },
25
+ });
26
+
27
+ const emit = defineEmits(['update:modelValue']);
28
+
29
+ const switchClass = computed(() => `form-switch-${props.variant}`);
30
+
31
+ function toggle() {
32
+ const newValue = !props.modelValue;
33
+ emit('update:modelValue', newValue);
34
+
35
+ const data = { ...props.row, [props.column]: newValue };
36
+
37
+ axios.post(props.url, data).then(
38
+ (response) => {
39
+ notifySuccess(response.data.message);
40
+ },
41
+ (error) => {
42
+ emit('update:modelValue', props.modelValue);
43
+ notifyError(error.response?.data?.message);
44
+ },
45
+ );
46
+ }
47
+ </script>
@@ -0,0 +1,39 @@
1
+ import { ref, onMounted } from 'vue';
2
+ const axios = window.axios;
3
+
4
+ export function useAdmin() {
5
+ const loading = ref(false);
6
+
7
+ function setLoading(value) {
8
+ loading.value = !!value;
9
+ }
10
+
11
+ onMounted(() => {
12
+ axios.interceptors.request.use(
13
+ (config) => {
14
+ setLoading(true);
15
+ return config;
16
+ },
17
+ (error) => {
18
+ setLoading(false);
19
+ return Promise.reject(error);
20
+ },
21
+ );
22
+
23
+ axios.interceptors.response.use(
24
+ (response) => {
25
+ setLoading(false);
26
+ return response;
27
+ },
28
+ (error) => {
29
+ setLoading(false);
30
+ return Promise.reject(error);
31
+ },
32
+ );
33
+ });
34
+
35
+ return {
36
+ loading,
37
+ setLoading,
38
+ };
39
+ }
@@ -0,0 +1,81 @@
1
+ import { ref } from 'vue';
2
+ import { useForm, validate as veeValidate } from 'vee-validate';
3
+ const axios = window.axios;
4
+
5
+ export function useBaseAuth(props, options = {}) {
6
+ const form = ref(options.formDefaults ? { ...options.formDefaults } : {});
7
+ const submitting = ref(false);
8
+ const successMessage = ref('');
9
+
10
+ const { setErrors, errors } = useForm();
11
+
12
+ async function onSubmit() {
13
+ if (options.validationSchema) {
14
+ const fieldErrors = {};
15
+ for (const [field, rules] of Object.entries(options.validationSchema)) {
16
+ const result = await veeValidate(form.value[field], rules, {
17
+ name: field,
18
+ values: form.value,
19
+ });
20
+ if (!result.valid) {
21
+ fieldErrors[field] = result.errors[0];
22
+ }
23
+ }
24
+
25
+ const errorState = {};
26
+ for (const field of Object.keys(options.validationSchema)) {
27
+ errorState[field] = fieldErrors[field] || undefined;
28
+ }
29
+ setErrors(errorState);
30
+
31
+ if (Object.keys(fieldErrors).length > 0) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ submitting.value = true;
37
+ successMessage.value = '';
38
+
39
+ axios
40
+ .post(props.action, form.value)
41
+ .then((response) => {
42
+ onSuccess(response.data);
43
+ })
44
+ .catch((err) => {
45
+ onFail(err.response?.data || {});
46
+ });
47
+ }
48
+
49
+ function onSuccess(data) {
50
+ submitting.value = false;
51
+ if (options.onSuccess) {
52
+ options.onSuccess(data);
53
+ return;
54
+ }
55
+ if (data.message) {
56
+ successMessage.value = data.message;
57
+ }
58
+ if (data.redirect) {
59
+ window.location.replace(data.redirect);
60
+ }
61
+ }
62
+
63
+ function onFail(data) {
64
+ submitting.value = false;
65
+ if (data.errors !== undefined) {
66
+ const fieldErrors = {};
67
+ Object.keys(data.errors).forEach((key) => {
68
+ fieldErrors[key] = data.errors[key][0];
69
+ });
70
+ setErrors(fieldErrors);
71
+ }
72
+ }
73
+
74
+ return {
75
+ form,
76
+ errors,
77
+ submitting,
78
+ successMessage,
79
+ onSubmit,
80
+ };
81
+ }