@autobusal/common 1.17.0 → 1.19.0
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 +21 -0
- package/SeoOverride/SeoOverride.tsx +115 -0
- package/SeoOverride/styles.ts +88 -0
- package/Table/Actions/Get.tsx +8 -1
- package/index.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ All notable changes to `@autobusal/common` are documented here. This project fol
|
|
|
6
6
|
> Note: 1.10.0 through 1.15.1 were published without changelog entries. The
|
|
7
7
|
> gap is left as-is rather than reconstructed after the fact.
|
|
8
8
|
|
|
9
|
+
## 1.19.0
|
|
10
|
+
|
|
11
|
+
`extra` joins `create` and `search` as a top-of-table slot rather than a
|
|
12
|
+
per-row action. Any table opting into the extra slot also rendered one empty,
|
|
13
|
+
iconless, still-clickable button in every row's options cell — Icon has no
|
|
14
|
+
case for `extra`, so it drew nothing while taking the click. The refund queue
|
|
15
|
+
has been doing this all along; the new review queue would have too.
|
|
16
|
+
|
|
17
|
+
## [1.18.0] - 2026-08-02
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`SeoOverride`** - a per-language editor for the SEO title/description
|
|
22
|
+
overrides, shared by the label, country and city forms. One language is
|
|
23
|
+
shown at a time (a brand writes copy for one or two, and thirteen sets of
|
|
24
|
+
empty inputs invite nobody to fill any of them in) and the languages that
|
|
25
|
+
already have copy are marked, so it is obvious which are done. Controlled
|
|
26
|
+
and deliberately form-less: those pages submit every field in one request,
|
|
27
|
+
so an editor with its own save would either post a partial update or leave
|
|
28
|
+
two save buttons meaning different things on one screen.
|
|
29
|
+
|
|
9
30
|
## [1.17.0] - 2026-08-02
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { ContainerLocales, LocaleButton, Notice, Field, Label, Input, Textarea } from './styles';
|
|
4
|
+
import { useGetSettings } from '@autobusal/providers/services';
|
|
5
|
+
|
|
6
|
+
export interface SeoEntry {
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SeoOverrides = Record<string, SeoEntry>;
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/**
|
|
15
|
+
* The stored map, keyed by locale. A language with no entry has no
|
|
16
|
+
* override, which is what returns that language's page to its own
|
|
17
|
+
* translated template.
|
|
18
|
+
*/
|
|
19
|
+
value?: SeoOverrides
|
|
20
|
+
t: TFunction<'common'>
|
|
21
|
+
onChange: (next: SeoOverrides) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-language SEO override editor.
|
|
26
|
+
*
|
|
27
|
+
* Ferjolt Ozuni - Date: 2026-08-02
|
|
28
|
+
*
|
|
29
|
+
* These overrides used to be one title and one description served to every
|
|
30
|
+
* language. That was merely a limitation while the whole site lived at one
|
|
31
|
+
* URL per page; once /{locale}/city/tirana existed and was announced to
|
|
32
|
+
* search engines as a distinct language version it became a contradiction -
|
|
33
|
+
* and because an override REPLACES the localised template the public pages
|
|
34
|
+
* build from live route and price data, filling one in actively made
|
|
35
|
+
* multilingual SEO worse than leaving it blank.
|
|
36
|
+
*
|
|
37
|
+
* CONTROLLED, and deliberately not a form of its own. The country and city
|
|
38
|
+
* pages submit every field in one request, so an editor with its own save
|
|
39
|
+
* button would either post a partial update and wipe the rest of the record,
|
|
40
|
+
* or leave two save buttons on one screen with different meanings. The
|
|
41
|
+
* parent owns the state and the submit; this owns the editing.
|
|
42
|
+
*
|
|
43
|
+
* One language is shown at a time rather than thirteen sets of fields down
|
|
44
|
+
* the page: a brand writes copy for one or two, and a wall of empty inputs
|
|
45
|
+
* invites nobody to fill in any of them. Languages that already have copy
|
|
46
|
+
* are marked, so it is obvious which ones are done without clicking through
|
|
47
|
+
* all of them.
|
|
48
|
+
*/
|
|
49
|
+
const SeoOverride = ({ value, t, onChange }: Props): JSX.Element => {
|
|
50
|
+
const { data } = useGetSettings();
|
|
51
|
+
|
|
52
|
+
const available = data.preferences.languages.available;
|
|
53
|
+
|
|
54
|
+
const [ locale, setLocale ] = useState<string>(data.preferences.languages.default);
|
|
55
|
+
|
|
56
|
+
const overrides = value ?? {};
|
|
57
|
+
|
|
58
|
+
const current = overrides[locale] ?? {};
|
|
59
|
+
|
|
60
|
+
const update = (field: keyof SeoEntry, next: string): void => {
|
|
61
|
+
onChange({
|
|
62
|
+
...overrides,
|
|
63
|
+
[locale]: { ...current, [field]: next }
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const written = (language: string): boolean => {
|
|
68
|
+
const entry = overrides[language];
|
|
69
|
+
|
|
70
|
+
return !!(entry?.title?.trim() || entry?.description?.trim());
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<ContainerLocales>
|
|
76
|
+
{ available.map(language => (
|
|
77
|
+
<LocaleButton
|
|
78
|
+
key={ language }
|
|
79
|
+
type="button"
|
|
80
|
+
$selected={ language === locale }
|
|
81
|
+
$written={ written(language) }
|
|
82
|
+
onClick={ () => setLocale(language) }
|
|
83
|
+
>
|
|
84
|
+
{ t(`languages.${ language }`, { ns: 'common' }) }
|
|
85
|
+
</LocaleButton>
|
|
86
|
+
)) }
|
|
87
|
+
</ContainerLocales>
|
|
88
|
+
|
|
89
|
+
<Notice>{ t('seo_override.notice', { ns: 'common' }) }</Notice>
|
|
90
|
+
|
|
91
|
+
<Field>
|
|
92
|
+
<Label>{ t('seo_override.title', { ns: 'common' }) }</Label>
|
|
93
|
+
|
|
94
|
+
<Input
|
|
95
|
+
type="text"
|
|
96
|
+
maxLength={ 250 }
|
|
97
|
+
value={ current.title ?? '' }
|
|
98
|
+
onChange={ (event) => update('title', event.target.value) }
|
|
99
|
+
/>
|
|
100
|
+
</Field>
|
|
101
|
+
|
|
102
|
+
<Field>
|
|
103
|
+
<Label>{ t('seo_override.description', { ns: 'common' }) }</Label>
|
|
104
|
+
|
|
105
|
+
<Textarea
|
|
106
|
+
maxLength={ 500 }
|
|
107
|
+
value={ current.description ?? '' }
|
|
108
|
+
onChange={ (event) => update('description', event.target.value) }
|
|
109
|
+
/>
|
|
110
|
+
</Field>
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default SeoOverride;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const ContainerLocales = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
gap: 8px;
|
|
7
|
+
margin-bottom: 12px;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One button per language.
|
|
12
|
+
*
|
|
13
|
+
* Ferjolt Ozuni - Date: 2026-08-02
|
|
14
|
+
* `$written` marks the languages that actually have copy, so it is obvious
|
|
15
|
+
* at a glance which ones are done - without it, thirteen identical buttons
|
|
16
|
+
* say nothing about where the work stands, and the only way to find out is
|
|
17
|
+
* to click every one of them.
|
|
18
|
+
*/
|
|
19
|
+
export const LocaleButton = styled.button<{ $selected: boolean, $written: boolean }>`
|
|
20
|
+
padding: 4px 12px;
|
|
21
|
+
border: none;
|
|
22
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
23
|
+
background: ${ props => (props.$selected ? props.theme.background.neutral : 'transparent') };
|
|
24
|
+
color: ${ props => (props.$written ? props.theme.font.normal : props.theme.font.faded) };
|
|
25
|
+
font-size: ${ props => props.theme.size.s };
|
|
26
|
+
font-weight: ${ props => (props.$selected ? 700 : 400) };
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
|
|
30
|
+
&:hover {
|
|
31
|
+
background: ${ props => props.theme.background.neutral };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
${ props => props.$written && css`
|
|
35
|
+
&::after {
|
|
36
|
+
content: '•';
|
|
37
|
+
margin-left: 6px;
|
|
38
|
+
color: ${ props => props.theme.primary.normal };
|
|
39
|
+
}
|
|
40
|
+
` }
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export const Notice = styled.p`
|
|
44
|
+
margin-bottom: 15px;
|
|
45
|
+
color: ${ props => props.theme.font.faded };
|
|
46
|
+
font-size: ${ props => props.theme.size.s };
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The editor's own fields, rather than Viewer rows.
|
|
51
|
+
*
|
|
52
|
+
* Ferjolt Ozuni - Date: 2026-08-02
|
|
53
|
+
* Viewer's inputs are uncontrolled (defaultValue + a react-hook-form ref),
|
|
54
|
+
* which cannot hold thirteen languages' worth of state while showing one -
|
|
55
|
+
* switching language would discard whatever had just been typed. These are
|
|
56
|
+
* controlled, and styled to match the rows they sit among.
|
|
57
|
+
*/
|
|
58
|
+
export const Field = styled.div`
|
|
59
|
+
margin-bottom: 15px;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export const Label = styled.label`
|
|
63
|
+
display: block;
|
|
64
|
+
margin-bottom: 5px;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
font-size: ${ props => props.theme.size.m };
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const field = css`
|
|
70
|
+
width: 100%;
|
|
71
|
+
padding: 10px;
|
|
72
|
+
border: 1px solid ${ props => props.theme.primary.neutral };
|
|
73
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
74
|
+
background: transparent;
|
|
75
|
+
color: ${ props => props.theme.font.normal };
|
|
76
|
+
font-family: inherit;
|
|
77
|
+
font-size: ${ props => props.theme.size.m };
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
export const Input = styled.input`
|
|
81
|
+
${ field }
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export const Textarea = styled.textarea`
|
|
85
|
+
${ field }
|
|
86
|
+
min-height: 90px;
|
|
87
|
+
resize: vertical;
|
|
88
|
+
`;
|
package/Table/Actions/Get.tsx
CHANGED
|
@@ -14,7 +14,14 @@ interface Props {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const Get = ({ id, url, urlWith, type, t, handlers, navigate }: Props): (JSX.Element | null) => {
|
|
17
|
-
|
|
17
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-02
|
|
18
|
+
// 'extra' belongs with 'create' and 'search': all three are TOP-of-table
|
|
19
|
+
// slots, not per-row actions. Without it here, any table that opts into
|
|
20
|
+
// the extra slot (the refund queue does, the review queue now does too)
|
|
21
|
+
// also renders one empty, iconless, clickable button in every row's
|
|
22
|
+
// options cell - Icon has no case for 'extra', so it draws nothing at all
|
|
23
|
+
// while still taking the click.
|
|
24
|
+
if (['create', 'search', 'extra'].includes(type)) {
|
|
18
25
|
return null;
|
|
19
26
|
}
|
|
20
27
|
|
package/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import RouteFeature from './RouteFeature/RouteFeature';
|
|
|
31
31
|
import Required from './Required/Required';
|
|
32
32
|
import Road from './Road/Road';
|
|
33
33
|
import LocaleGate from './Locale/LocaleGate';
|
|
34
|
+
import SeoOverride from './SeoOverride/SeoOverride';
|
|
34
35
|
import localiseRoutes from './Locale/routes';
|
|
35
36
|
import { splitLocale, withLocale, localeFromPath } from './Locale/locale';
|
|
36
37
|
import RouteItem from './RouteItem/RouteItem';
|
|
@@ -78,6 +79,7 @@ export {
|
|
|
78
79
|
Required,
|
|
79
80
|
Road,
|
|
80
81
|
LocaleGate,
|
|
82
|
+
SeoOverride,
|
|
81
83
|
localiseRoutes,
|
|
82
84
|
splitLocale,
|
|
83
85
|
withLocale,
|