You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-06-24 22:36:43 +02:00
Further dive into decomposing
This commit is contained in:
@ -10,9 +10,9 @@ The `src` folder contains a TypeScript code for the component and corresponding
|
||||
The `index.html` page includes a living example for each of the discussed scenarios, with links pointing to external playgrounds to work through the code if needed. [View it in your browser](https://twirl.github.io/examples/01.%20Decomposing%20UI%20Components/index.html).
|
||||
|
||||
The following improvements to the code are left as an exercise for the reader:
|
||||
* Make all builder functions options
|
||||
|
||||
* Returning operation status from the `SearchBox.search` method:
|
||||
* Make all builder functions configurable through options
|
||||
* Create a separate composer to close the gap between `OfferPanelComponent` and its buttons
|
||||
* Add returning an operation status from the `SearchBox.search` method:
|
||||
```
|
||||
public search(query: string): Promise<OperationResult>
|
||||
```
|
||||
|
@ -18,6 +18,16 @@
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
header * {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
ul#example-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
@ -51,6 +61,7 @@
|
||||
|
||||
#live-example {
|
||||
min-width: 390px;
|
||||
min-height: 500px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
@ -66,10 +77,27 @@
|
||||
<script src="sandbox.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
Examples of Overriding Different Aspects of a Decomposed UI
|
||||
Component
|
||||
</h1>
|
||||
<header>
|
||||
<p>
|
||||
Studying materials for
|
||||
<strong
|
||||
><a href="https://twirl.github.io/The-API-Book/"
|
||||
>“The API” book</a
|
||||
>
|
||||
by Sergey Konstantinov.</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://twirl.github.io/The-API-Book/#sdk-decomposing"
|
||||
>Chapter 43: Problems of Introducing UI Components</a
|
||||
>. Find the source code of the components and additional tasks
|
||||
for self-study at the
|
||||
<a
|
||||
href="https://github.com/twirl/The-API-Book/tree/gh-pages/docs/examples/01.%20Decomposing%20UI%20Components"
|
||||
>Github repository.</a
|
||||
>
|
||||
</p>
|
||||
</header>
|
||||
<div id="playground">
|
||||
<div id="live-example">
|
||||
<div>Live example <button id="refresh">Refresh</button></div>
|
||||
@ -77,26 +105,32 @@
|
||||
</div>
|
||||
<div id="examples">
|
||||
<ul id="example-list">
|
||||
<li id="example-00">
|
||||
<a href="javascript:showExample('00')"
|
||||
>The reference: A regular <code>SearchBox</code></a
|
||||
>
|
||||
</li>
|
||||
<li id="example-01">
|
||||
<a href="javascript:showExample('01')"
|
||||
>Example #1: A regular <code>SearchBox</code></a
|
||||
>Example #1: A <code>SearchBox</code> with a custom
|
||||
icon</a
|
||||
>
|
||||
</li>
|
||||
<li id="example-02">
|
||||
<a href="javascript:showExample('02')"
|
||||
>Example #2: A <code>SearchBox</code> with a custom
|
||||
icon</a
|
||||
>Example #2: A <code>SearchBox</code> with a map</a
|
||||
>
|
||||
</li>
|
||||
<li id="example-03">
|
||||
<a href="javascript:showExample('03')"
|
||||
>Example #3: A <code>SearchBox</code> with a map</a
|
||||
>Example #3: A <code>SearchBox</code> with in-place
|
||||
actions</a
|
||||
>
|
||||
</li>
|
||||
<li id="example-04">
|
||||
<a href="javascript:showExample('04')"
|
||||
>Example #4: A <code>SearchBox</code> with in-place
|
||||
actions</a
|
||||
>Example #4: A <code>SearchBox</code> with a dynamic
|
||||
set of buttons</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
File diff suppressed because one or more lines are too long
12
docs/examples/01. Decomposing UI Components/samples/00.js
Normal file
12
docs/examples/01. Decomposing UI Components/samples/00.js
Normal file
@ -0,0 +1,12 @@
|
||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
createOrder(offer) {
|
||||
alert(`Isn't actually implemented (yet)`);
|
||||
return super.createOrder(offer);
|
||||
}
|
||||
}
|
||||
|
||||
const searchBox = new CustomSearchBox(
|
||||
document.getElementById("search-box"),
|
||||
ourCoffeeSdk.dummyCoffeeApi
|
||||
);
|
||||
searchBox.search("Lungo");
|
@ -1,4 +1,56 @@
|
||||
const buildCustomOrderButton = function (
|
||||
offer,
|
||||
container
|
||||
) {
|
||||
return ourCoffeeSdk.OfferPanelComponent.buildCreateOrderButton(
|
||||
offer,
|
||||
container,
|
||||
{
|
||||
createOrderButtonUrl:
|
||||
offer && offer.createOrderButtonIcon,
|
||||
createOrderButtonText:
|
||||
(offer &&
|
||||
`Buy now for just ${offer.price.formattedValue}`) ||
|
||||
"Place an Order"
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||
generateOfferPreviews(offerList) {
|
||||
const result = super.generateOfferPreviews(
|
||||
offerList
|
||||
);
|
||||
return result === null
|
||||
? result
|
||||
: result.map((preview, index) => ({
|
||||
...preview,
|
||||
imageUrl: offerList[index].place.icon
|
||||
}));
|
||||
}
|
||||
|
||||
generateCurrentOfferFullView(offer, options) {
|
||||
return offer === null
|
||||
? offer
|
||||
: {
|
||||
...super.generateCurrentOfferFullView(
|
||||
offer,
|
||||
options
|
||||
),
|
||||
createOrderButtonIcon: offer.place.icon
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
buildComposer(context, container, options) {
|
||||
return new CustomComposer(
|
||||
context,
|
||||
container,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
createOrder(offer) {
|
||||
alert(`Isn't actually implemented (yet)`);
|
||||
return super.createOrder(offer);
|
||||
@ -7,6 +59,16 @@ class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
|
||||
const searchBox = new CustomSearchBox(
|
||||
document.getElementById("search-box"),
|
||||
ourCoffeeSdk.dummyCoffeeApi
|
||||
ourCoffeeSdk.dummyCoffeeApi,
|
||||
{
|
||||
offerPanel: {
|
||||
buttonBuilders: [
|
||||
buildCustomOrderButton,
|
||||
ourCoffeeSdk.OfferPanelComponent
|
||||
.buildCloseButton
|
||||
],
|
||||
closeButtonText: "❌Not Now"
|
||||
}
|
||||
}
|
||||
);
|
||||
searchBox.search("Lungo");
|
||||
|
@ -1,22 +1,78 @@
|
||||
const buildCustomOrderButton = function (
|
||||
offer,
|
||||
container
|
||||
) {
|
||||
return ourCoffeeSdk.OfferPanelComponent.buildCreateOrderButton(
|
||||
offer,
|
||||
container,
|
||||
{
|
||||
createOrderButtonUrl:
|
||||
offer && offer.createOrderButtonIcon,
|
||||
createOrderButtonText:
|
||||
(offer &&
|
||||
`Buy now for just ${offer.price.formattedValue}`) ||
|
||||
"Place an Order"
|
||||
class CustomOfferList {
|
||||
constructor(context, container, offerList) {
|
||||
this.context = context;
|
||||
this.container = container;
|
||||
this.events =
|
||||
new ourCoffeeSdk.util.EventEmitter();
|
||||
this.offerList = null;
|
||||
this.map = null;
|
||||
|
||||
this.onMarkerSelect = (markerId) => {
|
||||
this.events.emit("offerSelect", {
|
||||
offerId: markerId
|
||||
});
|
||||
};
|
||||
this.setOfferList = ({
|
||||
offerList: newOfferList
|
||||
}) => {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
this.map = null;
|
||||
}
|
||||
this.offerList = newOfferList;
|
||||
if (newOfferList) {
|
||||
this.map = new ourCoffeeSdk.DummyMapApi(
|
||||
this.container,
|
||||
[
|
||||
[16.355, 48.206],
|
||||
[16.375, 48.214]
|
||||
]
|
||||
);
|
||||
for (const offer of newOfferList) {
|
||||
this.map.addMarker(
|
||||
offer.offerId,
|
||||
offer.location,
|
||||
this.onMarkerSelect
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setOfferList({ offerList });
|
||||
this.contextListener = context.events.on(
|
||||
"offerPreviewListChange",
|
||||
this.setOfferList
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
}
|
||||
);
|
||||
};
|
||||
this.contextListener.off();
|
||||
}
|
||||
}
|
||||
|
||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||
buildOfferListComponent(
|
||||
context,
|
||||
container,
|
||||
offerList,
|
||||
contextOptions
|
||||
) {
|
||||
return new CustomOfferList(
|
||||
context,
|
||||
container,
|
||||
this.generateOfferPreviews(
|
||||
offerList,
|
||||
contextOptions
|
||||
),
|
||||
this.generateOfferListComponentOptions(
|
||||
contextOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
generateOfferPreviews(offerList) {
|
||||
const result = super.generateOfferPreviews(
|
||||
offerList
|
||||
@ -25,21 +81,10 @@ class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||
? result
|
||||
: result.map((preview, index) => ({
|
||||
...preview,
|
||||
imageUrl: offerList[index].place.icon
|
||||
location:
|
||||
offerList[index].place.location
|
||||
}));
|
||||
}
|
||||
|
||||
generateCurrentOfferFullView(offer, options) {
|
||||
return offer === null
|
||||
? offer
|
||||
: {
|
||||
...super.generateCurrentOfferFullView(
|
||||
offer,
|
||||
options
|
||||
),
|
||||
createOrderButtonIcon: offer.place.icon
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
@ -59,16 +104,6 @@ class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
|
||||
const searchBox = new CustomSearchBox(
|
||||
document.getElementById("search-box"),
|
||||
ourCoffeeSdk.dummyCoffeeApi,
|
||||
{
|
||||
offerPanel: {
|
||||
buttonBuilders: [
|
||||
buildCustomOrderButton,
|
||||
ourCoffeeSdk.OfferPanelComponent
|
||||
.buildCloseButton
|
||||
],
|
||||
closeButtonText: "❌Not Now"
|
||||
}
|
||||
}
|
||||
ourCoffeeSdk.dummyCoffeeApi
|
||||
);
|
||||
searchBox.search("Lungo");
|
||||
|
@ -1,55 +1,75 @@
|
||||
class CustomOfferList {
|
||||
constructor(context, container, offerList) {
|
||||
this.context = context;
|
||||
this.container = container;
|
||||
this.events =
|
||||
new ourCoffeeSdk.util.EventEmitter();
|
||||
this.offerList = null;
|
||||
this.map = null;
|
||||
|
||||
this.onMarkerSelect = (markerId) => {
|
||||
this.events.emit("offerSelect", {
|
||||
offerId: markerId
|
||||
});
|
||||
};
|
||||
this.setOfferList = ({
|
||||
offerList: newOfferList
|
||||
}) => {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
this.map = null;
|
||||
}
|
||||
this.offerList = newOfferList;
|
||||
if (newOfferList) {
|
||||
this.map = new ourCoffeeSdk.DummyMapApi(
|
||||
this.container,
|
||||
[
|
||||
[16.355, 48.206],
|
||||
[16.375, 48.214]
|
||||
]
|
||||
);
|
||||
for (const offer of newOfferList) {
|
||||
this.map.addMarker(
|
||||
offer.offerId,
|
||||
offer.location,
|
||||
this.onMarkerSelect
|
||||
class CustomOfferList extends ourCoffeeSdk.OfferListComponent {
|
||||
constructor(
|
||||
context,
|
||||
container,
|
||||
offerList,
|
||||
options
|
||||
) {
|
||||
super(context, container, offerList, options);
|
||||
this.onOfferButtonClickListener = (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
const offerId = e.target.dataset.offerId;
|
||||
if (action === "offerSelect") {
|
||||
this.events.emit(action, { offerId });
|
||||
} else if (action === "createOffer") {
|
||||
const offer =
|
||||
this.context.findOfferById(offerId);
|
||||
if (offer) {
|
||||
this.context.events.emit(
|
||||
"createOrder",
|
||||
{
|
||||
offer
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.setOfferList({ offerList });
|
||||
this.contextListener = context.events.on(
|
||||
"offerPreviewListChange",
|
||||
this.setOfferList
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
generateOfferHtml(offer) {
|
||||
return ourCoffeeSdk.util.html`<li
|
||||
class="custom-offer"
|
||||
>
|
||||
<aside><button
|
||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||
offer.offerId
|
||||
)}"
|
||||
data-action="createOffer">Buy now for ${
|
||||
offer.price.formattedValue
|
||||
}</button><button
|
||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||
offer.offerId
|
||||
)}"
|
||||
data-action="offerSelect">View details
|
||||
</button></aside>
|
||||
<div><strong>${offer.title}</strong></div>
|
||||
<div>${offer.subtitle}</div>
|
||||
<div>${offer.bottomLine}</div>
|
||||
</li>`.toString();
|
||||
}
|
||||
|
||||
setupDomListeners() {
|
||||
const buttons =
|
||||
this.container.querySelectorAll("button");
|
||||
for (const button of buttons) {
|
||||
button.addEventListener(
|
||||
"click",
|
||||
this.onOfferButtonClickListener
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
teardownDomListeners() {
|
||||
const buttons = document.querySelectorAll(
|
||||
this.container,
|
||||
"button"
|
||||
);
|
||||
for (const button of buttons) {
|
||||
button.removeEventListener(
|
||||
"click",
|
||||
this.onOfferButtonClickListener
|
||||
);
|
||||
}
|
||||
this.contextListener.off();
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,19 +92,6 @@ class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
generateOfferPreviews(offerList) {
|
||||
const result = super.generateOfferPreviews(
|
||||
offerList
|
||||
);
|
||||
return result === null
|
||||
? result
|
||||
: result.map((preview, index) => ({
|
||||
...preview,
|
||||
location:
|
||||
offerList[index].place.location
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
|
@ -1,102 +1,82 @@
|
||||
class CustomOfferList extends ourCoffeeSdk.OfferListComponent {
|
||||
constructor(
|
||||
context,
|
||||
container,
|
||||
offerList,
|
||||
options
|
||||
) {
|
||||
super(context, container, offerList, options);
|
||||
this.onOfferButtonClickListener = (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
const offerId = e.target.dataset.offerId;
|
||||
if (action === "offerSelect") {
|
||||
this.events.emit(action, { offerId });
|
||||
} else if (action === "createOffer") {
|
||||
const offer =
|
||||
this.context.findOfferById(offerId);
|
||||
if (offer) {
|
||||
this.context.events.emit(
|
||||
"createOrder",
|
||||
{
|
||||
offer
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const {
|
||||
SearchBox,
|
||||
SearchBoxComposer,
|
||||
OfferPanelComponent,
|
||||
OfferPanelButton,
|
||||
util,
|
||||
dummyCoffeeApi
|
||||
} = ourCoffeeSdk;
|
||||
|
||||
generateOfferHtml(offer) {
|
||||
return ourCoffeeSdk.util.html`<li
|
||||
class="custom-offer"
|
||||
>
|
||||
<div><strong>${offer.title}</strong></div>
|
||||
<div>${offer.subtitle}</div>
|
||||
<div>${offer.bottomLine} <button
|
||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||
offer.offerId
|
||||
)}"
|
||||
data-action="createOffer"
|
||||
>Buy now for ${
|
||||
offer.price.formattedValue
|
||||
}</button><button
|
||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||
offer.offerId
|
||||
)}"
|
||||
data-action="offerSelect"
|
||||
>View details</button>
|
||||
</div>
|
||||
<hr/>
|
||||
</li>`.toString();
|
||||
}
|
||||
const buildCallButton = function (
|
||||
offer,
|
||||
container,
|
||||
options
|
||||
) {
|
||||
return new OfferPanelButton("call", container, {
|
||||
text: util.html`<a href="tel:${util.attrValue(
|
||||
offer.phone
|
||||
)}" style="color: inherit; text-decoration: none;">${
|
||||
options.callButtonText
|
||||
}</a>`
|
||||
});
|
||||
};
|
||||
|
||||
setupDomListeners() {
|
||||
const buttons =
|
||||
this.container.querySelectorAll("button");
|
||||
for (const button of buttons) {
|
||||
button.addEventListener(
|
||||
"click",
|
||||
this.onOfferButtonClickListener
|
||||
);
|
||||
}
|
||||
}
|
||||
const buildCreateOrderButton =
|
||||
OfferPanelComponent.buildCreateOrderButton;
|
||||
const buildCloseButton =
|
||||
OfferPanelComponent.buildCloseButton;
|
||||
|
||||
teardownDomListeners() {
|
||||
const buttons = document.querySelectorAll(
|
||||
this.container,
|
||||
"button"
|
||||
);
|
||||
for (const button of buttons) {
|
||||
button.removeEventListener(
|
||||
"click",
|
||||
this.onOfferButtonClickListener
|
||||
);
|
||||
}
|
||||
class CustomOfferPanel extends OfferPanelComponent {
|
||||
show() {
|
||||
this.options.buttonBuilders = this
|
||||
.currentOffer.phone
|
||||
? [
|
||||
buildCreateOrderButton,
|
||||
buildCallButton,
|
||||
buildCloseButton
|
||||
]
|
||||
: [
|
||||
buildCreateOrderButton,
|
||||
buildCloseButton
|
||||
];
|
||||
return super.show();
|
||||
}
|
||||
}
|
||||
|
||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||
buildOfferListComponent(
|
||||
class CustomComposer extends SearchBoxComposer {
|
||||
buildOfferPanelComponent(
|
||||
context,
|
||||
container,
|
||||
offerList,
|
||||
currentOffer,
|
||||
contextOptions
|
||||
) {
|
||||
return new CustomOfferList(
|
||||
return new CustomOfferPanel(
|
||||
context,
|
||||
container,
|
||||
this.generateOfferPreviews(
|
||||
offerList,
|
||||
this.generateCurrentOfferFullView(
|
||||
currentOffer,
|
||||
contextOptions
|
||||
),
|
||||
this.generateOfferListComponentOptions(
|
||||
this.generateOfferPanelComponentOptions(
|
||||
contextOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
generateCurrentOfferFullView(offer, options) {
|
||||
const offerFullView =
|
||||
super.generateCurrentOfferFullView(
|
||||
offer,
|
||||
options
|
||||
);
|
||||
if (offer && offer.place.phone) {
|
||||
offerFullView.phone = offer.place.phone;
|
||||
}
|
||||
return offerFullView;
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
class CustomSearchBox extends SearchBox {
|
||||
buildComposer(context, container, options) {
|
||||
return new CustomComposer(
|
||||
context,
|
||||
@ -113,6 +93,13 @@ class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||
|
||||
const searchBox = new CustomSearchBox(
|
||||
document.getElementById("search-box"),
|
||||
ourCoffeeSdk.dummyCoffeeApi
|
||||
dummyCoffeeApi,
|
||||
{
|
||||
offerPanel: {
|
||||
createOrderButtonText: "🛒Place an Order",
|
||||
callButtonText: "☎️ Make a Call",
|
||||
closeButtonText: "❌Not Now"
|
||||
}
|
||||
}
|
||||
);
|
||||
searchBox.search("Lungo");
|
||||
|
@ -62,6 +62,6 @@ window.onload = function () {
|
||||
codeLens: false,
|
||||
fontFamily: 'local-monospace'
|
||||
});
|
||||
showExample('01');
|
||||
showExample('00');
|
||||
});
|
||||
};
|
||||
|
@ -25,7 +25,6 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
||||
container: HTMLElement;
|
||||
}> = [];
|
||||
protected listenerDisposers: IDisposer[] = [];
|
||||
protected buttonBuilders: ButtonBuilder[];
|
||||
|
||||
protected shown: boolean = false;
|
||||
|
||||
@ -35,10 +34,6 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
||||
protected currentOffer: IOfferFullView | null,
|
||||
protected readonly options: OfferPanelComponentOptions
|
||||
) {
|
||||
this.buttonBuilders = options.buttonBuilders ?? [
|
||||
OfferPanelComponent.buildCreateOrderButton,
|
||||
OfferPanelComponent.buildCloseButton
|
||||
];
|
||||
this.listenerDisposers.push(
|
||||
this.context.events.on(
|
||||
'offerFullViewToggle',
|
||||
@ -116,7 +111,11 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
||||
}
|
||||
|
||||
protected setupButtons() {
|
||||
for (const buttonBuilder of this.buttonBuilders) {
|
||||
const buttonBuilders = this.options.buttonBuilders ?? [
|
||||
OfferPanelComponent.buildCreateOrderButton,
|
||||
OfferPanelComponent.buildCloseButton
|
||||
];
|
||||
for (const buttonBuilder of buttonBuilders) {
|
||||
const container = document.createElement('li');
|
||||
this.buttonsContainer.appendChild(container);
|
||||
const button = buttonBuilder(
|
||||
|
@ -23,6 +23,7 @@ export interface ISearchResult {
|
||||
walkTime: IFormattedDuration;
|
||||
location: ILocation;
|
||||
icon?: string;
|
||||
phone?: string;
|
||||
};
|
||||
price: IFormattedPrice;
|
||||
}
|
||||
|
@ -182,3 +182,34 @@
|
||||
color: #3a3a3c;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-offer {
|
||||
clear: right;
|
||||
border-bottom: 0.5px solid rgba(60, 60, 67, 0.36);
|
||||
font-size: 15px;
|
||||
line-height: 34px;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.custom-offer > aside {
|
||||
float: right;
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.custom-offer > aside > button {
|
||||
height: 30px;
|
||||
width: 120px;
|
||||
background: #3a3a3c;
|
||||
border-radius: 7px;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
letter-spacing: -0.408px;
|
||||
color: #ffffff;
|
||||
border: transparent;
|
||||
cursor: pointer;
|
||||
margin: 1px 0;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
@ -25,7 +25,8 @@ export const CAFEE_CHAMOMILE_LUNGO_OFFER = {
|
||||
intervalValueSeconds: 120,
|
||||
formattedValue: '2 min'
|
||||
},
|
||||
icon: '../../assets/coffee.png'
|
||||
icon: '../../assets/coffee.png',
|
||||
phone: '12484345508'
|
||||
},
|
||||
price: {
|
||||
decimalValue: '37.00',
|
||||
|
Reference in New Issue
Block a user