@autobusal/routes-order 1.31.3 → 1.31.5
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.
- package/CHANGELOG.md +13 -0
- package/Facts/types.ts +7 -0
- package/Sections/Schema.tsx +26 -0
- package/Step4/Passenger/Passenger.tsx +29 -7
- package/Step5/Addons/Addons.tsx +7 -0
- package/Step5/Billing/Billing.tsx +36 -22
- package/Step5/Coupons/Coupons.tsx +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.31.5
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **BusTrip carries the pair's AggregateRating** when `/routes/search/facts` reports one (`FactsData.rating`). The aggregate is computed server-side over the same published-review rules the reviews listing uses, and rides the facts cache - the pair page keeps its one-fetch design. Absent rating, the key is not emitted at all.
|
|
8
|
+
|
|
9
|
+
## 1.31.4
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **The checkout form's fields have accessible names.** Passenger and billing inputs had no `id`/`for` pairing, no `aria-label` and no wrapping label - programmatically anonymous on the one form that decides whether a sale completes. Ids are `useId`-prefixed, which is what stops `adult1_*`, `adult2_*` and `child1_*` colliding; a hardcoded id would have traded one accessibility bug for another.
|
|
14
|
+
- Coupon code and the add-on phone fields get an `aria-label`, having only ever had a placeholder - and a placeholder disappears the moment someone types.
|
|
15
|
+
|
|
3
16
|
## 1.31.3
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/Facts/types.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface FactsData {
|
|
|
17
17
|
// how many coaches serve the pair in the TIMETABLE, not on a given date
|
|
18
18
|
departures: number
|
|
19
19
|
operators: string[]
|
|
20
|
+
// Edited: Claude - Date: 2026-08-20
|
|
21
|
+
// The pair's own review score - the published reviews of every route in
|
|
22
|
+
// this very timetable, aggregated by obtapi with the same threshold rule
|
|
23
|
+
// as everything else (null until there is enough to say something honest).
|
|
24
|
+
// For structured data; note ScheduleRow.rating below is the OPERATOR's
|
|
25
|
+
// company-wide number, a different thing.
|
|
26
|
+
rating: { average: number, count: number } | null
|
|
20
27
|
// Two numbers that must never be conflated.
|
|
21
28
|
//
|
|
22
29
|
// `road` is real driving distance from a routing service and is null unless
|
package/Sections/Schema.tsx
CHANGED
|
@@ -27,6 +27,19 @@ interface Props {
|
|
|
27
27
|
* claiming travellers rated this journey when they rated the company. When
|
|
28
28
|
* an operator has no published rating - below the display threshold - it is
|
|
29
29
|
* simply absent, exactly as it is on the page.
|
|
30
|
+
*
|
|
31
|
+
* Edited: Claude - Date: 2026-08-20
|
|
32
|
+
*
|
|
33
|
+
* The trip now ALSO carries its own AggregateRating - and that is not the
|
|
34
|
+
* claim the paragraph above forbids, because `facts.rating` is a different
|
|
35
|
+
* number: obtapi aggregates the published reviews of exactly the routes in
|
|
36
|
+
* this timetable (Facts::rating, the same set Reviews\BrowseController's
|
|
37
|
+
* `pair` mode reads), not a company-wide score stretched over one leg.
|
|
38
|
+
* Travellers who booked these very coaches wrote those reviews, and every
|
|
39
|
+
* card on the page links to the operator profile where they are read in
|
|
40
|
+
* full. Below the display threshold obtapi sends null and nothing is
|
|
41
|
+
* emitted here - an AggregateRating of nothing is a guideline violation,
|
|
42
|
+
* worse than none.
|
|
30
43
|
*/
|
|
31
44
|
const Schema = ({ facts, t }: Props): (JSX.Element | null) => {
|
|
32
45
|
const schedule = facts.schedule ?? [];
|
|
@@ -62,6 +75,19 @@ const Schema = ({ facts, t }: Props): (JSX.Element | null) => {
|
|
|
62
75
|
departureBusStop: { '@type': 'BusStop', name: facts.from },
|
|
63
76
|
arrivalBusStop: { '@type': 'BusStop', name: facts.to },
|
|
64
77
|
|
|
78
|
+
// Edited: Claude - Date: 2026-08-20
|
|
79
|
+
// The pair's own score - see the header comment. Null below the
|
|
80
|
+
// display threshold, and then the property simply does not exist.
|
|
81
|
+
...(facts.rating ? {
|
|
82
|
+
aggregateRating: {
|
|
83
|
+
'@type': 'AggregateRating',
|
|
84
|
+
ratingValue: facts.rating.average,
|
|
85
|
+
reviewCount: facts.rating.count,
|
|
86
|
+
bestRating: 5,
|
|
87
|
+
worstRating: 1
|
|
88
|
+
}
|
|
89
|
+
} : {}),
|
|
90
|
+
|
|
65
91
|
...(providers.length > 0 ? {
|
|
66
92
|
provider: providers.map(item => organisation(item.name, item.rating))
|
|
67
93
|
} : {}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
1
2
|
import { TFunction } from 'i18next';
|
|
2
3
|
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
3
4
|
import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
|
|
@@ -47,6 +48,20 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
47
48
|
|
|
48
49
|
const has = (field: string): boolean => fields.includes(field);
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Edited: Claude - Date: 2026-08-19
|
|
53
|
+
* Every field here was rendered as a bare <input> beside its label text,
|
|
54
|
+
* with no id and no association - so a screen reader announced the whole
|
|
55
|
+
* passenger block as a run of unnamed "edit text". The field NAMES are
|
|
56
|
+
* already unique per passenger (adult1_fn, adult2_fn), but they belong to
|
|
57
|
+
* the form payload; ids are page-global, so they get a useId prefix -
|
|
58
|
+
* this component renders once per passenger, and two forms on one page
|
|
59
|
+
* would otherwise emit duplicate ids, which is its own defect.
|
|
60
|
+
*/
|
|
61
|
+
const uid = useId();
|
|
62
|
+
|
|
63
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
64
|
+
|
|
50
65
|
return (
|
|
51
66
|
<Container className="box">
|
|
52
67
|
<Title>
|
|
@@ -55,10 +70,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
55
70
|
|
|
56
71
|
<div className="column">
|
|
57
72
|
<div className="row">
|
|
58
|
-
<Required>{ t('routes_order.step4.first_name') }</Required>
|
|
73
|
+
<Required htmlFor={ id(names.firstName) }>{ t('routes_order.step4.first_name') }</Required>
|
|
59
74
|
|
|
60
75
|
<input
|
|
61
76
|
type="text"
|
|
77
|
+
id={ id(names.firstName) }
|
|
62
78
|
defaultValue={ values !== undefined ? values[names.firstName] : '' }
|
|
63
79
|
{ ...refs(names.firstName, Validate('required|min_length:2|max_length:100', t)) }
|
|
64
80
|
/>
|
|
@@ -67,10 +83,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
67
83
|
</div>
|
|
68
84
|
|
|
69
85
|
<div className="row">
|
|
70
|
-
<Required>{ t('routes_order.step4.last_name') }</Required>
|
|
86
|
+
<Required htmlFor={ id(names.lastName) }>{ t('routes_order.step4.last_name') }</Required>
|
|
71
87
|
|
|
72
88
|
<input
|
|
73
89
|
type="text"
|
|
90
|
+
id={ id(names.lastName) }
|
|
74
91
|
defaultValue={ values !== undefined ? values[names.lastName] : '' }
|
|
75
92
|
{ ...refs(names.lastName, Validate('required|min_length:2|max_length:100', t)) }
|
|
76
93
|
/>
|
|
@@ -81,11 +98,12 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
81
98
|
|
|
82
99
|
<div className="column">
|
|
83
100
|
<div className="row">
|
|
84
|
-
<Required>{ t('routes_order.step4.dob') }</Required>
|
|
101
|
+
<Required htmlFor={ id(names.dob) }>{ t('routes_order.step4.dob') }</Required>
|
|
85
102
|
|
|
86
103
|
<Calendar
|
|
87
104
|
type="dob"
|
|
88
105
|
name={ names.dob }
|
|
106
|
+
id={ id(names.dob) }
|
|
89
107
|
defaultValue={ values !== undefined ? String(values[names.dob]) : undefined }
|
|
90
108
|
t={ t }
|
|
91
109
|
validation="required"
|
|
@@ -116,10 +134,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
116
134
|
<div className="column">
|
|
117
135
|
{ has('passport') && (
|
|
118
136
|
<div className="row">
|
|
119
|
-
<Required>{ t('routes_order.step4.passport') }</Required>
|
|
137
|
+
<Required htmlFor={ id(names.passport) }>{ t('routes_order.step4.passport') }</Required>
|
|
120
138
|
|
|
121
139
|
<input
|
|
122
140
|
type="text"
|
|
141
|
+
id={ id(names.passport) }
|
|
123
142
|
defaultValue={ values !== undefined ? values[names.passport] : '' }
|
|
124
143
|
{ ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
|
|
125
144
|
/>
|
|
@@ -130,10 +149,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
130
149
|
|
|
131
150
|
{ has('phone') && (
|
|
132
151
|
<div className="row">
|
|
133
|
-
<Required>{ t('routes_order.step4.phone') }</Required>
|
|
152
|
+
<Required htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</Required>
|
|
134
153
|
|
|
135
154
|
<input
|
|
136
155
|
type="text"
|
|
156
|
+
id={ id(names.phone) }
|
|
137
157
|
defaultValue={ values !== undefined ? values[names.phone] : '' }
|
|
138
158
|
{ ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
|
|
139
159
|
/>
|
|
@@ -156,10 +176,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
156
176
|
<div className="column">
|
|
157
177
|
{ has('whatsapp') && (
|
|
158
178
|
<div className="row">
|
|
159
|
-
<Required>{ t('routes_order.step4.whatsapp') }</Required>
|
|
179
|
+
<Required htmlFor={ id(names.whatsapp) }>{ t('routes_order.step4.whatsapp') }</Required>
|
|
160
180
|
|
|
161
181
|
<input
|
|
162
182
|
type="text"
|
|
183
|
+
id={ id(names.whatsapp) }
|
|
163
184
|
defaultValue={ values !== undefined ? values[names.whatsapp] : '' }
|
|
164
185
|
{ ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
|
|
165
186
|
/>
|
|
@@ -170,10 +191,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
170
191
|
|
|
171
192
|
{ has('telegram') && (
|
|
172
193
|
<div className="row">
|
|
173
|
-
<Required>{ t('routes_order.step4.telegram') }</Required>
|
|
194
|
+
<Required htmlFor={ id(names.telegram) }>{ t('routes_order.step4.telegram') }</Required>
|
|
174
195
|
|
|
175
196
|
<input
|
|
176
197
|
type="text"
|
|
198
|
+
id={ id(names.telegram) }
|
|
177
199
|
defaultValue={ values !== undefined ? values[names.telegram] : '' }
|
|
178
200
|
{ ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
|
|
179
201
|
/>
|
package/Step5/Addons/Addons.tsx
CHANGED
|
@@ -85,9 +85,15 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
|
|
|
85
85
|
|
|
86
86
|
<Notice>{ t('routes_order.step5.addons.whatsapp.description') }</Notice>
|
|
87
87
|
|
|
88
|
+
{ /* Edited: Claude - Date: 2026-08-19
|
|
89
|
+
The number fields below have no visible label of their own -
|
|
90
|
+
they appear under the checkbox they belong to - so the
|
|
91
|
+
placeholder wording is repeated as an accessible name rather
|
|
92
|
+
than leaving them unnamed. */ }
|
|
88
93
|
{ whatsapp && (
|
|
89
94
|
<input
|
|
90
95
|
type="text"
|
|
96
|
+
aria-label={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
|
|
91
97
|
placeholder={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
|
|
92
98
|
value={ whatsappNumber }
|
|
93
99
|
onChange={ (event) => onChangeWhatsappNumber(event.target.value) }
|
|
@@ -143,6 +149,7 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
|
|
|
143
149
|
{ sms && (
|
|
144
150
|
<input
|
|
145
151
|
type="text"
|
|
152
|
+
aria-label={ t('routes_order.step5.addons.sms.phone_placeholder') }
|
|
146
153
|
placeholder={ t('routes_order.step5.addons.sms.phone_placeholder') }
|
|
147
154
|
value={ smsNumber }
|
|
148
155
|
onChange={ (event) => onChangeSmsNumber(event.target.value) }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useId } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { Required } from '@autobusal/common';
|
|
4
4
|
import { UseFormRegister, FieldErrors } from 'react-hook-form';
|
|
@@ -20,6 +20,18 @@ interface Props {
|
|
|
20
20
|
const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
21
21
|
const [ company, setCompany ] = useState<boolean>(false);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Edited: Claude - Date: 2026-08-19
|
|
25
|
+
* Same fix as the passenger block: the labels here were text sitting
|
|
26
|
+
* beside the field rather than attached to it, so the billing form - the
|
|
27
|
+
* last thing between a buyer and paying - announced as unnamed edit
|
|
28
|
+
* boxes. Prefixed with useId rather than using the field name directly,
|
|
29
|
+
* because ids are page-global and these names are not guaranteed to be.
|
|
30
|
+
*/
|
|
31
|
+
const uid = useId();
|
|
32
|
+
|
|
33
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
34
|
+
|
|
23
35
|
const countries = useCountries('all');
|
|
24
36
|
|
|
25
37
|
if (countries.isLoading) {
|
|
@@ -43,17 +55,17 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
43
55
|
|
|
44
56
|
<div className="column">
|
|
45
57
|
<div className="row">
|
|
46
|
-
<Required>{ t('routes_order.step5.billing.first_name') }</Required>
|
|
58
|
+
<Required htmlFor={ id('bill_fn') }>{ t('routes_order.step5.billing.first_name') }</Required>
|
|
47
59
|
|
|
48
|
-
<input type="text" { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
|
|
60
|
+
<input type="text" id={ id('bill_fn') } { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
|
|
49
61
|
|
|
50
62
|
{ Display(errors.bill_fn) }
|
|
51
63
|
</div>
|
|
52
64
|
|
|
53
65
|
<div className="row">
|
|
54
|
-
<Required>{ t('routes_order.step5.billing.last_name') }</Required>
|
|
66
|
+
<Required htmlFor={ id('bill_ln') }>{ t('routes_order.step5.billing.last_name') }</Required>
|
|
55
67
|
|
|
56
|
-
<input type="text" { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
|
|
68
|
+
<input type="text" id={ id('bill_ln') } { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
|
|
57
69
|
|
|
58
70
|
{ Display(errors.bill_ln) }
|
|
59
71
|
</div>
|
|
@@ -61,9 +73,9 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
61
73
|
|
|
62
74
|
<div className="column">
|
|
63
75
|
<div className="row">
|
|
64
|
-
<Required>{ t('routes_order.step5.billing.email') }</Required>
|
|
65
|
-
|
|
66
|
-
<input type="email" { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
76
|
+
<Required htmlFor={ id('email') }>{ t('routes_order.step5.billing.email') }</Required>
|
|
77
|
+
|
|
78
|
+
<input type="email" id={ id('email') } { ...refs('email', Validate('required|email|max_length:255', t)) } />
|
|
67
79
|
|
|
68
80
|
{ Display(errors.email) }
|
|
69
81
|
|
|
@@ -73,35 +85,35 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
73
85
|
</div>
|
|
74
86
|
|
|
75
87
|
<div className="row">
|
|
76
|
-
<Required>{ t('routes_order.step5.billing.phone') }</Required>
|
|
88
|
+
<Required htmlFor={ id('bill_phone') }>{ t('routes_order.step5.billing.phone') }</Required>
|
|
77
89
|
|
|
78
|
-
<input type="text" { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
|
|
90
|
+
<input type="text" id={ id('bill_phone') } { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
|
|
79
91
|
|
|
80
92
|
{ Display(errors.bill_phone) }
|
|
81
93
|
</div>
|
|
82
94
|
</div>
|
|
83
95
|
|
|
84
96
|
<div className="row">
|
|
85
|
-
<Required>{ t('routes_order.step5.billing.address') }</Required>
|
|
97
|
+
<Required htmlFor={ id('bill_address') }>{ t('routes_order.step5.billing.address') }</Required>
|
|
86
98
|
|
|
87
|
-
<textarea { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
|
|
99
|
+
<textarea id={ id('bill_address') } { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
|
|
88
100
|
|
|
89
101
|
{ Display(errors.bill_address) }
|
|
90
102
|
</div>
|
|
91
103
|
|
|
92
104
|
<div className="column">
|
|
93
105
|
<div className="row">
|
|
94
|
-
<Required>{ t('routes_order.step5.billing.city') }</Required>
|
|
106
|
+
<Required htmlFor={ id('bill_city') }>{ t('routes_order.step5.billing.city') }</Required>
|
|
95
107
|
|
|
96
|
-
<input type="text" { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
|
|
108
|
+
<input type="text" id={ id('bill_city') } { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
|
|
97
109
|
|
|
98
110
|
{ Display(errors.bill_city) }
|
|
99
111
|
</div>
|
|
100
112
|
|
|
101
113
|
<div className="row">
|
|
102
|
-
<Required>{ t('routes_order.step5.billing.country') }</Required>
|
|
114
|
+
<Required htmlFor={ id('bill_country') }>{ t('routes_order.step5.billing.country') }</Required>
|
|
103
115
|
|
|
104
|
-
<select { ...refs('bill_country', Validate('required|min:1', t)) }>
|
|
116
|
+
<select id={ id('bill_country') } { ...refs('bill_country', Validate('required|min:1', t)) }>
|
|
105
117
|
<option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
|
|
106
118
|
{ items }
|
|
107
119
|
</select>
|
|
@@ -119,17 +131,19 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
119
131
|
{ company && (
|
|
120
132
|
<div className="column">
|
|
121
133
|
<div className="row">
|
|
122
|
-
{
|
|
134
|
+
{ /* not Required - these are optional, so a plain <label>
|
|
135
|
+
rather than the asterisked one, but still associated */ }
|
|
136
|
+
<label htmlFor={ id('bill_company') }>{ t('routes_order.step5.billing.company') }</label>
|
|
123
137
|
|
|
124
|
-
<input type="text" { ...refs('bill_company') } />
|
|
138
|
+
<input type="text" id={ id('bill_company') } { ...refs('bill_company') } />
|
|
125
139
|
|
|
126
140
|
{ Display(errors.bill_company) }
|
|
127
141
|
</div>
|
|
128
142
|
|
|
129
143
|
<div className="row">
|
|
130
|
-
{ t('routes_order.step5.billing.nipt') }
|
|
144
|
+
<label htmlFor={ id('bill_nipt') }>{ t('routes_order.step5.billing.nipt') }</label>
|
|
131
145
|
|
|
132
|
-
<input type="text" { ...refs('bill_nipt') } />
|
|
146
|
+
<input type="text" id={ id('bill_nipt') } { ...refs('bill_nipt') } />
|
|
133
147
|
|
|
134
148
|
{ Display(errors.bill_nipt) }
|
|
135
149
|
</div>
|
|
@@ -137,9 +151,9 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
|
|
|
137
151
|
) }
|
|
138
152
|
|
|
139
153
|
<div className="row">
|
|
140
|
-
{ t('routes_order.step5.billing.comments') }
|
|
154
|
+
<label htmlFor={ id('comments') }>{ t('routes_order.step5.billing.comments') }</label>
|
|
141
155
|
|
|
142
|
-
<textarea { ...refs('comments') }></textarea>
|
|
156
|
+
<textarea id={ id('comments') } { ...refs('comments') }></textarea>
|
|
143
157
|
</div>
|
|
144
158
|
|
|
145
159
|
<div className="row row-nomargin">
|
|
@@ -60,7 +60,11 @@ const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element |
|
|
|
60
60
|
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
61
61
|
<ContainerForm>
|
|
62
62
|
<ContainerInput>
|
|
63
|
-
|
|
63
|
+
{ /* Edited: Claude - Date: 2026-08-19
|
|
64
|
+
aria-label rather than a <label for>, because this field has
|
|
65
|
+
no visible label to attach - the placeholder is the only
|
|
66
|
+
wording, and a placeholder disappears the moment you type. */ }
|
|
67
|
+
<input type="text" aria-label={ t('routes_order.step5.coupon.code') } placeholder={ t('routes_order.step5.coupon.code') } { ...register('code', Validate('required|min_length:4', t)) } />
|
|
64
68
|
|
|
65
69
|
<Apply
|
|
66
70
|
type="submit"
|