@autobusal/routes-order 1.9.0 → 1.9.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.
- package/CHANGELOG.md +673 -0
- package/Checkout/Checkout.tsx +519 -0
- package/Checkout/Sidebar/Sidebar.tsx +124 -0
- package/Checkout/Sidebar/styles.ts +52 -0
- package/Facts/Facts.tsx +271 -0
- package/Facts/questions.ts +250 -0
- package/Facts/service.ts +111 -0
- package/Facts/services.ts +19 -0
- package/Facts/styles.ts +112 -0
- package/Facts/types.ts +167 -0
- package/Faq/Faq.tsx +52 -0
- package/Faq/styles.ts +68 -0
- package/Found/Dates/Dates.tsx +59 -5
- package/Found/Dates/styles.ts +137 -7
- package/Found/Empty/Empty.tsx +136 -0
- package/Found/Empty/styles.ts +98 -0
- package/Found/Filters/Filters.tsx +10 -7
- package/Found/Filters/styles.ts +79 -8
- package/Found/Found.tsx +128 -29
- package/Found/Group/Group.tsx +3 -1
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +25 -4
- package/Found/Picks/Picks.tsx +10 -3
- package/Found/Picks/styles.ts +60 -9
- package/Found/Route/Route.tsx +189 -33
- package/Found/Route/styles.ts +488 -50
- package/Found/SameDay/SameDay.tsx +35 -0
- package/Found/SameDay/styles.ts +27 -0
- package/Found/Sort/styles.ts +52 -7
- package/Found/Summary/Summary.tsx +1 -1
- package/Found/Summary/styles.ts +12 -0
- package/Found/Watch/Watch.tsx +187 -0
- package/Found/Watch/styles.ts +118 -0
- package/Found/refine.ts +69 -0
- package/Links/Links.tsx +91 -0
- package/Links/styles.ts +75 -0
- package/RoutesOrder.tsx +239 -31
- package/Schedule/Schedule.tsx +123 -0
- package/Schedule/styles.ts +71 -0
- package/Sections/Schema.tsx +186 -0
- package/Sections/Sections.tsx +160 -0
- package/Sections/styles.ts +44 -0
- package/Step1/prepare.ts +21 -2
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +253 -24
- package/Step4/Passenger/styles.ts +21 -1
- package/Step4/Passengers/Display.tsx +10 -2
- package/Step4/Passengers/Passengers.tsx +53 -4
- package/Step4/Saved/Choose.tsx +99 -0
- package/Step4/Saved/Remember.tsx +58 -0
- package/Step4/Saved/styles.ts +87 -0
- package/Step4/checkAge.ts +15 -4
- package/Step5/Addons/Addons.tsx +146 -4
- package/Step5/Addons/styles.ts +94 -0
- package/Step5/Billing/Billing.tsx +138 -70
- package/Step5/Coupons/Coupons.tsx +28 -18
- package/Step5/Coupons/styles.ts +41 -8
- package/Step5/Payments/Payments.tsx +36 -5
- package/Step5/Payments/styles.ts +16 -7
- package/Step5/Summary/About/About.tsx +99 -27
- package/Step5/Summary/About/styles.ts +118 -1
- package/Step5/Summary/styles.ts +12 -5
- package/Step5/utilities.ts +67 -12
- package/Steps/Steps.tsx +3 -1
- package/commerce.ts +24 -0
- package/package.json +1 -1
- package/services.ts +295 -12
- package/styles.ts +126 -0
- package/types.ts +78 -1
- package/Found/Booking/Booking.tsx +0 -61
- package/Found/Booking/styles.ts +0 -50
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,684 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.1 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- The return date falls back to the outbound when the URL carries no real one ('none' on a one-way link), so switching a collapsed search to Round Trip opens the calendar on the month being travelled and cannot offer a day before the departure.
|
|
6
|
+
- The collapsed search panel sits above the step markers: its backdrop-filter creates a stacking context, so the date picker used to draw underneath them.
|
|
7
|
+
|
|
8
|
+
## 1.33.9 (2026-08-28)
|
|
9
|
+
|
|
10
|
+
- Return-leg empty state fetches RETURN alternatives (swapped pair + return date) and its suggested-day links fill the return slot instead of rewriting the departure; nearby-city swaps map back to the URL's outbound pair (sweep #50).
|
|
11
|
+
- Checkout money parsing survives the server's space-thousands format: currency is the last token, coupon amounts strip grouping spaces - totals over 1000 no longer corrupt the rendered total or the GA4 values (sweep #51).
|
|
12
|
+
- useGetFlex takes the coupon-adjusted total so the quoted flex price matches what the server bills on a couponed booking (sweep #53).
|
|
13
|
+
- Passenger age bands match the server rules: adult 12+, child 2-12, baby under 2 (sweep #48).
|
|
14
|
+
|
|
15
|
+
## 1.33.8 (2026-08-28)
|
|
16
|
+
|
|
17
|
+
- usePostOrder replays the stored referral token as `referral`, so widget/link arrivals attribute their affiliate at checkout.
|
|
18
|
+
|
|
19
|
+
## 1.33.7 (2026-08-26)
|
|
20
|
+
|
|
21
|
+
- Seat-selection add-on: checkout reads settings addons.seats - picking turns optional with the fee on the CTA and a live summary line; submission strips unpicked (zero) seat fields and sends addon_seats only when a hand-picked seat remains. Fixes the return-leg picked-seat list rebuilding from the departure's on re-pick.
|
|
22
|
+
|
|
23
|
+
## 1.33.6
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **The return date strip asks the API for return-leg pricing** (new leg parameter) - it used to quote one-way fares over days the buyer pays the round-trip difference for. (Audit BOOK-02-b; needs obtapi >= 2.4.63.)
|
|
28
|
+
|
|
29
|
+
## 1.33.5
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **The summary's TICKET line shows the gross fare.** With a coupon it used to show the discounted figure AND a discount line - "TICKET 40 / DISCOUNT -5 / TOTAL 40", arithmetic that doesn't add up on screen. (Audit BOOK-09-b.)
|
|
34
|
+
|
|
35
|
+
## 1.33.4
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Complimentary in the web checkout.** An operator/employee selling on their OWN operator's route is offered the zero-cost Complimentary option the mobile app already had - the route-aware flag comes from `/api/funds/allowed?route=...` and the ownership rule is re-checked server-side at payment time. (Audit decision BOOK-07; needs obtapi >= 2.4.37.)
|
|
40
|
+
|
|
41
|
+
## 1.33.3
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Checkout swallowed every booking error.** A 4xx from order creation - a seat taken while the form was open, the unpaid-hold limit, a route gone unavailable - left the buyer on the filled form with no message. It now surfaces the API's message. (Audit BOOK-13-c.)
|
|
46
|
+
|
|
47
|
+
## 1.33.2
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **A signed-in customer with an incomplete profile is now asked for what is missing**, in the same billing fields a guest already fills, instead of reaching an API that had nothing to put in three NOT NULL columns. Only the gaps are asked for: a complete profile sees no change at all.
|
|
52
|
+
|
|
53
|
+
This is not a rare state. A self-registered visitor has a name and nothing else, and the form that collects phone, address and city is reachable only by admin-created accounts — so the customer could not have completed it in advance even if asked to.
|
|
54
|
+
|
|
55
|
+
- `Billing` takes an optional `only` list; undefined keeps the whole guest block exactly as it was. Field rules follow whichever profile the value will be stored on, so the form never accepts more than the API will keep.
|
|
56
|
+
|
|
57
|
+
- Checkout treats "profile not loaded yet" as unknown rather than empty — the user store persists a redacted stub and re-fetches on mount, so the other reading would flash required fields at customers whose profile is complete.
|
|
58
|
+
|
|
59
|
+
## 1.33.1
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **A pair with no future scheduled service says so**, with the date it last ran, instead of showing a facts block that quietly omits frequency and a departure list that is empty on every date the visitor tries. The links to journeys that do run stay prominent, so the page converts rather than dead-ends. No `noindex`: an honest page about a seasonal route is not thin content, and a route that stopped in December will want its ranking back in June.
|
|
64
|
+
|
|
65
|
+
## 1.33.0
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **The pair page is a page about the journey, not just a search box over it.** `/routes/search/facts` now carries how often the pair is served (the union of days a traveller can leave on, plus departures per week), how many of the scheduled services run non-stop, how many travel overnight, the cities in between, what is on board, the two countries, and the dearest fare alongside the cheapest - all read off the timetable, all riding the existing 6h cache, none of it estimated.
|
|
70
|
+
- Every list-shaped fact carries an `every` flag beside it, and the copy is chosen from that flag. Six of the ten Tirana-Thessaloniki coaches are non-stop and they carry different equipment, so "every bus calls at Korca" and "some buses also call at Korca" are different claims; obtapi says which one the data supports rather than leaving the wording to guess.
|
|
71
|
+
- **A visible FAQ** (`Faq/`), built from `Facts/questions` - the same array the `FAQPage` structured data is emitted from, so the answer Google quotes is the string the page renders, character for character, in all fifteen languages. Native `<details>`, no state: the answer is in the DOM whether it is open or shut, which is what a prerendered snapshot and a crawler that does not click both need. Questions with no data behind them are not asked.
|
|
72
|
+
- **`BusTrip` gained an `itinerary`** - but only where every service on the pair calls at every stop listed. An itinerary is a statement about *the* trip, and on a pair where most coaches run non-stop there is no single itinerary to state. Departure and arrival stops carry a `PostalAddress` where obtapi knows the country.
|
|
73
|
+
- **A "travelling back?" link**, from the new `links.reverse`. Pairs are directional, so the return leg is a different page with its own timetable - and it belonged to neither "popular from" nor "popular to", being a pair from the destination back to the origin. Null, and absent, when nobody sells it.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- **"3h 0m" in the facts grid, "3h" in the answer below it.** The facts block carried its own copy of the duration formatter without the whole-hour rule the FAQ answers use, so a three-hour coach was described two ways on one page. One exported formatter now.
|
|
78
|
+
- **The departure count called itself the wrong thing.** `facts.departures` counts the distinct coaches in the timetable, and a single daily one of them makes seven departures a week - so "10 scheduled departures" beside the new "every day, 70 scheduled departures a week" read as a contradiction rather than as two facts. The label and the FAQ answer now say *scheduled services* in all fifteen languages; the number they print is unchanged.
|
|
79
|
+
- **"There are 1 scheduled departures"** - the sentence a thin pair got from the FAQ schema for as long as the block has existed, and a thin pair is most of them. A separate key rather than an i18next plural rule; the plural categories of the eight Slavic locales here are not something one English-shaped rule gets right.
|
|
80
|
+
|
|
81
|
+
## 1.32.1
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- **A saved-passenger chooser and a "remember this passenger" checkbox**, per passenger rather than per order, because "remember me but not the person I am buying for" is the ordinary case and one control per booking cannot say it. The list is fetched once for the whole checkout, not once per traveller.
|
|
86
|
+
|
|
87
|
+
Picking applies the whole person, not the filled parts: a passport left from a previous choice is cleared, because the customer has said who is sitting in that seat. Only fields the route actually collects are written - react-hook-form submits values set for names it never registered, so writing the rest would post a WhatsApp number to a route that never asked for one.
|
|
88
|
+
|
|
89
|
+
A journey that needs a field the saved profile lacks says so in the form's own words. `missing` absent means *not measured* and is not treated as "nothing missing".
|
|
90
|
+
|
|
91
|
+
## 1.32.0
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- **"Watch this fare" on the results page.** An inline control at the foot of the results asks for an email address and arms a price alert on the pair and date already searched (`POST /api/alerts/price/email`). It renders from `<Found>`, which is the one component mounted in every outcome a search can have - trips listed, every trip hidden by a filter, and nothing found at all; "no buses for this search" is exactly when somebody wants telling if that changes, so a control living inside the listing would have been missing where it matters most. On the return leg the pair is reversed and the return date used, the same swap `useGetDates` makes.
|
|
96
|
+
- Every answer the endpoint can give is said out loud rather than swallowed: the confirmation quotes `baseline_price_display` so the customer knows which number is being watched, a 404 says plainly that nobody prices this pair, a 422 names the address, and a 429 asks for a minute. `retry: false` - none of those three get better by being asked again.
|
|
97
|
+
- The email field has a real accessible name (`<label htmlFor>` over a `useId`-prefixed id), matching what the checkout and `@autobusal/auth` were fixed to do. No threshold field: "tell me if it drops" needs no number, and a second input is how a one-line control becomes a form nobody fills in. No `token` field either - obtapi validates it as `prohibited` here, it belongs to the device half of the same feature.
|
|
98
|
+
|
|
99
|
+
## 1.31.5
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
|
|
103
|
+
- **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.
|
|
104
|
+
|
|
105
|
+
## 1.31.4
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
|
|
109
|
+
- **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.
|
|
110
|
+
- 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.
|
|
111
|
+
|
|
112
|
+
## 1.31.3
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **The results page's facts/schedule block now renders on the very first screen**, not only once a date has been picked. `/bus-lines/:from/:to` - the dateless URL the sitemap lists and the prerender crawl snapshots - was always on step 1, so `<Sections>` (price/duration/departures + the per-route schedule table + the pair's JSON-LD) never mounted for a single search engine or non-JS crawler. It now also renders on step 1; the bare `/bus-lines` search form (no pair in the URL) is unaffected since it has nothing to fetch.
|
|
117
|
+
|
|
118
|
+
## 1.31.2
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- **The route card and booking summary show the operator's `display_name`**, not `company` - an operator that set a brand name (`@autobusal/providers` 1.34.0) now appears under that name in search results and the booking summary, same as everywhere else it's now shown.
|
|
123
|
+
|
|
124
|
+
## 1.31.1
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- **One payment method means one button.** The picker no longer renders when
|
|
129
|
+
there is nothing to pick, and the button reads "Pay Now" rather than "Make
|
|
130
|
+
Payment" - a heading over a single option the buyer had to press before
|
|
131
|
+
pressing the real one was a click that only ever had one answer, on the
|
|
132
|
+
last screen before paying.
|
|
133
|
+
|
|
134
|
+
## 1.31.0
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- **The checkout Order Summary stacks the legs vertically and numbers them**
|
|
139
|
+
— "Trip 1" above the outbound, "Trip 2" above the return.
|
|
140
|
+
|
|
141
|
+
They sat side by side from 768px up, which was written when the summary
|
|
142
|
+
spanned the page. It has lived in the checkout *sidebar* since 1.11.0 — a
|
|
143
|
+
narrow column — so two legs abreast left each about half the width a single
|
|
144
|
+
line like "Terminali Lindor Interurban dhe Nderkombetar i Autobusave (TEG)"
|
|
145
|
+
needs, and nothing said which was which. Vertical at every width now, in the
|
|
146
|
+
order they are travelled.
|
|
147
|
+
|
|
148
|
+
**The number is only rendered on a round trip.** A one-way has nothing to
|
|
149
|
+
enumerate, and a lone "Trip 1" would send the reader looking for a Trip 2
|
|
150
|
+
that was never bought. `Checkout/Sidebar` is the one component that knows
|
|
151
|
+
whether a second leg exists, so it is the one that decides — `About` just
|
|
152
|
+
takes an optional `number`.
|
|
153
|
+
|
|
154
|
+
"Trip" rather than "Segment": segment is airline jargon, and this is the
|
|
155
|
+
last screen before somebody pays for a bus ticket.
|
|
156
|
+
|
|
157
|
+
## 1.30.1
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- **Step 3 could never list a single return leg.** `useGetReturns` spread the
|
|
162
|
+
search form straight into the query string, so the return date went over as
|
|
163
|
+
`_return` — the form's name for it, because `return` is a reserved word in
|
|
164
|
+
JS — while the API validates `return`. Every request answered
|
|
165
|
+
`422 The return field is required`, and "Select a Return Schedule" was empty
|
|
166
|
+
for every round trip ever searched. The order builder in the same file has
|
|
167
|
+
always done this mapping (`order.return = step1._return`); only this call
|
|
168
|
+
was missed.
|
|
169
|
+
|
|
170
|
+
Paired with an obtapi fix — `Routes\Search\Returns::find()` returned nothing
|
|
171
|
+
on its success path — so the step was broken twice over and neither failure
|
|
172
|
+
could be seen past the other.
|
|
173
|
+
|
|
174
|
+
## 1.30.0
|
|
175
|
+
|
|
176
|
+
### Added
|
|
177
|
+
|
|
178
|
+
- **The SMS booking-confirmation add-on at checkout**, with its own number
|
|
179
|
+
field prefilled from the passenger phone already collected — somebody
|
|
180
|
+
booking for a relative wants the text to reach the traveller. The
|
|
181
|
+
description says the free route alerts are not what is being sold here.
|
|
182
|
+
- **A line under the passenger phone field** saying what the number is used
|
|
183
|
+
for: reaching you about this trip, and service messages if the departure
|
|
184
|
+
changes — never marketing. The number was always collected so a driver
|
|
185
|
+
could ring it; texting it is a different use of the same data, and saying
|
|
186
|
+
so at the point of collection is what makes it consent rather than an
|
|
187
|
+
assumption.
|
|
188
|
+
## 1.29.0
|
|
189
|
+
|
|
190
|
+
### Removed
|
|
191
|
+
|
|
192
|
+
- **The reviews block on the route-pair page.** A review wall sat in the
|
|
193
|
+
middle of a page whose job is to compare departures. What a reader needs
|
|
194
|
+
there about reputation is one number per operator, which the result cards
|
|
195
|
+
already carry; the reviews themselves belong on the operator's own page,
|
|
196
|
+
where somebody who wants to read them has gone looking for them. The `pair`
|
|
197
|
+
mode of the reviews API is untouched — nothing calls it from here now.
|
|
198
|
+
- The "Reviews" entry in the in-page nav, with it.
|
|
199
|
+
|
|
200
|
+
## 1.28.0
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
|
|
204
|
+
- **Overnight trips now say so.** A result card that arrives on a later day
|
|
205
|
+
prints the calendar day under BOTH ends and a `+1` against the arrival
|
|
206
|
+
time; the pair-page timetable carries the same marker. "22:30 → 06:00" is
|
|
207
|
+
the one thing on a result card that can be read as the exact opposite of
|
|
208
|
+
what it means, and the duration beside it does not fix that because nobody
|
|
209
|
+
reads a duration to work out a date.
|
|
210
|
+
- Nothing is added to a same-day card. A date under every arrival would make
|
|
211
|
+
the overnight ones stop standing out, which is the only reason this exists.
|
|
212
|
+
|
|
213
|
+
### Changed
|
|
214
|
+
|
|
215
|
+
- The day comes from the timetable's own midnight markers, computed
|
|
216
|
+
server-side, never from "the arrival looks earlier than the departure" —
|
|
217
|
+
which is wrong for exactly the cross-border journeys whose two ends keep
|
|
218
|
+
different clocks.
|
|
219
|
+
|
|
220
|
+
## 1.27.0
|
|
221
|
+
|
|
222
|
+
**`reloadDocument` is gone — the results follow the URL.**
|
|
223
|
+
|
|
224
|
+
The search lives on `Step1`, which is **not mounted** once results are on screen. So a link that changed the address bar was read by nobody: the URL said one journey and the results underneath still showed another. That is why the empty-search suggestions threw away the whole application on every click.
|
|
225
|
+
|
|
226
|
+
`RoutesOrder` now watches the search spec the URL describes and re-runs when it changes. Keyed on **the spec**, not on a "have we searched yet" flag — a flag can only answer once, a key re-answers whenever the question changes and stays quiet while it does not. It cannot loop either: writing state does not alter the URL the key derives from. The first search is still Step1's, so it does not run twice.
|
|
227
|
+
|
|
228
|
+
Choosing a different search also clears the previously chosen coach. Leaving it set would let the step guards resolve to a checkout for a route the buyer never picked.
|
|
229
|
+
|
|
230
|
+
Verified client-side: navigating from Tirana → Thessaloniki (10 trips) to Durres → Tirana updated the header and the results with the page never reloading.
|
|
231
|
+
|
|
232
|
+
## 1.26.0
|
|
233
|
+
|
|
234
|
+
**Funnel events on every step**: the result list, the coach chosen, checkout reached, the Flexible Ticket ticked or unticked, and the value stashed for the purchase event.
|
|
235
|
+
|
|
236
|
+
- `view_item_list` is keyed on the **result set**, not on render — this component re-renders on every filter tick and sort change, and announcing again each time would report a dozen searches for one.
|
|
237
|
+
- `begin_checkout` lives in `Checkout` rather than `RoutesOrder`, because that screen already derives the currency from its own formatted total (the public settings payload deliberately carries none) and it mounts exactly once per arrival.
|
|
238
|
+
- The **add-on attach rate** is the one input the Flexible Ticket pricing decision has been waiting on — it cannot be banded by window without knowing how often each window is actually bought.
|
|
239
|
+
- One `item()` mapping shared by every event, keyed on the **route code** rather than the numeric id, so a report can be reconciled against something a human recognises.
|
|
240
|
+
|
|
241
|
+
## 1.25.0
|
|
242
|
+
|
|
243
|
+
**Reviews on the route-pair page.** A new section between the timetable and the internal links, with its own nav anchor.
|
|
244
|
+
|
|
245
|
+
Renders nothing below the display threshold, so a pair without enough reviews does not gain an empty box.
|
|
246
|
+
|
|
247
|
+
## 1.24.0
|
|
248
|
+
|
|
249
|
+
**The booking wizard has real URLs.** The step now lives in the URL and `current` is derived from it, rather than the reverse.
|
|
250
|
+
|
|
251
|
+
Before: the wizard held the step in component state and never touched the address bar, so the URL still said "search" while the buyer was on checkout. Nothing in the flow was shareable, a refresh dropped you back to the search, and Back did not step back through the wizard — it left it entirely, because no step had ever created a history entry.
|
|
252
|
+
|
|
253
|
+
- A **search param** (`?step=checkout` / `?step=return`), not a path segment. `/bus-lines/:from/:to` is a crawlable page in the sitemap, and child routes would put near-identical URLs in front of a crawler for what is UI state. The search spec is already in the path on the 9-segment route, so between the two the whole position is in the URL.
|
|
254
|
+
- **Guards, not a lookup.** A step is honoured only when the state it needs exists: a cold `?step=checkout` has no chosen route, so it falls back to results and clears the stale param rather than rendering half a screen.
|
|
255
|
+
|
|
256
|
+
**Bug fixed on the way.** `Checkout` chose its back target with `step1._return ? 3 : 2`. On the 9-segment URL a one-way trip carries the literal segment `none` as its return date — a truthy string — so **every one-way booking's Back button aimed at the return-leg step**, which that booking does not have. Now keyed on `step1.type`, and guarded again in `onBack` so a step nobody can honour never reaches the URL.
|
|
257
|
+
|
|
258
|
+
Verified: results → checkout writes the param; browser Back and Forward move between them; the in-page Back returns to results; a cold checkout URL falls back; and eight back/forward transitions produced no blank frame and no console error.
|
|
259
|
+
|
|
260
|
+
## 1.23.5
|
|
261
|
+
|
|
262
|
+
**The operator cell centres its contents at every width.** LinkCompany already carried an auto horizontal margin, so the logo was centred in the cell whatever the screen - but the rating and the route code sat at flex-start, under the left edge of a box whose logo was in the middle of it. On a 768px card that was a ~300px gap between the logo and the code below it.
|
|
263
|
+
|
|
264
|
+
Measured after: logo and chip centres agree to within 1px (sub-pixel rounding) at 768px and 1400px, with no cell overflow and no sideways page scroll.
|
|
265
|
+
|
|
266
|
+
## 1.23.4
|
|
267
|
+
|
|
268
|
+
**The route code is a chip rather than a line of grey text.** Rounded, on the neutral background, bold and letter-spaced, with a route glyph (MdOutlineAltRoute) leading it and the word "Route" as its tooltip.
|
|
269
|
+
|
|
270
|
+
A route symbol rather than a bus or a ticket: the code identifies which LINE this is - the same job a flight number does - and a bus icon would only repeat what the whole card already says. The glyph is a size larger than the text beside it, because at this scale the icon carries the meaning and the letters are just the value.
|
|
271
|
+
|
|
272
|
+
Albanian `routes_order.step2.route.name` corrected from "Rruga" (a street) to "Linja" (a line). The key's only consumer is now this tooltip, so it was safe to fix.
|
|
273
|
+
|
|
274
|
+
## 1.23.3
|
|
275
|
+
|
|
276
|
+
**The arrival heading was painting over the price.** Measured: the arrival cell is 165px wide and its content needed **215px**, so it ran 50px into the fare beside it.
|
|
277
|
+
|
|
278
|
+
Two causes, both fixed — widths alone would only have moved the width at which it broke:
|
|
279
|
+
|
|
280
|
+
- **No spaces to wrap at.** JSX drops the whitespace around a newline, so city, country and time were concatenated into one unbreakable token. Real spaces added, plus overflow-wrap: break-word above 640px as a backstop for a long city name on its own line.
|
|
281
|
+
- **The operator cell was taking room it did not need.** 185px to 120px with a smaller logo, and the one-row gap from 15px to 10px. That returns 85px to the two location columns, which go 165px to 208px.
|
|
282
|
+
|
|
283
|
+
Verified no overflow on any card at 1024 / 1100 / 1280 / 1440 / 1920px. 1024 is the tightest - it is where the card first lays out in a single row.
|
|
284
|
+
|
|
285
|
+
**The route code moved out of the collapsed details and under the logo**, where it sits beside the operator it identifies. That frees a slot in the details row for things that actually need explaining - customs on a cross-border run, for one.
|
|
286
|
+
|
|
287
|
+
The route information link is now **Route Info** rather than "Read Operator Information".
|
|
288
|
+
|
|
289
|
+
## 1.23.2
|
|
290
|
+
|
|
291
|
+
- Flexible Ticket detail is now behind a small **Read More** link rather than the title. Its own control, so the thing you click to *read* is never next to the thing you click to *buy*. Closed by default; one line — "without it, this ticket cannot be cancelled or changed" — stays outside the collapse, because that is the material fact and hiding it would be a dark pattern rather than a tidy layout.
|
|
292
|
+
- **Date strip: dropped the joining comma.** Above 1024px the day number carried a `::before` comma so the cell would read "Mon, 03 Aug" on one line. The cells are narrow enough that the two parts wrap anyway, so the comma led the second line and every desktop cell rendered as "Mon" over ", 03 Aug". Punctuation that only works when text happens not to wrap will keep breaking.
|
|
293
|
+
|
|
294
|
+
## 1.23.1
|
|
295
|
+
|
|
296
|
+
**Flexible Ticket as a title, a price and an Add button, with the detail collapsed.**
|
|
297
|
+
|
|
298
|
+
A button rather than a checkbox, because this is not the same kind of choice as the notification add-ons beside it — those are a cheap tick, this one changes what the ticket *is*. The toggle and the Add button are separate controls on purpose: reading the terms must never be a way to accidentally buy them.
|
|
299
|
+
|
|
300
|
+
One line stays **outside** the collapse — "without it, this ticket cannot be cancelled or changed". Everything else is detail somebody can choose to read, but that is the material fact, and hiding what a buyer gets for nothing behind a toggle is the shape of a dark pattern rather than a tidy layout.
|
|
301
|
+
|
|
302
|
+
## 1.23.0
|
|
303
|
+
|
|
304
|
+
**Flexible Ticket at checkout.**
|
|
305
|
+
|
|
306
|
+
- New `useGetFlex(departurePriceId, returnPriceId, adults, children)`. Its own request rather than a field on the search result, because the answer depends on **both legs at once**: an operator who will not amend one half removes the add-on from the whole booking, and the price is computed on the combined fare.
|
|
307
|
+
- `Addons` renders the option below passenger details, where it already sat. It shows the **default first** — "without it, this ticket cannot be cancelled or changed" — then only the capabilities this booking actually includes, then that the fee itself is not refunded, then that statutory rights when the *operator* cancels are untouched. Nothing renders at all when the offer is unavailable: a checkbox that unlocks nothing is worse than no checkbox.
|
|
308
|
+
- `AddonsSelection` gains `flex`; the order payload gains `addon_flex`.
|
|
309
|
+
- `Found/Route` reads `routes_order.step2.route.information.*` after the `policies` rename.
|
|
310
|
+
|
|
311
|
+
Copy is **Flexible Ticket** / **Biletë Fleksibël**, never insurance — the distinction decides how the line is taxed and whether selling it needs a licence.
|
|
312
|
+
|
|
3
313
|
All notable changes to this package are documented here. This project follows
|
|
4
314
|
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
315
|
|
|
6
316
|
> Note: 1.8.0 was published without a changelog entry. The gap is left as-is
|
|
7
317
|
> rather than reconstructed after the fact.
|
|
8
318
|
|
|
319
|
+
## 1.22.0
|
|
320
|
+
|
|
321
|
+
Structured data on route-pair pages: `BusTrip` with its operators and
|
|
322
|
+
fares-from, and a `FAQPage` answering the five things people actually search
|
|
323
|
+
for — how long, how much, how many a day, when the first and last leave, and
|
|
324
|
+
who runs it.
|
|
325
|
+
|
|
326
|
+
One rule governs all of it: **never state anything the page does not show.**
|
|
327
|
+
Every value is the same one rendered in the facts block or the timetable a few
|
|
328
|
+
hundred pixels below. A rich result built on a claim a visitor cannot find is
|
|
329
|
+
the mismatch that gets a site's snippets pulled, and it is also just a lie told
|
|
330
|
+
to a machine.
|
|
331
|
+
|
|
332
|
+
The rating hangs off the **provider**, not the trip. An operator's rating is
|
|
333
|
+
earned across everything they run; attaching it to one city pair would claim
|
|
334
|
+
travellers rated this journey when they rated the company. Operators below the
|
|
335
|
+
review display threshold carry no rating in the markup, exactly as they carry
|
|
336
|
+
none on the page.
|
|
337
|
+
|
|
338
|
+
## 1.21.1
|
|
339
|
+
|
|
340
|
+
No horizontal scroll on a tablet's date strip. The 900px floor belongs to the
|
|
341
|
+
seven-day layout, which only appears from 1024px — applied from 640px it made
|
|
342
|
+
a tablet showing five days scroll sideways for a row that fitted comfortably,
|
|
343
|
+
because the strip was reserving width for two cells it was not rendering.
|
|
344
|
+
|
|
345
|
+
## 1.21.0
|
|
346
|
+
|
|
347
|
+
The same-day notice no longer hedges about timezones. obtapi now holds a zone
|
|
348
|
+
per country, so "has this coach already left" is judged against the departure
|
|
349
|
+
stop's own clock — the warning is about the real remaining risk, which is
|
|
350
|
+
whether a traveller can reach the terminal in time.
|
|
351
|
+
|
|
352
|
+
## 1.20.0
|
|
353
|
+
|
|
354
|
+
The date strip: three days on a phone, five on a tablet, seven on a desktop —
|
|
355
|
+
and the label becomes "04 AUG" below desktop rather than "Tue 04".
|
|
356
|
+
|
|
357
|
+
Seven fitted on a phone once the labels were trimmed, but only at ~36px a
|
|
358
|
+
cell: readable, not comfortable to hit, with the price wrapping to three lines
|
|
359
|
+
under it. Each step up in width now buys two more days instead of squeezing
|
|
360
|
+
the same seven into less room.
|
|
361
|
+
|
|
362
|
+
The weekday is what gives way, not the month. With three days showing, a date
|
|
363
|
+
needs to say which date it is more than which weekday — "04 AUG" is
|
|
364
|
+
unambiguous alone, while "Tue 04" leaves somebody counting forward to work out
|
|
365
|
+
whether that is this week or next. On a full seven-day strip the weekday earns
|
|
366
|
+
its place, because the row reads as a week; on three days it does not.
|
|
367
|
+
|
|
368
|
+
## 1.19.0
|
|
369
|
+
|
|
370
|
+
Same-day travel, and the last of the phone layout.
|
|
371
|
+
|
|
372
|
+
**Today is bookable**, so today's results carry a countdown — "Leaves in 43
|
|
373
|
+
min", turning red under the hour. Computed server-side: a countdown built from
|
|
374
|
+
the visitor's own clock is wrong for anybody whose device is off or in another
|
|
375
|
+
timezone, and that is exactly the claim that must not be wrong.
|
|
376
|
+
|
|
377
|
+
**A same-day caution above the results.** A coach that has already gone is
|
|
378
|
+
filtered out server-side, but "already gone" is judged against our clock and
|
|
379
|
+
the system holds no timezone for a city — so on a cross-border route the
|
|
380
|
+
stored departure time and our idea of now can be an hour apart. The notice asks
|
|
381
|
+
the traveller to check against their own local time.
|
|
382
|
+
|
|
383
|
+
**Five days on a phone, seven above it.** Seven fitted after the labels were
|
|
384
|
+
trimmed, but only at ~36px a cell. The five are centred on the selected day and
|
|
385
|
+
clamped at both ends, so shrinking never hides the day being looked at.
|
|
386
|
+
|
|
387
|
+
**Fare and button share a row**, tax note beneath — reordered in CSS, because
|
|
388
|
+
the source order (fare, note, button) is the right one to read aloud and the
|
|
389
|
+
right one for a crawler: the note explains the number it follows.
|
|
390
|
+
|
|
391
|
+
## 1.18.0
|
|
392
|
+
|
|
393
|
+
The results page on a phone, rebuilt for density.
|
|
394
|
+
|
|
395
|
+
**The three picks are one strip.** Cheapest, fastest and recommended stacked
|
|
396
|
+
into three full-width bands before a single result — the thing that summarises
|
|
397
|
+
the list was pushing the list off the screen. Side by side they are comparable
|
|
398
|
+
at a glance, which is what they are for. The operator's name drops below 640px:
|
|
399
|
+
at ~110px a card it would ellipsis to three characters.
|
|
400
|
+
|
|
401
|
+
**All five sort options on one row.** They were wrapping onto three lines,
|
|
402
|
+
taking more height than the first result they reorder. Sized to their own text
|
|
403
|
+
rather than equal shares — equal shares gave all five 59px, which four did not
|
|
404
|
+
need and "Recommended" did, so it alone was truncated to "Recom..." while its
|
|
405
|
+
neighbours sat in whitespace.
|
|
406
|
+
|
|
407
|
+
**The seven-day strip no longer scrolls sideways.** A 900px floor made half the
|
|
408
|
+
week invisible on a phone, including whichever day was cheapest — and a week is
|
|
409
|
+
a shape you read at a glance, not one you drag. The month is dropped from each
|
|
410
|
+
label (it is identical across a seven-day window) and the week arrows are
|
|
411
|
+
squeezed; the day prices wrap rather than painting over each other.
|
|
412
|
+
|
|
413
|
+
**Details collapse.** Features, route, transit and policies were four stacked
|
|
414
|
+
bands on every result. They are one wrapped row behind a toggle now, hidden
|
|
415
|
+
with CSS rather than unmounted so the prerendered snapshot a crawler reads is
|
|
416
|
+
unchanged.
|
|
417
|
+
|
|
418
|
+
**The trip stacks vertically** with the arrow turned to point down. Side by
|
|
419
|
+
side was tried first and does not survive the content: "Thessaloniki (GRC)
|
|
420
|
+
16:30" measures 215px in a 96px column and painted 50px outside the card — the
|
|
421
|
+
page never scrolled sideways, so nothing looked broken until it was measured.
|
|
422
|
+
|
|
423
|
+
## 1.17.0
|
|
424
|
+
|
|
425
|
+
Internal linking blocks: "Popular routes from Tirana" / "Popular routes to
|
|
426
|
+
Thessaloniki", as real router links, because the entire value is that a
|
|
427
|
+
crawler follows them.
|
|
428
|
+
|
|
429
|
+
The sitemap already announces every sellable pair, but a sitemap is a hint —
|
|
430
|
+
a page nothing links to is still orphaned. These put every pair one click from
|
|
431
|
+
another.
|
|
432
|
+
|
|
433
|
+
Ordered by paid bookings, never filtered by them: the pairs with no orders yet
|
|
434
|
+
are exactly the orphans this exists to reach. The block also survives a pair
|
|
435
|
+
with no facts, which is precisely where a reader most needs a way onward.
|
|
436
|
+
|
|
437
|
+
## 1.16.0
|
|
438
|
+
|
|
439
|
+
The crawlable timetable, and a section layout around the wizard.
|
|
440
|
+
|
|
441
|
+
Every service on the pair as a real `<table>` — departs, arrives, duration,
|
|
442
|
+
stops, operator with its rating, price — sorted by departure time. This is the
|
|
443
|
+
part of Tier 3 that actually ranks: dense, unique to the pair, and made of the
|
|
444
|
+
words somebody types, without a sentence of written copy. No interactivity, so
|
|
445
|
+
a prerendered snapshot shows a non-JS crawler exactly what a reader sees.
|
|
446
|
+
|
|
447
|
+
A sticky in-page nav (Trips · Facts · Timetable) so somebody who landed from a
|
|
448
|
+
search engine can reach the timetable without scrolling past a booking form
|
|
449
|
+
they have not decided to use. The wizard itself is untouched — this is a
|
|
450
|
+
layout around it, not a change to the funnel.
|
|
451
|
+
|
|
452
|
+
Facts and the timetable share ONE request. Two components each fetching the
|
|
453
|
+
same response would have doubled what the prerender crawl costs on every pair.
|
|
454
|
+
|
|
455
|
+
No "type" or "class" column, which the roadmap asked for: every route in this
|
|
456
|
+
system is typed `international` and the bus names are inventory identifiers
|
|
457
|
+
like "A100". A column that says nothing on every row reads as padding.
|
|
458
|
+
|
|
459
|
+
## 1.15.0
|
|
460
|
+
|
|
461
|
+
Distance is now measured along the route's own stops rather than as one line
|
|
462
|
+
from origin to destination, and the facts payload carries two numbers instead
|
|
463
|
+
of one: `road`, real driving distance from a routing service, and `approx`,
|
|
464
|
+
the straight lines summed stop by stop.
|
|
465
|
+
|
|
466
|
+
Real road distance prints plainly. The approximation prints with a caveat,
|
|
467
|
+
because it understates every journey — 309 km against 401 km of actual tarmac
|
|
468
|
+
on Tirana–Thessaloniki via Korça, a 23% shortfall. A straight line is the
|
|
469
|
+
shortest path there is, so no number of extra points can ever close that gap.
|
|
470
|
+
|
|
471
|
+
## 1.14.0
|
|
472
|
+
|
|
473
|
+
The facts block on a route-pair page: fares from, journey time, first and last
|
|
474
|
+
departure, how many run, distance and who operates it.
|
|
475
|
+
|
|
476
|
+
Read off the timetable rather than a dated search, so it does not change with
|
|
477
|
+
the date in the URL — a "from EUR 12" that moved depending on which day a
|
|
478
|
+
crawler arrived would look unstable to a search engine and dishonest to a
|
|
479
|
+
reader who came back.
|
|
480
|
+
|
|
481
|
+
Distance is labelled "straight line" because that is what it is: we hold city
|
|
482
|
+
coordinates, not routing data, so the figure is always shorter than the road.
|
|
483
|
+
|
|
484
|
+
Renders nothing at all for a pair with no facts. An empty facts block is worse
|
|
485
|
+
than none — it is the thin content this tier exists to remove.
|
|
486
|
+
|
|
487
|
+
## 1.13.0
|
|
488
|
+
|
|
489
|
+
The operator's rating on each result card. The operator's rather than the
|
|
490
|
+
route's: a single route rarely has enough published reviews to clear the
|
|
491
|
+
display threshold, and a card with a rating beats one without.
|
|
492
|
+
|
|
493
|
+
## [1.12.0] - 2026-08-01
|
|
494
|
+
|
|
495
|
+
### Removed
|
|
496
|
+
|
|
497
|
+
- **The Booking.com hotel banner.** It was unmounted from `Found` earlier on
|
|
498
|
+
2026-08-01 for sitting between the date strip and the trips - advertising a
|
|
499
|
+
hotel to somebody still deciding whether they can travel at all - and the
|
|
500
|
+
component is now deleted rather than kept dormant. It was never an
|
|
501
|
+
integration, only a hand-built `searchresults.xu.html` link built from
|
|
502
|
+
`city.booking_id`; leaving the component, the column and its admin field in
|
|
503
|
+
place made it look like one. The `routes_order.step2.booking` strings and
|
|
504
|
+
the `s-ec.bstatic.com` CSP entry go with it.
|
|
505
|
+
|
|
506
|
+
## [1.11.1] - 2026-08-01
|
|
507
|
+
|
|
508
|
+
### Changed
|
|
509
|
+
|
|
510
|
+
- **Coupon and payment method moved into the sidebar**, coupon directly
|
|
511
|
+
above the totals it changes and the method picker directly above the
|
|
512
|
+
button that acts on it. Both sat several sections away in the main
|
|
513
|
+
column, so applying a coupon changed a number that was off screen.
|
|
514
|
+
- **The results heading names the trip** - "Online Bus tickets from Tirana
|
|
515
|
+
Albania to Thessaloniki Greece" instead of "Select a Departure Schedule".
|
|
516
|
+
Countries are dropped when both ends share one, since "from Tirana
|
|
517
|
+
Albania to Korca Albania" is noise. Read from the city list rather than
|
|
518
|
+
from the results, so the heading is right even when the search finds
|
|
519
|
+
nothing - which is exactly when knowing what was searched for matters.
|
|
520
|
+
- **Order summary**: the operator's logo under the route name, departure
|
|
521
|
+
and arrival icons sitting on the connecting line, and the departure date
|
|
522
|
+
and time in bold - the two things a buyer checks before paying.
|
|
523
|
+
|
|
524
|
+
### Fixed
|
|
525
|
+
|
|
526
|
+
- The coupon and payment blocks switched layout at a VIEWPORT breakpoint,
|
|
527
|
+
so in the ~290px sidebar they used the wide arrangement and the coupon
|
|
528
|
+
input came out 86px wide. They now wrap on their own width.
|
|
529
|
+
|
|
530
|
+
## [1.11.0] - 2026-08-01
|
|
531
|
+
|
|
532
|
+
### Changed
|
|
533
|
+
|
|
534
|
+
- **Passenger details and checkout are one screen.** Split across two
|
|
535
|
+
steps, the buyer typed passenger details with no sight of the price, then
|
|
536
|
+
saw the price on a screen where they could no longer check what they had
|
|
537
|
+
typed - and ticking an add-on changed a total that had already scrolled
|
|
538
|
+
away. The wizard is four steps now, not five.
|
|
539
|
+
- **A sticky sidebar** carries the trip, the ticket price, each add-on, any
|
|
540
|
+
discount, the total due, and the button that commits to it. The button
|
|
541
|
+
follows the chosen method, so it reads "Reserve" for a reservation rather
|
|
542
|
+
than claiming to take a payment.
|
|
543
|
+
- **The trip summary states both ends of each leg**, city and country in
|
|
544
|
+
full, origin above destination. It previously showed one end only - the
|
|
545
|
+
departure point outbound, the arrival point on the return - so a one-way
|
|
546
|
+
buyer reached payment having never been shown where the bus drops them.
|
|
547
|
+
- **Required fields are marked.** The form validated them but never said
|
|
548
|
+
which they were, so the first a buyer heard of it was an error message
|
|
549
|
+
after pressing pay.
|
|
550
|
+
|
|
551
|
+
`usePostOrder` now takes the passenger details in its mutation variables
|
|
552
|
+
rather than as a hook argument: on one screen they are only known at the
|
|
553
|
+
moment pay is pressed, so a closure captured at render time would have sent
|
|
554
|
+
stale state. The two forms stay separate react-hook-form instances - they
|
|
555
|
+
validate different things, and billing is not rendered at all for a
|
|
556
|
+
signed-in buyer - and paying validates them in order, stopping at the first
|
|
557
|
+
failure so the buyer lands on the thing that needs fixing.
|
|
558
|
+
|
|
559
|
+
Verified by completing a real reservation end to end: order 61 / ticket 75
|
|
560
|
+
carried every entered value, seat included.
|
|
561
|
+
|
|
562
|
+
## [1.10.0] - 2026-08-01
|
|
563
|
+
|
|
564
|
+
### Added
|
|
565
|
+
|
|
566
|
+
- **A search that finds nothing now answers instead of apologising.** In
|
|
567
|
+
place of the bare "we couldn't find any tickets", the page offers nearby
|
|
568
|
+
departure cities, nearby arrival cities and other dates that have seats.
|
|
569
|
+
Every suggestion is a search obtapi has already confirmed returns
|
|
570
|
+
results on the requested date, quoted with its fare, so a click is never
|
|
571
|
+
a second dead end. Nearby cities are ordered by real distance from the
|
|
572
|
+
city that was asked for - the passenger's problem is getting to the bus,
|
|
573
|
+
not saving two euros.
|
|
574
|
+
- Fetched only once the departure search has come back empty, so a search
|
|
575
|
+
that succeeded never pays for it.
|
|
576
|
+
|
|
577
|
+
Suggestion links do a full page load on purpose: RoutesOrder holds the
|
|
578
|
+
search in component state and Step1 only starts a search when it has not
|
|
579
|
+
already redirected, so a client-side navigation would change the URL and
|
|
580
|
+
leave the empty result set on screen.
|
|
581
|
+
|
|
582
|
+
## [1.9.6] - 2026-08-01
|
|
583
|
+
|
|
584
|
+
### Added
|
|
585
|
+
|
|
586
|
+
- **The search form stays on screen while choosing a route.** Adjusting a
|
|
587
|
+
date, a city or the passenger count used to mean "Back to Search", which
|
|
588
|
+
threw the results away along with any filters. Shown only on the
|
|
589
|
+
route-selection screens: past that point the passenger has committed to a
|
|
590
|
+
specific trip, and re-running the search under them would discard details
|
|
591
|
+
they had already typed.
|
|
592
|
+
|
|
593
|
+
## [1.9.5] - 2026-08-01
|
|
594
|
+
|
|
595
|
+
### Fixed
|
|
596
|
+
|
|
597
|
+
- **The column header row now lines up with the result cards** - 0px drift
|
|
598
|
+
on all five columns, and content-independent. It had 4px horizontal
|
|
599
|
+
padding against the card's 15px and no column gap against the card's
|
|
600
|
+
15px, which pushed every label right of the data it named (Duration was
|
|
601
|
+
out by 34px).
|
|
602
|
+
- **The card's two location columns are now equal width.** `flex: 1` with
|
|
603
|
+
the default `min-width: auto` meant they sized to their content, so the
|
|
604
|
+
arrival column (long city plus long stop name) took 215px while the
|
|
605
|
+
departure column got 155px. Beyond breaking any header alignment, this
|
|
606
|
+
let one long stop name steal width from the other side of the card.
|
|
607
|
+
|
|
608
|
+
## [1.9.4] - 2026-08-01
|
|
609
|
+
|
|
610
|
+
### Changed
|
|
611
|
+
|
|
612
|
+
- **Fare decimals ride as superscript** and the currency is set smaller.
|
|
613
|
+
On a fare board the cents are almost never what anyone is comparing, and
|
|
614
|
+
at full size they took a third of the width of the widest thing on the
|
|
615
|
+
card. Split from obtapi's already-formatted string rather than
|
|
616
|
+
re-formatted from `price.value`, so the label's own currency and number
|
|
617
|
+
formatting stay the single source of truth - an unexpected format falls
|
|
618
|
+
back to rendering the string whole.
|
|
619
|
+
- **The taxes/passenger line is smaller and tucked against the price**,
|
|
620
|
+
which reads as one unit and frees the room the Select button needed.
|
|
621
|
+
- **Amenities moved into the details row** as its first column, so route,
|
|
622
|
+
transit and policies sit beside them - four columns instead of a separate
|
|
623
|
+
full-width band under every card.
|
|
624
|
+
- **Amenities are icon-only on this page**, with the name as a hover
|
|
625
|
+
tooltip (`compact` on `RouteFeature`). Eight amenities spelled out took
|
|
626
|
+
more width than everything else in that row put together.
|
|
627
|
+
|
|
628
|
+
## [1.9.3] - 2026-08-01
|
|
629
|
+
|
|
630
|
+
### Changed
|
|
631
|
+
|
|
632
|
+
- **Trip length reads as a duration, not a clock time.** "08:30" sat
|
|
633
|
+
directly between two real departure times with nothing to say it meant
|
|
634
|
+
eight and a half hours rather than half past eight. It now renders as
|
|
635
|
+
`07h 30m` behind an hourglass, and an exact number of hours drops the
|
|
636
|
+
minutes (`08h`, not `08h 00m`). The unit labels come from the translation
|
|
637
|
+
files - "h"/"m" are a Latin-script convention, so Greek reads `07ω 30λ`,
|
|
638
|
+
Bulgarian and Macedonian `07ч 30м`, Albanian `07o 30m`.
|
|
639
|
+
obtapi's `-` sentinel (no computable duration) still passes straight
|
|
640
|
+
through rather than becoming a confident "0h 0m".
|
|
641
|
+
- **Stop name down to 12px** on the result cards.
|
|
642
|
+
|
|
643
|
+
## [1.9.2] - 2026-08-01
|
|
644
|
+
|
|
645
|
+
### Changed
|
|
646
|
+
|
|
647
|
+
- **Stop name stepped down to 14px** on the result cards. At the same 16px
|
|
648
|
+
as body copy it competed with the city name above it; the city should be
|
|
649
|
+
read first and the stop only when it is needed.
|
|
650
|
+
- **Country code renders upper case and smaller.** It arrives from the
|
|
651
|
+
database lower case ("alb", "grc") - an ISO country code is upper case by
|
|
652
|
+
definition, so it is cased at render time rather than left to the data,
|
|
653
|
+
where every consumer would have to remember.
|
|
654
|
+
|
|
655
|
+
## [1.9.1] - 2026-08-01
|
|
656
|
+
|
|
657
|
+
### Changed
|
|
658
|
+
|
|
659
|
+
- **Filters became a sidebar** at 1280px and up - always visible, sticky
|
|
660
|
+
while the list scrolls, no click needed to discover them. Below 1280px
|
|
661
|
+
they stay the collapsible panel above the list. The breakpoint is 1280
|
|
662
|
+
rather than 1024 because the result cards switch to their wide row layout
|
|
663
|
+
at 1024px on VIEWPORT width, not container width, so taking a column out
|
|
664
|
+
of a 1024px window would leave the cards using a layout their remaining
|
|
665
|
+
space cannot carry.
|
|
666
|
+
- **Facet counts are a filled pill**, separate from the from-price. They
|
|
667
|
+
used to read as one muted string ("5 · 38.00 EUR") where the two numbers
|
|
668
|
+
ran together and it was not obvious which was which.
|
|
669
|
+
- **Facet labels wrap instead of truncating.** In a sidebar column an
|
|
670
|
+
ellipsis reduced two different stops at the same city to the same
|
|
671
|
+
"Terminali Lindor Inte..." - the one distinction the filter exists to
|
|
672
|
+
make.
|
|
673
|
+
- **The result-count / sort bar sits on the card surface** rather than
|
|
674
|
+
floating on the page background.
|
|
675
|
+
|
|
676
|
+
### Fixed
|
|
677
|
+
|
|
678
|
+
- Filter panel grid tracks used `1fr`, whose implicit min-width is `auto`,
|
|
679
|
+
so a long stop name pushed the whole panel wider than its column and
|
|
680
|
+
pushed the counts and prices out of view entirely. Now `minmax(0, 1fr)`.
|
|
681
|
+
|
|
9
682
|
## [1.9.0] - 2026-08-01
|
|
10
683
|
|
|
11
684
|
### Added
|