@autobusal/common 1.33.3 → 1.33.4
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 +6 -0
- package/LocalizedFields/LocalizedFields.tsx +103 -0
- package/LocalizedFields/styles.ts +52 -0
- package/index.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.33.4
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **New `LocalizedFields` component** - a generic per-language field editor generalising SeoOverride (fixed to title/description) and Faqs/Translations (fixed to question/answer) into one taking an arbitrary field list. A section with just one translatable field (a category name, a menu label) no longer needs its own bespoke ~120-line copy of the same editor to support more than English+Albanian.
|
|
8
|
+
|
|
3
9
|
## 1.33.3
|
|
4
10
|
|
|
5
11
|
### Added
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { useGetSettings } from '@autobusal/providers/services';
|
|
4
|
+
import { Container, ContainerLocales, LocaleButton, Textarea } from './styles';
|
|
5
|
+
|
|
6
|
+
export type LocalizedValue = Record<string, Record<string, string>>;
|
|
7
|
+
|
|
8
|
+
export interface LocalizedFieldSpec {
|
|
9
|
+
name: string
|
|
10
|
+
label: string
|
|
11
|
+
type?: 'text' | 'textarea'
|
|
12
|
+
maxLength?: number
|
|
13
|
+
rows?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
fields: LocalizedFieldSpec[]
|
|
18
|
+
/** Keyed by locale, then by field name - `{ en: { name: 'Foo' } }` */
|
|
19
|
+
value: LocalizedValue
|
|
20
|
+
t: TFunction<'normal'>
|
|
21
|
+
onChange: (next: LocalizedValue) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A generic per-language field editor.
|
|
26
|
+
*
|
|
27
|
+
* Ferjolt Ozuni - Date: 2026-08-11
|
|
28
|
+
*
|
|
29
|
+
* The same shape as SeoOverride and Faqs/Translations - one language shown
|
|
30
|
+
* at a time, languages that already carry copy are marked, the parent owns
|
|
31
|
+
* the state and the submit. This is the generalised version of both:
|
|
32
|
+
* SeoOverride is fixed to title/description, Faqs/Translations to question/
|
|
33
|
+
* answer - this one takes an arbitrary field list, so a section with just
|
|
34
|
+
* ONE translatable field (a category name, a menu label) doesn't need its
|
|
35
|
+
* own bespoke copy of the same ~120 lines to get the same editor.
|
|
36
|
+
*
|
|
37
|
+
* English is the tab an empty editor starts on - it's the required floor
|
|
38
|
+
* every one of these fields falls back to publicly.
|
|
39
|
+
*/
|
|
40
|
+
const LocalizedFields = ({ fields, value, t, onChange }: Props): JSX.Element => {
|
|
41
|
+
const { data } = useGetSettings();
|
|
42
|
+
|
|
43
|
+
const available = data.preferences.languages.available;
|
|
44
|
+
|
|
45
|
+
const [ locale, setLocale ] = useState<string>(data.preferences.languages.default);
|
|
46
|
+
|
|
47
|
+
const current = value[locale] ?? {};
|
|
48
|
+
|
|
49
|
+
const update = (field: string, next: string): void => {
|
|
50
|
+
onChange({
|
|
51
|
+
...value,
|
|
52
|
+
[locale]: { ...current, [field]: next }
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const written = (language: string): boolean => {
|
|
57
|
+
const entry = value[language] ?? {};
|
|
58
|
+
|
|
59
|
+
return fields.some(field => entry[field.name]?.trim());
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Container className="box">
|
|
64
|
+
<ContainerLocales>
|
|
65
|
+
{ available.map(language => (
|
|
66
|
+
<LocaleButton
|
|
67
|
+
key={ language }
|
|
68
|
+
type="button"
|
|
69
|
+
$selected={ language === locale }
|
|
70
|
+
$written={ written(language) }
|
|
71
|
+
onClick={ () => setLocale(language) }
|
|
72
|
+
>
|
|
73
|
+
{ t(`languages.${ language }`, { ns: 'common' }) }
|
|
74
|
+
</LocaleButton>
|
|
75
|
+
)) }
|
|
76
|
+
</ContainerLocales>
|
|
77
|
+
|
|
78
|
+
{ fields.map((field, index) => (
|
|
79
|
+
<div key={ field.name } className={ index === fields.length - 1 ? 'row row-nomargin' : 'row' }>
|
|
80
|
+
{ field.label }
|
|
81
|
+
|
|
82
|
+
{ field.type === 'textarea' ? (
|
|
83
|
+
<Textarea
|
|
84
|
+
maxLength={ field.maxLength }
|
|
85
|
+
$rows={ field.rows }
|
|
86
|
+
value={ current[field.name] ?? '' }
|
|
87
|
+
onChange={ (event) => update(field.name, event.target.value) }
|
|
88
|
+
/>
|
|
89
|
+
) : (
|
|
90
|
+
<input
|
|
91
|
+
type="text"
|
|
92
|
+
maxLength={ field.maxLength }
|
|
93
|
+
value={ current[field.name] ?? '' }
|
|
94
|
+
onChange={ (event) => update(field.name, event.target.value) }
|
|
95
|
+
/>
|
|
96
|
+
) }
|
|
97
|
+
</div>
|
|
98
|
+
)) }
|
|
99
|
+
</Container>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default LocalizedFields;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Same card treatment as SeoOverride's own Container - this editor sits
|
|
5
|
+
* directly above a Viewer form, so it needs to read as part of the form
|
|
6
|
+
* rather than loose furniture above it.
|
|
7
|
+
*/
|
|
8
|
+
export const Container = styled.div`
|
|
9
|
+
margin-bottom: 20px;
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export const ContainerLocales = styled.div`
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
margin-bottom: 12px;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One button per language, `$written` marking which ones actually have
|
|
21
|
+
* copy - the same treatment as SeoOverride/Faqs' Translations, so every
|
|
22
|
+
* per-language editor in the admin reads as the same control.
|
|
23
|
+
*/
|
|
24
|
+
export const LocaleButton = styled.button<{ $selected: boolean, $written: boolean }>`
|
|
25
|
+
padding: 4px 12px;
|
|
26
|
+
border: none;
|
|
27
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
28
|
+
background: ${ props => (props.$selected ? props.theme.background.neutral : 'transparent') };
|
|
29
|
+
color: ${ props => (props.$written ? props.theme.font.normal : props.theme.font.faded) };
|
|
30
|
+
font-size: ${ props => props.theme.size.s };
|
|
31
|
+
font-weight: ${ props => (props.$selected ? 700 : 400) };
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
|
|
35
|
+
&:hover {
|
|
36
|
+
background: ${ props => props.theme.background.neutral };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
${ props => props.$written && css`
|
|
40
|
+
&::after {
|
|
41
|
+
content: '•';
|
|
42
|
+
margin-left: 6px;
|
|
43
|
+
color: ${ props => props.theme.primary.normal };
|
|
44
|
+
}
|
|
45
|
+
` }
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export const Textarea = styled.textarea<{ $rows?: number }>`
|
|
49
|
+
height: ${ props => (props.$rows ? props.$rows * 30 : 90) }px;
|
|
50
|
+
padding: 8px 10px;
|
|
51
|
+
resize: vertical;
|
|
52
|
+
`;
|
package/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import Gender from './Gender';
|
|
|
19
19
|
import { General, Inline, Box, BoxMiddle, GeneralFull } from './Loading/Loading';
|
|
20
20
|
import IpLogs from './IpLogs/IpLogs';
|
|
21
21
|
import JsonLd from './JsonLd';
|
|
22
|
+
import LocalizedFields from './LocalizedFields/LocalizedFields';
|
|
22
23
|
import Meta from './Meta';
|
|
23
24
|
import Modal from './Modal/Modal';
|
|
24
25
|
import NotificationIcon from './Notifications/Icon';
|
|
@@ -71,6 +72,7 @@ export {
|
|
|
71
72
|
Inline,
|
|
72
73
|
IpLogs,
|
|
73
74
|
JsonLd,
|
|
75
|
+
LocalizedFields,
|
|
74
76
|
Meta,
|
|
75
77
|
Modal,
|
|
76
78
|
NotificationIcon,
|