Skip to content

Commit

Permalink
feat: amount input styles update (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lykhoyda authored Feb 9, 2024
1 parent 0f38d7c commit 6101005
Show file tree
Hide file tree
Showing 12 changed files with 726 additions and 346 deletions.
164 changes: 99 additions & 65 deletions packages/widget/src/components/amount-selector/amount-selector.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,151 @@
import type { Resource } from '@buildwithsygma/sygma-sdk-core';
import type { HTMLTemplateResult } from 'lit';
import { html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { map } from 'lit/directives/map.js';
import { when } from 'lit/directives/when.js';
import { classMap } from 'lit/directives/class-map.js';
import { BaseComponent } from '../base-component/base-component';
import type { DropdownOption } from '../common/dropdown/dropdown';
import { networkIconsMap } from '../../assets';
import { styles } from './styles';

@customElement('sygma-resource-selector')
export class AmountSelector extends BaseComponent {
static styles = styles;

@property({
type: Array,
hasChanged: (n, o) => n !== o
})
resources: Resource[] = [];

@property({
type: Boolean
})
@property({ type: Boolean })
disabled = false;

@property({
type: String
})
@property({ type: String })
accountBalance?: string;

@property({
type: String
})
@property({ type: String })
preselectedToken?: string;

@property({
type: Number
})
@property({ type: Number })
preselectedAmount?: number;

@property({
attribute: false
})
onResourceSelected?: (resource: Resource) => void;
@property({ attribute: false })
onResourceSelected: (resource: Resource, amount: number) => void = () => {};

@property({
attribute: false
})
onAmountChange?: (amount: number) => void;
@state() validationMessage: string | null = null;
@state() selectedResource: Resource | null = null;
@state() amount: string | null = null;

@query('.amountSelectorInput', true)
_input!: HTMLInputElement;
_useMaxBalance = (): void => {
if (Number.parseFloat(this.accountBalance!) === 0) {
this.validationMessage = 'Insufficient balance';
return;
}

useMaxBalance = (): void => {
this.preselectedAmount = Number.parseFloat(this.accountBalance!);
this._onInputAmountChange();
this.amount = this.accountBalance!;
};

_onInputAmountChange = (): void => {
const amount = Number.parseFloat(this._input.value);
this.onAmountChange?.(amount);
};
_onInputAmountChangeHandler = (event: Event): void => {
const { value } = event.target as HTMLInputElement;
if (!this._validateAmount(value)) return;

_onResourceSelected = (event: Event): void => {
const { value } = event.target as HTMLOptionElement;
const resource = this.resources.find((n) => String(n.resourceId) == value);
if (resource) {
this.onResourceSelected?.(resource);
this.amount = value;
if (this.selectedResource) {
this.onResourceSelected(this.selectedResource, Number.parseFloat(value));
}
};

renderBalance(): HTMLTemplateResult {
_onResourceSelectedHandler = ({ value }: DropdownOption<Resource>): void => {
this.selectedResource = value;
const amount = Number.parseFloat(this.amount!);

if (value) this.onResourceSelected(value, amount);
};

_validateAmount(amount: string): boolean {
const parsedAmount = Number.parseFloat(amount);
if (isNaN(parsedAmount) || parsedAmount <= 0) {
this.validationMessage = 'Amount must be greater than 0';
return false;
} else if (
this.accountBalance &&
parsedAmount > Number.parseFloat(this.accountBalance)
) {
this.validationMessage = 'Amount exceeds account balance';
return false;
} else {
this.validationMessage = null;
return true;
}
}

_renderBalance(): HTMLTemplateResult {
return html`
<section class="balanceContent">
<span>${`${Number.parseFloat(this.accountBalance!).toFixed(4)}`}</span>
<button class="maxButton" @click=${this.useMaxBalance}>Max</button>
<button class="maxButton" @click=${this._useMaxBalance}>Max</button>
</section>
`;
}

renderEntries(): Generator<unknown, void> | HTMLTemplateResult {
if (this.resources) {
return map(this.resources, (entry: Resource) => {
// TODO: render resource/token icon
return html`<option value=${entry.resourceId}>${entry.symbol}</option>`;
});
}
return html`<option selected value="">Token</option>`;
_renderAccountBalance(): HTMLTemplateResult {
return when(this.accountBalance, () => this._renderBalance());
}

_renderErrorMessages(): HTMLTemplateResult {
return when(
this.validationMessage,
() => html`<div class="validationMessage">${this.validationMessage}</div>`
);
}

_normalizeOptions(): DropdownOption<Resource>[] {
return when(this.resources, () =>
this.resources.map((entry) => ({
id: entry.resourceId,
name: entry.symbol!,
icon: networkIconsMap.default,
value: entry
}))
);
}

render(): HTMLTemplateResult {
const amountSelectorContainerClasses = classMap({
amountSelectorContainer: true,
hasError: !!this.validationMessage
});

return html`
<div class="amountSelectorContainer">
<div class=${amountSelectorContainerClasses}>
<section class="tokenBalanceSection">
<label class="amountSelectorLabel">Amount to transfer</label>
${when(this.accountBalance, () => this.renderBalance())}
${this._renderAccountBalance()}
</section>
<section class="amountSelectorSection">
<input
type="text"
class="amountSelectorInput"
placeholder="0.000"
@change=${this._onInputAmountChange}
value=${ifDefined(this.preselectedAmount)}
/>
<section class="selectorSection">
<select
@change=${this._onResourceSelected}
?disabled=${this.disabled}
class="selector amountSelectorInput"
>
<option value="-1">-</option>
${this.renderEntries()}
</select>
</section>
<div class="amountWrapper">
<input
type="number"
class="amountSelectorInput"
placeholder="0.000"
@change=${this._onInputAmountChangeHandler}
value=${this.amount || ifDefined(this.preselectedAmount)}
/>
<section class="selectorSection">
<dropdown-component
.selectedOption=${this.preselectedToken}
?disabled=${this.disabled}
.onOptionSelected=${this._onResourceSelectedHandler}
.options=${this._normalizeOptions()}
>
</section>
</div>
<div class="errorWrapper">
${this._renderErrorMessages()}
</div>
</section>
</div>
`;
Expand Down
61 changes: 49 additions & 12 deletions packages/widget/src/components/amount-selector/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,95 @@ import { css } from 'lit';
export const styles = css`
.amountSelectorContainer {
display: flex;
padding: 4px 12px;
padding: 0.75rem;
margin: 0.5rem 0;
align-items: center;
border-radius: 24px;
width: 314px; /* TODO: remove hardcoded values */
height: 116px; /* TODO: ↑ */
border-radius: 1.5rem;
flex-direction: column;
border: 0.0625rem solid rgb(228, 228, 231);
justify-content: center;
&.hasError {
border-color: var(--red-600);
}
}
.amountSelectorLabel {
display: flex;
width: 100%;
justify-content: flex-start;
color: var(--neutral-600);
font-size: 14px;
font-size: 0.875rem;
font-weight: 500;
line-height: 20px; /* 142.857% */
line-height: 1.25em;
}
.amountSelectorSection {
display: flex;
flex-direction: column;
width: 100%;
justify-content: space-between;
align-items: center;
margin: 0.5rem 0;
}
.amountWrapper {
width: 100%;
display: flex;
justify-content: space-between;
}
.errorWrapper {
width: 100%;
}
.validationMessage {
margin-top: 0.5rem;
color: var(--red-600);
}
.amountSelectorInput {
border: none;
outline: none;
color: var(--neutral-600);
font-size: 34px;
font-size: 2rem;
font-weight: 500;
line-height: 40px;
letter-spacing: -0.68px;
width: 137px; /* TODO: remove hardcoded values */
line-height: 1.25em;
letter-spacing: -0.0425rem;
width: 8.5625rem;
}
.tokenBalanceSection {
display: flex;
margin-top: 8px;
width: 100%;
}
.balanceContent {
display: flex;
width: 100%;
justify-content: flex-end;
gap: 6px;
gap: 0.375rem;
}
.maxButton {
cursor: pointer;
color: var(--blue-600);
border: none;
background: none;
font-weight: 500;
}
dropdown-component::part(dropdownWrapper) {
max-width: 8.1875rem;
width: 100%;
border-radius: 2.5rem;
background: var(--zinc-100);
min-height: 2.375rem;
padding: 0 0.5rem;
box-sizing: border-box;
}
dropdown-component::part(optionName) {
max-width: 4rem;
}
`;
Loading

0 comments on commit 6101005

Please sign in to comment.