mautourco-components 0.2.142 → 0.2.144
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/dist/components/molecules/LanguageSelector/LanguageSelector.css +120 -0
- package/dist/components/molecules/LanguageSelector/LanguageSelector.d.ts +11 -0
- package/dist/components/molecules/LanguageSelector/LanguageSelector.js +28 -0
- package/dist/components/molecules/TextWithIcon/TextWithIcon.js +1 -1
- package/dist/components/organisms/Docket/Docket.d.ts +31 -6
- package/dist/components/organisms/Docket/Docket.js +33 -13
- package/dist/components/organisms/Docket/DocketEmptyState.d.ts +14 -3
- package/dist/components/organisms/Docket/DocketEmptyState.js +7 -2
- package/dist/components/organisms/Docket/DocketFooter.d.ts +24 -0
- package/dist/components/organisms/Docket/DocketFooter.js +11 -3
- package/dist/components/organisms/DocketAccordion/DocketAccordion.d.ts +10 -1
- package/dist/components/organisms/DocketAccordion/DocketAccordion.js +2 -2
- package/dist/hooks/useMobile.d.ts +2 -1
- package/dist/hooks/useMobile.js +1 -0
- package/dist/styles/components/organism/docket.css +14 -20
- package/package.json +1 -1
- package/src/components/molecules/LanguageSelector/LanguageSelector.css +83 -0
- package/src/components/molecules/LanguageSelector/LanguageSelector.tsx +102 -0
- package/src/components/molecules/TextWithIcon/TextWithIcon.tsx +1 -1
- package/src/components/organisms/Docket/Docket.tsx +106 -24
- package/src/components/organisms/Docket/DocketEmptyState.tsx +29 -7
- package/src/components/organisms/Docket/DocketFooter.tsx +82 -26
- package/src/components/organisms/DocketAccordion/DocketAccordion.tsx +12 -2
- package/src/styles/components/organism/docket.css +13 -20
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Language Selector - Trigger */
|
|
2
|
+
.language-selector__trigger {
|
|
3
|
+
@apply flex items-center justify-center gap-2 cursor-pointer;
|
|
4
|
+
@apply px-4;
|
|
5
|
+
height: 44px;
|
|
6
|
+
border: solid 1px var(--color-tuna-500);
|
|
7
|
+
border-radius: 12px;
|
|
8
|
+
transition: all 0.5s ease;
|
|
9
|
+
&:hover {
|
|
10
|
+
border-color: #262626;
|
|
11
|
+
}
|
|
12
|
+
&.language-selector__trigger--with-text {
|
|
13
|
+
border-color: #262626;
|
|
14
|
+
color: #262626;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.language-selector__placeholder {
|
|
19
|
+
color: var(--color-tuna-500);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.language-selector__trigger-icon {
|
|
23
|
+
@apply flex-shrink-0 text-[var(--color-neutral-800)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.language-selector__trigger-text {
|
|
27
|
+
@apply text-sm font-medium text-[var(--color-neutral-800)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.language-selector__trigger-chevron {
|
|
31
|
+
@apply flex-shrink-0 text-[var(--color-neutral-800)] transition-transform duration-200;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.language-selector__trigger[data-state='open'] .language-selector__trigger-chevron {
|
|
35
|
+
transform: rotate(180deg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Popover */
|
|
39
|
+
.language-selector__popover {
|
|
40
|
+
@apply bg-white;
|
|
41
|
+
padding: var(--spacing-padding-px-2);
|
|
42
|
+
border-radius: var(--border-radius-rounded-xl);
|
|
43
|
+
min-width: 8rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Popover Content */
|
|
47
|
+
.language-selector__content {
|
|
48
|
+
@apply flex flex-col py-1 bg-white;
|
|
49
|
+
min-width: 8rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.language-selector__option {
|
|
53
|
+
@apply flex items-center w-full cursor-pointer gap-x-2;
|
|
54
|
+
height: 2.25rem;
|
|
55
|
+
padding-inline: var(--spacing-padding-px-3);
|
|
56
|
+
border-radius: var(--border-radius-rounded-md);
|
|
57
|
+
font-size: var(--typography-font-size-sm);
|
|
58
|
+
font-weight: var(--typography-font-weight-medium);
|
|
59
|
+
color: var(--color-neutral-800);
|
|
60
|
+
transition: background-color 0.2s ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.language-selector__option:hover {
|
|
64
|
+
background-color: var(--color-neutral-100);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.language-selector__option--selected {
|
|
68
|
+
background-color: var(--color-neutral-100);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.language-selector__selected-language {
|
|
72
|
+
@apply flex gap-x-2 items-center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.language-selector__flag {
|
|
76
|
+
@apply flex items-center justify-center;
|
|
77
|
+
@apply grow-0 shrink-0 basis-4 h-4;
|
|
78
|
+
@apply rounded-full overflow-hidden;
|
|
79
|
+
.fi {
|
|
80
|
+
@apply w-4;
|
|
81
|
+
flex: 0 0 1.333333em;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { cn } from '@/src/lib/utils';
|
|
2
|
+
import 'flag-icons/css/flag-icons.min.css';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Icon from '../../atoms/Icon/Icon';
|
|
5
|
+
import { Text } from '../../atoms/Typography/Typography';
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
|
|
7
|
+
import { languagesMap } from '../ServiceLanguages/constant';
|
|
8
|
+
import './LanguageSelector.css';
|
|
9
|
+
|
|
10
|
+
export const defaultLanguages = ['English', 'Français', 'German', 'Italian'];
|
|
11
|
+
|
|
12
|
+
export interface LanguageSelectorProps {
|
|
13
|
+
languages?: string[];
|
|
14
|
+
defaultLanguage?: string;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
onSelectLanguage?: (language: string) => void;
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function LanguageSelector(props: LanguageSelectorProps) {
|
|
21
|
+
const {
|
|
22
|
+
languages = defaultLanguages,
|
|
23
|
+
defaultLanguage,
|
|
24
|
+
placeholder = 'Prefered language',
|
|
25
|
+
label,
|
|
26
|
+
onSelectLanguage,
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
|
|
31
|
+
defaultLanguage
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const handleSelectLanguage = (language: string) => {
|
|
35
|
+
setSelectedLanguage(language);
|
|
36
|
+
onSelectLanguage?.(language);
|
|
37
|
+
setOpen(false);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setSelectedLanguage(defaultLanguage);
|
|
42
|
+
}, [defaultLanguage]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
{label && (
|
|
47
|
+
<label className="mb-2">
|
|
48
|
+
<Text size="sm" as="span">
|
|
49
|
+
{label}
|
|
50
|
+
</Text>
|
|
51
|
+
</label>
|
|
52
|
+
)}
|
|
53
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
54
|
+
<PopoverTrigger asChild>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
className={cn('language-selector__trigger', {
|
|
58
|
+
'language-selector__trigger--with-text': !!selectedLanguage,
|
|
59
|
+
})}>
|
|
60
|
+
{selectedLanguage ? (
|
|
61
|
+
<span className="language-selector__selected-language">
|
|
62
|
+
<span className="language-selector__flag">
|
|
63
|
+
<i
|
|
64
|
+
className={`fi fi-${languagesMap[selectedLanguage.toLowerCase()]}`}></i>
|
|
65
|
+
</span>
|
|
66
|
+
{selectedLanguage}
|
|
67
|
+
</span>
|
|
68
|
+
) : (
|
|
69
|
+
<span className="language-selector__placeholder">{placeholder}</span>
|
|
70
|
+
)}
|
|
71
|
+
<Icon
|
|
72
|
+
name="chevron-down"
|
|
73
|
+
size="sm"
|
|
74
|
+
className="language-selector__trigger-chevron"
|
|
75
|
+
/>
|
|
76
|
+
</button>
|
|
77
|
+
</PopoverTrigger>
|
|
78
|
+
<PopoverContent
|
|
79
|
+
className="language-selector__popover"
|
|
80
|
+
align="end"
|
|
81
|
+
side="bottom"
|
|
82
|
+
sideOffset={8}>
|
|
83
|
+
<div className="language-selector__content">
|
|
84
|
+
{languages.map((language) => (
|
|
85
|
+
<div
|
|
86
|
+
className={cn('language-selector__option', {
|
|
87
|
+
'language-selector__option--selected': selectedLanguage === language,
|
|
88
|
+
})}
|
|
89
|
+
onClick={() => handleSelectLanguage(language)}
|
|
90
|
+
key={language}>
|
|
91
|
+
<span className="language-selector__flag">
|
|
92
|
+
<i className={`fi fi-${languagesMap[language.toLowerCase()]}`}></i>
|
|
93
|
+
</span>
|
|
94
|
+
{language}
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -56,7 +56,7 @@ function TextWithIcon(props: TextWithIconProps) {
|
|
|
56
56
|
return (
|
|
57
57
|
<div
|
|
58
58
|
className={cn(
|
|
59
|
-
'flex gap-x-2',
|
|
59
|
+
'flex gap-x-2 items-center',
|
|
60
60
|
color === 'yellow' && 'text-[var(--color-yellow-600)]',
|
|
61
61
|
color === 'accent' && 'text-[var(--color-text-accent)]'
|
|
62
62
|
)}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ProposalSorted } from '@/src/types/docket/docket.types';
|
|
2
2
|
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Breakpoint } from '../../../hooks/useMobile';
|
|
3
4
|
import '../../../styles/components/organism/docket.css';
|
|
4
5
|
import {
|
|
5
6
|
AccommodationService,
|
|
@@ -18,19 +19,30 @@ import { TransferDocket } from '../../molecules/TransferDocket/TransferDocket';
|
|
|
18
19
|
import DocketAccordion from '../DocketAccordion/DocketAccordion';
|
|
19
20
|
import { DocketCollapsedHeader } from './DocketCollapsedHeader';
|
|
20
21
|
import { DocketEmptyState } from './DocketEmptyState';
|
|
21
|
-
import { DocketFooter } from './DocketFooter';
|
|
22
|
+
import { DocketFooter, DocketFooterLabelsType } from './DocketFooter';
|
|
22
23
|
import { DocketHeader } from './DocketHeader';
|
|
23
24
|
|
|
24
25
|
// Re-export sub-components and their props
|
|
25
26
|
export { DocketCollapsedHeader } from './DocketCollapsedHeader';
|
|
26
27
|
export type { DocketCollapsedHeaderProps } from './DocketCollapsedHeader';
|
|
27
28
|
export { DocketEmptyState } from './DocketEmptyState';
|
|
28
|
-
export type {
|
|
29
|
+
export type {
|
|
30
|
+
DocketEmptyStateLabelsType,
|
|
31
|
+
DocketEmptyStateProps,
|
|
32
|
+
} from './DocketEmptyState';
|
|
29
33
|
export { DocketFooter } from './DocketFooter';
|
|
30
34
|
export type { DocketFooterProps } from './DocketFooter';
|
|
31
35
|
export { DocketHeader } from './DocketHeader';
|
|
32
36
|
export type { DocketHeaderProps } from './DocketHeader';
|
|
33
37
|
|
|
38
|
+
export type DocketLabelsType = {
|
|
39
|
+
manageQuotation: string;
|
|
40
|
+
close: string;
|
|
41
|
+
/** Bold part of the empty state message */
|
|
42
|
+
emptyStateBold?: string;
|
|
43
|
+
/** Regular part of the empty state message */
|
|
44
|
+
emptyStateRegular?: string;
|
|
45
|
+
} & DocketFooterLabelsType;
|
|
34
46
|
export interface DocketProps {
|
|
35
47
|
/**
|
|
36
48
|
* Array of nested Docket objects
|
|
@@ -128,11 +140,6 @@ export interface DocketProps {
|
|
|
128
140
|
*/
|
|
129
141
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
130
142
|
|
|
131
|
-
/**
|
|
132
|
-
* Optional data attributes for testing or tracking
|
|
133
|
-
*/
|
|
134
|
-
'data-testid'?: string;
|
|
135
|
-
|
|
136
143
|
/**
|
|
137
144
|
* Remove mode - replaces price chips with remove buttons in all service dockets
|
|
138
145
|
*/
|
|
@@ -143,6 +150,31 @@ export interface DocketProps {
|
|
|
143
150
|
* Receives the service index and service data
|
|
144
151
|
*/
|
|
145
152
|
onServiceRemove?: (index: number, service: DocketService) => void;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Button done mode - replaces the "Book now" button with a "Done" button
|
|
156
|
+
*/
|
|
157
|
+
doneButtonMode?: boolean;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handler for the "Done" button click
|
|
161
|
+
*/
|
|
162
|
+
onDoneClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Labels for the docket
|
|
166
|
+
*/
|
|
167
|
+
labels?: DocketLabelsType;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Maximum number of proposals
|
|
171
|
+
*/
|
|
172
|
+
maxProposals?: number;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* URL of the empty state illustration image (when there are no proposals or no services)
|
|
176
|
+
*/
|
|
177
|
+
emptyStateImageUrl?: string;
|
|
146
178
|
}
|
|
147
179
|
|
|
148
180
|
/**
|
|
@@ -150,7 +182,7 @@ export interface DocketProps {
|
|
|
150
182
|
* It provides a structured layout for travel booking summaries with sections
|
|
151
183
|
* for accommodation, services, pricing, and actions.
|
|
152
184
|
*/
|
|
153
|
-
export
|
|
185
|
+
export function Docket({
|
|
154
186
|
proposals,
|
|
155
187
|
activeProposal: initialActiveProposal = 0,
|
|
156
188
|
onProposalSelect,
|
|
@@ -166,10 +198,25 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
166
198
|
className = '',
|
|
167
199
|
containerClassName = '',
|
|
168
200
|
onClick,
|
|
169
|
-
'data-testid': testId,
|
|
170
201
|
removeMode = false,
|
|
171
202
|
onServiceRemove,
|
|
172
|
-
|
|
203
|
+
doneButtonMode = false,
|
|
204
|
+
onDoneClick,
|
|
205
|
+
maxProposals = 5,
|
|
206
|
+
emptyStateImageUrl,
|
|
207
|
+
labels = {
|
|
208
|
+
manageQuotation: 'Manage quotation',
|
|
209
|
+
close: 'Close',
|
|
210
|
+
addNewQuote: 'Add new quote',
|
|
211
|
+
compare: 'Compare',
|
|
212
|
+
view: 'View',
|
|
213
|
+
save: 'Save',
|
|
214
|
+
bookNow: 'Book now',
|
|
215
|
+
done: 'Done',
|
|
216
|
+
emptyStateBold: 'Start by adding a service here—',
|
|
217
|
+
emptyStateRegular: "you'll be able to create and compare multiple quotations.",
|
|
218
|
+
},
|
|
219
|
+
}: DocketProps): React.ReactElement {
|
|
173
220
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
174
221
|
const [isMobile, setIsMobile] = useState<boolean>(false);
|
|
175
222
|
const [expandedProposalIndex, setExpandedProposalIndex] =
|
|
@@ -177,7 +224,7 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
177
224
|
|
|
178
225
|
useEffect(() => {
|
|
179
226
|
const checkMobile = () => {
|
|
180
|
-
setIsMobile(window.innerWidth <
|
|
227
|
+
setIsMobile(window.innerWidth < Breakpoint.LG); // Desktop S breakpoint (1440px)
|
|
181
228
|
};
|
|
182
229
|
|
|
183
230
|
checkMobile();
|
|
@@ -260,7 +307,16 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
260
307
|
|
|
261
308
|
const renderContent = () => {
|
|
262
309
|
if (!proposals || proposals.length === 0) {
|
|
263
|
-
return
|
|
310
|
+
return (
|
|
311
|
+
<DocketEmptyState
|
|
312
|
+
imageUrl={emptyStateImageUrl}
|
|
313
|
+
labels={
|
|
314
|
+
labels?.emptyStateBold != null && labels?.emptyStateRegular != null
|
|
315
|
+
? { bold: labels.emptyStateBold, regular: labels.emptyStateRegular }
|
|
316
|
+
: undefined
|
|
317
|
+
}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
264
320
|
}
|
|
265
321
|
|
|
266
322
|
// If there are multiple proposals, render each in a DocketAccordion
|
|
@@ -276,6 +332,13 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
276
332
|
expanded={index === expandedProposalIndex}
|
|
277
333
|
onExpandedChange={(isExpanded) => handleProposalToggle(index, isExpanded)}
|
|
278
334
|
moreOptions={accordionMoreOptions}
|
|
335
|
+
manageQuotationLabel={labels?.manageQuotation}
|
|
336
|
+
emptyStateImageUrl={emptyStateImageUrl}
|
|
337
|
+
emptyStateLabels={
|
|
338
|
+
labels?.emptyStateBold != null && labels?.emptyStateRegular != null
|
|
339
|
+
? { bold: labels.emptyStateBold, regular: labels.emptyStateRegular }
|
|
340
|
+
: undefined
|
|
341
|
+
}
|
|
279
342
|
/>
|
|
280
343
|
))}
|
|
281
344
|
</div>
|
|
@@ -287,7 +350,16 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
287
350
|
const allServices = currentProposal.Services || [];
|
|
288
351
|
|
|
289
352
|
if (allServices.length === 0) {
|
|
290
|
-
return
|
|
353
|
+
return (
|
|
354
|
+
<DocketEmptyState
|
|
355
|
+
imageUrl={emptyStateImageUrl}
|
|
356
|
+
labels={
|
|
357
|
+
labels?.emptyStateBold != null && labels?.emptyStateRegular != null
|
|
358
|
+
? { bold: labels.emptyStateBold, regular: labels.emptyStateRegular }
|
|
359
|
+
: undefined
|
|
360
|
+
}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
291
363
|
}
|
|
292
364
|
|
|
293
365
|
const renderedServices = allServices
|
|
@@ -295,7 +367,16 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
295
367
|
.filter(Boolean);
|
|
296
368
|
|
|
297
369
|
if (renderedServices.length === 0) {
|
|
298
|
-
return
|
|
370
|
+
return (
|
|
371
|
+
<DocketEmptyState
|
|
372
|
+
imageUrl={emptyStateImageUrl}
|
|
373
|
+
labels={
|
|
374
|
+
labels?.emptyStateBold != null && labels?.emptyStateRegular != null
|
|
375
|
+
? { bold: labels.emptyStateBold, regular: labels.emptyStateRegular }
|
|
376
|
+
: undefined
|
|
377
|
+
}
|
|
378
|
+
/>
|
|
379
|
+
);
|
|
299
380
|
}
|
|
300
381
|
|
|
301
382
|
return <div className="docket__services">{renderedServices}</div>;
|
|
@@ -305,7 +386,7 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
305
386
|
const allPrices = proposals && proposals.length === 1 ? proposals[0].Prices || [] : [];
|
|
306
387
|
|
|
307
388
|
const renderDocketContent = () => (
|
|
308
|
-
<div className={classes} onClick={onClick}
|
|
389
|
+
<div className={classes} onClick={onClick}>
|
|
309
390
|
<DocketHeader
|
|
310
391
|
title={title}
|
|
311
392
|
moreOptions={moreOptions}
|
|
@@ -326,6 +407,10 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
326
407
|
onViewClick={onViewClick}
|
|
327
408
|
onSaveClick={onSaveClick}
|
|
328
409
|
onBookNowClick={onBookNowClick}
|
|
410
|
+
doneButtonMode={doneButtonMode}
|
|
411
|
+
onDoneClick={onDoneClick}
|
|
412
|
+
labels={labels}
|
|
413
|
+
disableAddNewQuoteButton={proposals ? proposals.length >= maxProposals : false}
|
|
329
414
|
/>
|
|
330
415
|
</div>
|
|
331
416
|
);
|
|
@@ -341,23 +426,20 @@ export const Docket: React.FC<DocketProps> = ({
|
|
|
341
426
|
)}
|
|
342
427
|
{isMobile && isOpen && (
|
|
343
428
|
<div className="docket__mobile-wrapper">
|
|
344
|
-
<
|
|
429
|
+
<button className="docket__close-header" onClick={handleClose}>
|
|
345
430
|
<div className="docket__close-header-content">
|
|
346
|
-
<h2 className="docket__close-header-text">
|
|
347
|
-
<button
|
|
348
|
-
className="docket__close-button"
|
|
349
|
-
onClick={handleClose}
|
|
350
|
-
aria-label="Close docket">
|
|
431
|
+
<h2 className="docket__close-header-text">{labels?.close}</h2>
|
|
432
|
+
<div className="docket__close-button" aria-label="Close docket">
|
|
351
433
|
<Icon name="close" size="lg" />
|
|
352
|
-
</
|
|
434
|
+
</div>
|
|
353
435
|
</div>
|
|
354
|
-
</
|
|
436
|
+
</button>
|
|
355
437
|
{renderDocketContent()}
|
|
356
438
|
</div>
|
|
357
439
|
)}
|
|
358
440
|
{!isMobile && renderDocketContent()}
|
|
359
441
|
</div>
|
|
360
442
|
);
|
|
361
|
-
}
|
|
443
|
+
}
|
|
362
444
|
|
|
363
445
|
export default Docket;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
export type DocketEmptyStateLabelsType = {
|
|
4
|
+
/** Bold part of the empty state message */
|
|
5
|
+
bold: string;
|
|
6
|
+
/** Regular part of the empty state message */
|
|
7
|
+
regular: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
3
10
|
export interface DocketEmptyStateProps {
|
|
11
|
+
/**
|
|
12
|
+
* URL of the empty state illustration image
|
|
13
|
+
*/
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Labels for the empty state text (bold and regular parts)
|
|
18
|
+
*/
|
|
19
|
+
labels?: DocketEmptyStateLabelsType;
|
|
20
|
+
|
|
4
21
|
/**
|
|
5
22
|
* Additional CSS classes
|
|
6
23
|
*/
|
|
@@ -10,7 +27,16 @@ export interface DocketEmptyStateProps {
|
|
|
10
27
|
/**
|
|
11
28
|
* Empty state component for docket when no content is provided
|
|
12
29
|
*/
|
|
30
|
+
const DEFAULT_EMPTY_STATE_IMAGE = '/images/docket-empty-illustration.svg';
|
|
31
|
+
|
|
32
|
+
const DEFAULT_EMPTY_STATE_LABELS: DocketEmptyStateLabelsType = {
|
|
33
|
+
bold: 'Start by adding a service here—',
|
|
34
|
+
regular: "you'll be able to create and compare multiple quotations.",
|
|
35
|
+
};
|
|
36
|
+
|
|
13
37
|
export const DocketEmptyState: React.FC<DocketEmptyStateProps> = ({
|
|
38
|
+
imageUrl = DEFAULT_EMPTY_STATE_IMAGE,
|
|
39
|
+
labels = DEFAULT_EMPTY_STATE_LABELS,
|
|
14
40
|
className = '',
|
|
15
41
|
}) => {
|
|
16
42
|
return (
|
|
@@ -18,18 +44,14 @@ export const DocketEmptyState: React.FC<DocketEmptyStateProps> = ({
|
|
|
18
44
|
<div className="docket__empty-state-content">
|
|
19
45
|
<div className="docket__empty-state-illustration">
|
|
20
46
|
<img
|
|
21
|
-
src=
|
|
47
|
+
src={imageUrl}
|
|
22
48
|
alt="Empty docket illustration"
|
|
23
49
|
className="docket__empty-state-image"
|
|
24
50
|
/>
|
|
25
51
|
</div>
|
|
26
52
|
<p className="docket__empty-state-text">
|
|
27
|
-
<span className="docket__empty-state-text-bold">
|
|
28
|
-
|
|
29
|
-
</span>
|
|
30
|
-
<span className="docket__empty-state-text-regular">
|
|
31
|
-
you'll be able to create and compare multiple quotations.
|
|
32
|
-
</span>
|
|
53
|
+
<span className="docket__empty-state-text-bold">{labels.bold}</span>
|
|
54
|
+
<span className="docket__empty-state-text-regular">{labels.regular}</span>
|
|
33
55
|
</p>
|
|
34
56
|
</div>
|
|
35
57
|
</div>
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Button from '../../atoms/Button/Button';
|
|
3
3
|
|
|
4
|
+
export type DocketFooterLabelsType = {
|
|
5
|
+
addNewQuote: string;
|
|
6
|
+
compare: string;
|
|
7
|
+
view: string;
|
|
8
|
+
save: string;
|
|
9
|
+
bookNow: string;
|
|
10
|
+
done: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
4
13
|
export interface DocketFooterProps {
|
|
5
14
|
/**
|
|
6
15
|
* Handler for "Add new quote" button click
|
|
@@ -36,6 +45,26 @@ export interface DocketFooterProps {
|
|
|
36
45
|
* Additional CSS classes
|
|
37
46
|
*/
|
|
38
47
|
className?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Button done mode - replaces the "Book now" button with a "Done" button
|
|
51
|
+
*/
|
|
52
|
+
doneButtonMode?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handler for the "Done" button click
|
|
56
|
+
*/
|
|
57
|
+
onDoneClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Disable the "Add new quote" button
|
|
61
|
+
*/
|
|
62
|
+
disableAddNewQuoteButton?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Labels for the buttons
|
|
66
|
+
*/
|
|
67
|
+
labels?: DocketFooterLabelsType;
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
/**
|
|
@@ -49,19 +78,34 @@ export const DocketFooter: React.FC<DocketFooterProps> = ({
|
|
|
49
78
|
onSaveClick,
|
|
50
79
|
onBookNowClick,
|
|
51
80
|
className = '',
|
|
81
|
+
doneButtonMode = false,
|
|
82
|
+
onDoneClick,
|
|
83
|
+
disableAddNewQuoteButton = false,
|
|
84
|
+
labels = {
|
|
85
|
+
addNewQuote: 'Add new quote',
|
|
86
|
+
compare: 'Compare',
|
|
87
|
+
view: 'View',
|
|
88
|
+
save: 'Save',
|
|
89
|
+
bookNow: 'Book now',
|
|
90
|
+
done: 'Done',
|
|
91
|
+
},
|
|
52
92
|
}) => {
|
|
93
|
+
const isDoneMode = doneButtonMode && onDoneClick;
|
|
94
|
+
|
|
53
95
|
return (
|
|
54
96
|
<div className={`docket__footer ${className}`}>
|
|
55
|
-
{(onAddNewQuoteClick || onCompareClick) && (
|
|
97
|
+
{!isDoneMode && (onAddNewQuoteClick || onCompareClick) && (
|
|
56
98
|
<div className="docket__footer-top-buttons">
|
|
57
99
|
{onAddNewQuoteClick && (
|
|
58
100
|
<Button
|
|
59
101
|
variant="outline-secondary"
|
|
60
102
|
size="sm"
|
|
61
|
-
leadingIcon=
|
|
103
|
+
leadingIcon={disableAddNewQuoteButton ? undefined : 'plus'}
|
|
104
|
+
trailingIcon={disableAddNewQuoteButton ? 'info' : undefined}
|
|
105
|
+
disabled={disableAddNewQuoteButton}
|
|
62
106
|
onClick={onAddNewQuoteClick}
|
|
63
107
|
className={`docket__footer-button ${!showCompareButton ? 'docket__footer-button--full' : ''}`}>
|
|
64
|
-
|
|
108
|
+
{labels.addNewQuote}
|
|
65
109
|
</Button>
|
|
66
110
|
)}
|
|
67
111
|
{showCompareButton && onCompareClick && (
|
|
@@ -71,38 +115,50 @@ export const DocketFooter: React.FC<DocketFooterProps> = ({
|
|
|
71
115
|
onClick={onCompareClick}
|
|
72
116
|
leadingIcon="document"
|
|
73
117
|
className="docket__footer-button">
|
|
74
|
-
|
|
118
|
+
{labels.compare}
|
|
75
119
|
</Button>
|
|
76
120
|
)}
|
|
77
121
|
</div>
|
|
78
122
|
)}
|
|
79
123
|
<div className="docket__footer-actions">
|
|
80
|
-
{
|
|
81
|
-
<Button
|
|
82
|
-
variant="outline-secondary"
|
|
83
|
-
size="sm"
|
|
84
|
-
onClick={onViewClick}
|
|
85
|
-
className="docket__footer-view-link">
|
|
86
|
-
<span className="docket__footer-view-text">View</span>
|
|
87
|
-
</Button>
|
|
88
|
-
)}
|
|
89
|
-
{onSaveClick && (
|
|
90
|
-
<Button
|
|
91
|
-
variant="outline-secondary"
|
|
92
|
-
size="sm"
|
|
93
|
-
onClick={onSaveClick}
|
|
94
|
-
className="docket__footer-button">
|
|
95
|
-
Save
|
|
96
|
-
</Button>
|
|
97
|
-
)}
|
|
98
|
-
{onBookNowClick && (
|
|
124
|
+
{isDoneMode ? (
|
|
99
125
|
<Button
|
|
100
126
|
variant="secondary"
|
|
101
127
|
size="sm"
|
|
102
|
-
onClick={
|
|
103
|
-
className="docket__footer-button docket__footer-button--primary">
|
|
104
|
-
|
|
128
|
+
onClick={onDoneClick}
|
|
129
|
+
className="docket__footer-button docket__footer-button--primary docket__footer-button--half">
|
|
130
|
+
{labels.done}
|
|
105
131
|
</Button>
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
{onViewClick && (
|
|
135
|
+
<Button
|
|
136
|
+
variant="outline-secondary"
|
|
137
|
+
size="sm"
|
|
138
|
+
onClick={onViewClick}
|
|
139
|
+
className="docket__footer-button">
|
|
140
|
+
<span className="docket__footer-view-text">{labels.view}</span>
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
{onSaveClick && (
|
|
144
|
+
<Button
|
|
145
|
+
variant="outline-secondary"
|
|
146
|
+
size="sm"
|
|
147
|
+
onClick={onSaveClick}
|
|
148
|
+
className="docket__footer-button">
|
|
149
|
+
{labels.save}
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
152
|
+
{onBookNowClick && (
|
|
153
|
+
<Button
|
|
154
|
+
variant="secondary"
|
|
155
|
+
size="sm"
|
|
156
|
+
onClick={onBookNowClick}
|
|
157
|
+
className="docket__footer-button docket__footer-button--primary">
|
|
158
|
+
{labels.bookNow}
|
|
159
|
+
</Button>
|
|
160
|
+
)}
|
|
161
|
+
</>
|
|
106
162
|
)}
|
|
107
163
|
</div>
|
|
108
164
|
</div>
|
|
@@ -18,7 +18,7 @@ import DocketPrices from '../../molecules/DocketPrices';
|
|
|
18
18
|
import { ExcursionDocket } from '../../molecules/ExcursionDocket/ExcursionDocket';
|
|
19
19
|
import { OtherServiceDocket } from '../../molecules/OtherServiceDocket/OtherServiceDocket';
|
|
20
20
|
import { TransferDocket } from '../../molecules/TransferDocket/TransferDocket';
|
|
21
|
-
import { DocketEmptyState } from '../Docket/Docket';
|
|
21
|
+
import { DocketEmptyState, type DocketEmptyStateLabelsType } from '../Docket/Docket';
|
|
22
22
|
|
|
23
23
|
export interface DocketAccordionProps {
|
|
24
24
|
proposal: ProposalSorted;
|
|
@@ -29,6 +29,14 @@ export interface DocketAccordionProps {
|
|
|
29
29
|
expanded?: boolean;
|
|
30
30
|
onExpandedChange?: (expanded: boolean) => void;
|
|
31
31
|
moreOptions?: ActionDropdownItem[];
|
|
32
|
+
/**
|
|
33
|
+
* URL of the empty state illustration image (when the proposal has no services)
|
|
34
|
+
*/
|
|
35
|
+
emptyStateImageUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Labels for the empty state (bold and regular text) when the proposal has no services
|
|
38
|
+
*/
|
|
39
|
+
emptyStateLabels?: DocketEmptyStateLabelsType;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
@@ -52,6 +60,8 @@ export default function DocketAccordion({
|
|
|
52
60
|
onExpandedChange,
|
|
53
61
|
moreOptions = [],
|
|
54
62
|
manageQuotationLabel = 'Manage quotation',
|
|
63
|
+
emptyStateImageUrl,
|
|
64
|
+
emptyStateLabels,
|
|
55
65
|
}: DocketAccordionProps) {
|
|
56
66
|
const allServices = proposal.Services || [];
|
|
57
67
|
|
|
@@ -157,7 +167,7 @@ export default function DocketAccordion({
|
|
|
157
167
|
footer={allServices?.length > 0 ? renderAccordionFooter : undefined}>
|
|
158
168
|
<div className="docket-accordion__content">
|
|
159
169
|
{allServices.length === 0 ? (
|
|
160
|
-
<DocketEmptyState />
|
|
170
|
+
<DocketEmptyState imageUrl={emptyStateImageUrl} labels={emptyStateLabels} />
|
|
161
171
|
) : (
|
|
162
172
|
<div className="docket-accordion__services">
|
|
163
173
|
{allServices.map((service, index) => renderService(service, index))}
|