Added new deposit/loan taking UX for touch based input
This commit is contained in:
parent
86b1cc4478
commit
d3b80f1e89
190
components.js
190
components.js
@ -177,17 +177,18 @@ customElements.define('sm-form', class extends HTMLElement {
|
||||
mode: 'open'
|
||||
}).append(smForm.content.cloneNode(true))
|
||||
|
||||
this.form = this.shadowRoot.querySelector('form')
|
||||
this.form = this.shadowRoot.querySelector('form');
|
||||
this.formElements
|
||||
this.requiredElements
|
||||
this.submitButton
|
||||
this.resetButton
|
||||
this.allRequiredValid = false
|
||||
this.allRequiredValid = false;
|
||||
|
||||
this.debounce = this.debounce.bind(this)
|
||||
this.handleInput = this.handleInput.bind(this)
|
||||
this._checkValidity = this._checkValidity.bind(this)
|
||||
this.handleKeydown = this.handleKeydown.bind(this)
|
||||
this.reset = this.reset.bind(this)
|
||||
this.elementsChanged = this.elementsChanged.bind(this)
|
||||
}
|
||||
debounce(callback, wait) {
|
||||
let timeoutId = null;
|
||||
@ -198,7 +199,7 @@ customElements.define('sm-form', class extends HTMLElement {
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
handleInput(e) {
|
||||
_checkValidity() {
|
||||
this.allRequiredValid = this.requiredElements.every(elem => elem.isValid)
|
||||
if (!this.submitButton) return;
|
||||
if (this.allRequiredValid) {
|
||||
@ -211,7 +212,13 @@ customElements.define('sm-form', class extends HTMLElement {
|
||||
handleKeydown(e) {
|
||||
if (e.key === 'Enter' && e.target.tagName !== 'SM-TEXTAREA') {
|
||||
if (this.allRequiredValid) {
|
||||
this.submitButton.click()
|
||||
if (this.submitButton && this.submitButton.tagName === 'SM-BUTTON') {
|
||||
this.submitButton.click()
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}
|
||||
else {
|
||||
this.requiredElements.find(elem => !elem.isValid).vibrate()
|
||||
@ -221,24 +228,24 @@ customElements.define('sm-form', class extends HTMLElement {
|
||||
reset() {
|
||||
this.formElements.forEach(elem => elem.reset())
|
||||
}
|
||||
elementsChanged() {
|
||||
this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')]
|
||||
this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required'));
|
||||
this.submitButton = this.querySelector('[variant="primary"], [type="submit"]');
|
||||
this.resetButton = this.querySelector('[type="reset"]');
|
||||
if (this.resetButton) {
|
||||
this.resetButton.addEventListener('click', this.reset);
|
||||
}
|
||||
this._checkValidity()
|
||||
}
|
||||
connectedCallback() {
|
||||
const slot = this.shadowRoot.querySelector('slot')
|
||||
slot.addEventListener('slotchange', e => {
|
||||
this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')]
|
||||
this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required'));
|
||||
// this.submitButton = e.target.assignedElements().find(elem => elem.getAttribute('variant') === 'primary' || elem.getAttribute('type') === 'submit');
|
||||
this.submitButton = this.querySelector('[variant="primary"], [type="submit"]');
|
||||
// this.resetButton = e.target.assignedElements().find(elem => elem.getAttribute('type') === 'reset');
|
||||
this.resetButton = this.querySelector('[type="reset"]');
|
||||
if (this.resetButton) {
|
||||
this.resetButton.addEventListener('click', this.reset);
|
||||
}
|
||||
})
|
||||
this.addEventListener('input', this.debounce(this.handleInput, 100));
|
||||
slot.addEventListener('slotchange', this.elementsChanged)
|
||||
this.addEventListener('input', this.debounce(this._checkValidity, 100));
|
||||
this.addEventListener('keydown', this.debounce(this.handleKeydown, 100));
|
||||
}
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('input', this.debounce(this.handleInput, 100));
|
||||
this.removeEventListener('input', this.debounce(this._checkValidity, 100));
|
||||
this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100));
|
||||
}
|
||||
})
|
||||
@ -2046,4 +2053,149 @@ customElements.define('strip-option', class extends HTMLElement {
|
||||
this.removeEventListener('click', this.fireEvent);
|
||||
this.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const slideButton = document.createElement('template')
|
||||
slideButton.innerHTML = `
|
||||
<style>
|
||||
*{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.slide-button{
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: rgba(var(--text-color), 0.1);
|
||||
border-radius: var(--button-border-radius, 0.3rem);
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slide-thumb{
|
||||
position: relative;
|
||||
display: flex;
|
||||
aspect-ratio: 1/1;
|
||||
cursor: grab;
|
||||
padding: 1rem;
|
||||
background-color: var(--accent-color, teal);
|
||||
border-radius: var(--button-border-radius, 0.3rem);
|
||||
touch-action: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.icon{
|
||||
height: var(--arrow-height, 1.5rem);
|
||||
width: var(--arrow-width, 1.5rem);
|
||||
fill: var(--arrow-fill, white);
|
||||
}
|
||||
.transition{
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.message{
|
||||
position: absolute;
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
:host([disabled]) .slide-thumb{
|
||||
pointer-events: none;
|
||||
background-color: rgba(var(--text-color), 0.5);
|
||||
}
|
||||
</style>
|
||||
<div class="slide-button">
|
||||
<div class="slide-thumb">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="message"><slot>Slide to confirm</slot></p>
|
||||
</div>
|
||||
`;
|
||||
class SlideButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({
|
||||
mode: 'open'
|
||||
}).append(slideButton.content.cloneNode(true));
|
||||
|
||||
this.handleTouchStart = this.handleTouchStart.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.fireEvent = this.fireEvent.bind(this);
|
||||
this.thumb = this.shadowRoot.querySelector('.slide-thumb');
|
||||
|
||||
this.startX = 0;
|
||||
this.threshold = 0;
|
||||
this.bound = 0;
|
||||
}
|
||||
get disabled() {
|
||||
return this.hasAttribute('disabled');
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
if (value) {
|
||||
this.setAttribute('disabled', '');
|
||||
} else {
|
||||
this.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.thumb.setAttribute('style', `transform: translateX(0)`);
|
||||
}
|
||||
|
||||
fireEvent() {
|
||||
this.dispatchEvent(new CustomEvent('confirmed', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
handleTouchStart(e) {
|
||||
this.thumb.classList.remove('transition')
|
||||
const thumbDimensions = this.thumb.getBoundingClientRect();
|
||||
const buttonDimensions = this.getBoundingClientRect();
|
||||
this.bound = buttonDimensions.width - thumbDimensions.width;
|
||||
this.startX = e.clientX;
|
||||
this.threshold = this.bound / 2;
|
||||
console.log(e.clientX, this.startX)
|
||||
this.thumb.setPointerCapture(e.pointerId);
|
||||
this.thumb.addEventListener('pointermove', this.handleTouchMove);
|
||||
this.thumb.addEventListener('pointerup', this.handleTouchEnd);
|
||||
}
|
||||
handleTouchMove(e) {
|
||||
requestAnimationFrame(() => {
|
||||
this.thumb.setAttribute('style', `transform: translateX(${Math.max(0, Math.min((this.bound), e.clientX - this.startX))}px)`);
|
||||
})
|
||||
}
|
||||
handleTouchEnd(e) {
|
||||
this.thumb.classList.add('transition');
|
||||
if (e.clientX > this.threshold) {
|
||||
this.fireEvent();
|
||||
this.thumb.setAttribute('style', `transform: translateX(${this.bound}px)`);
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
this.thumb.releasePointerCapture(e.pointerId);
|
||||
this.thumb.removeEventListener('pointermove', this.handleTouchMove);
|
||||
this.thumb.removeEventListener('pointerup', this.handleTouchEnd);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.thumb.addEventListener('pointerdown', this.handleTouchStart);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.thumb.removeEventListener('pointerdown', this.handleTouchStart);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('slide-button', SlideButton);
|
||||
@ -1255,7 +1255,8 @@ strip-option:last-of-type {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loader-button-wrapper sm-button {
|
||||
.loader-button-wrapper sm-button,
|
||||
.loader-button-wrapper slide-button {
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
-webkit-transition: -webkit-clip-path 0.3s;
|
||||
@ -1265,7 +1266,8 @@ strip-option:last-of-type {
|
||||
-webkit-clip-path: circle(100%);
|
||||
clip-path: circle(100%);
|
||||
}
|
||||
.loader-button-wrapper sm-button.clip {
|
||||
.loader-button-wrapper sm-button.clip,
|
||||
.loader-button-wrapper slide-button.clip {
|
||||
pointer-events: none;
|
||||
-webkit-clip-path: circle(0);
|
||||
clip-path: circle(0);
|
||||
|
||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1069,7 +1069,8 @@ strip-option {
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
sm-button {
|
||||
sm-button,
|
||||
slide-button {
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
transition: clip-path 0.3s;
|
||||
|
||||
137
index.html
137
index.html
@ -531,7 +531,7 @@
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<sm-form>
|
||||
<sm-form id="deposit_form">
|
||||
<div class="flex space-between align-center">
|
||||
<div id="deposit__icon" class="grid gap-0-5">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
|
||||
@ -552,9 +552,7 @@
|
||||
<sm-input id="get_deposit_amount" type="number" min="1" step="0.01"
|
||||
error-text="Amount should be at least ₹1" placeholder="₹Amount" required>
|
||||
</sm-input>
|
||||
<div id="deposit_button_wrapper" class="loader-button-wrapper">
|
||||
<sm-button id="deposit_button" variant="primary" disabled onclick="initDeposit()">Deposit</sm-button>
|
||||
</div>
|
||||
<div id="deposit_button_wrapper" class="loader-button-wrapper"></div>
|
||||
</sm-form>
|
||||
</article>
|
||||
<article id="loan" class="page hide-completely page-layout">
|
||||
@ -565,7 +563,7 @@
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<sm-form>
|
||||
<sm-form id="loan_form">
|
||||
<div class="flex space-between align-center">
|
||||
<div id="loan__icon" class="grid gap-0-5">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
@ -584,9 +582,7 @@
|
||||
<sm-input id="get_loan_amount" type="number" min="1" step="0.01" error-text="Amount should be at least ₹1"
|
||||
placeholder="₹Amount" required>
|
||||
</sm-input>
|
||||
<div id="loan_button_wrapper" class="loader-button-wrapper">
|
||||
<sm-button id="loan_button" variant="primary" disabled onclick="initLoan()">Request loan</sm-button>
|
||||
</div>
|
||||
<div id="loan_button_wrapper" class="loader-button-wrapper"></div>
|
||||
</sm-form>
|
||||
</article>
|
||||
<article id="result" class="page hide-completely page-layout">
|
||||
@ -1767,17 +1763,34 @@
|
||||
})
|
||||
|
||||
getRef('get_deposit_amount').addEventListener('input', e => {
|
||||
if (e.target.value.trim() !== '') {
|
||||
getRef('deposit_button').textContent = `Deposit ${parseFloat(e.target.value).toLocaleString('en-IN', { currency: 'INR', style: 'currency' })}`
|
||||
let message = ''
|
||||
if (getRef(`deposit_button_wrapper`).children[0].tagName === 'SM-BUTTON') {
|
||||
message = `Deposit`
|
||||
} else {
|
||||
getRef('deposit_button').textContent = `Deposit`
|
||||
message = `Slide to deposit`
|
||||
}
|
||||
if (e.target.value.trim() !== '') {
|
||||
getRef(`deposit_button_wrapper`).children[0].textContent = `${message} ${utils.formatAmount(parseFloat(e.target.value))}`
|
||||
} else {
|
||||
getRef(`deposit_button_wrapper`).children[0].textContent = message
|
||||
}
|
||||
})
|
||||
getRef('get_loan_amount').addEventListener('input', e => {
|
||||
let message = ''
|
||||
if (e.target.value.trim() !== '') {
|
||||
getRef('loan_button').textContent = `Request loan of ${parseFloat(e.target.value).toLocaleString('en-IN', { currency: 'INR', style: 'currency' })}`
|
||||
if (getRef(`loan_button_wrapper`).children[0].tagName === 'SM-BUTTON') {
|
||||
message = `Request loan of`
|
||||
} else {
|
||||
message = `Slide to request loan of`
|
||||
}
|
||||
getRef(`loan_button_wrapper`).children[0].textContent = `${message} ${utils.formatAmount(parseFloat(e.target.value))}`
|
||||
} else {
|
||||
getRef('loan_button').textContent = `Request loan`
|
||||
if (getRef(`loan_button_wrapper`).children[0].tagName === 'SM-BUTTON') {
|
||||
message = `Request loan`
|
||||
} else {
|
||||
message = `Slide to request loan`
|
||||
}
|
||||
getRef(`loan_button_wrapper`).children[0].textContent = message
|
||||
}
|
||||
})
|
||||
|
||||
@ -1803,40 +1816,77 @@
|
||||
getRef(`get_${type}_amount`).value = ''
|
||||
}
|
||||
|
||||
async function initDeposit() {
|
||||
// Sets UX according to supported pointing device
|
||||
const checkTouchSupport = (mql) => {
|
||||
if (mql.matches) {
|
||||
getRef('deposit_button_wrapper').innerHTML = `<sm-button id="deposit_button" variant="primary" onclick="confirmDeposit()" disabled>Deposit</sm-button>`
|
||||
getRef('loan_button_wrapper').innerHTML = `<sm-button id="loan_button" variant="primary" onclick="confirmLoan()" disabled>Request loan</sm-button>`
|
||||
|
||||
getRef('deposit_form').addEventListener('submit', confirmDeposit)
|
||||
getRef('loan_form').addEventListener('submit', confirmLoan)
|
||||
|
||||
getRef('deposit_button_wrapper').removeEventListener('confirmed', initDeposit)
|
||||
getRef('loan_button_wrapper').removeEventListener('confirmed', initLoan)
|
||||
} else {
|
||||
// touch supported
|
||||
getRef('deposit_button_wrapper').innerHTML = `<slide-button id="deposit_slide" type="submit" disabled>Slide to deposit</slide-button>`
|
||||
getRef('loan_button_wrapper').innerHTML = `<slide-button id="loan_slide" type="submit" disabled>Slide to request loan</slide-button>`
|
||||
|
||||
getRef('deposit_button_wrapper').addEventListener('confirmed', initDeposit)
|
||||
getRef('loan_button_wrapper').addEventListener('confirmed', initLoan)
|
||||
|
||||
getRef('deposit_form').removeEventListener('submit', confirmDeposit)
|
||||
getRef('loan_form').removeEventListener('submit', confirmLoan)
|
||||
}
|
||||
getRef('deposit_form').elementsChanged()
|
||||
getRef('loan_form').elementsChanged()
|
||||
}
|
||||
window.matchMedia("(any-hover: hover)").addEventListener('change', checkTouchSupport);
|
||||
|
||||
async function confirmDeposit() {
|
||||
const amount = parseFloat(getRef('get_deposit_amount').value)
|
||||
const confirm = await getConfirmation('Continue?', `Confirm deposit of ${amount.toLocaleString('en-IN', { currency: 'INR', style: 'currency' })}?`, 'Cancel', 'Confirm')
|
||||
const confirm = await getConfirmation('Continue?', `Confirm deposit of ${utils.formatAmount(amount)}?`, 'Cancel', 'Confirm')
|
||||
if (confirm) {
|
||||
showProcess('deposit')
|
||||
bank_app.makeDeposit(amount)
|
||||
.then(() => {
|
||||
window.location.hash = `#/result?type=deposit&amount=${amount}&status=pending`
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.hash = `#/result?type=deposit&amount=${amount}&status=failed&reason=${err}`
|
||||
})
|
||||
.finally(() => {
|
||||
hideProcess('deposit')
|
||||
})
|
||||
initDeposit()
|
||||
}
|
||||
}
|
||||
async function initLoan() {
|
||||
|
||||
function initDeposit() {
|
||||
const amount = parseFloat(getRef('get_deposit_amount').value)
|
||||
showProcess('deposit')
|
||||
bank_app.makeDeposit(amount)
|
||||
.then(() => {
|
||||
window.location.hash = `#/result?type=deposit&amount=${amount}&status=pending`
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.hash = `#/result?type=deposit&amount=${amount}&status=failed&reason=${err}`
|
||||
})
|
||||
.finally(() => {
|
||||
hideProcess('deposit')
|
||||
})
|
||||
}
|
||||
|
||||
async function confirmLoan(params) {
|
||||
const amount = parseFloat(getRef('get_loan_amount').value)
|
||||
const confirm = await getConfirmation('Continue?', `Confirm loan of ${amount.toLocaleString('en-IN', { currency: 'INR', style: 'currency' })}?`, 'Cancel', 'Confirm')
|
||||
const confirm = await getConfirmation('Continue?', `Confirm loan of ${utils.formatAmount(amount)}?`, 'Cancel', 'Confirm')
|
||||
if (confirm) {
|
||||
showProcess('loan')
|
||||
bank_app.requestLoan(amount)
|
||||
.then(() => {
|
||||
window.location.hash = `#/result?type=loan&amount=${amount}&status=pending`
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.hash = `#/result?type=loan&amount=${amount}&status=failed&reason=${err}`
|
||||
})
|
||||
.finally(() => {
|
||||
hideProcess('loan')
|
||||
})
|
||||
initLoan()
|
||||
}
|
||||
}
|
||||
function initLoan() {
|
||||
const amount = parseFloat(getRef('get_loan_amount').value)
|
||||
showProcess('loan')
|
||||
bank_app.requestLoan(amount)
|
||||
.then(() => {
|
||||
window.location.hash = `#/result?type=loan&amount=${amount}&status=pending`
|
||||
})
|
||||
.catch(err => {
|
||||
window.location.hash = `#/result?type=loan&amount=${amount}&status=failed&reason=${err}`
|
||||
})
|
||||
.finally(() => {
|
||||
hideProcess('loan')
|
||||
})
|
||||
}
|
||||
|
||||
function showProcess(type) {
|
||||
getRef(`${type}_button_wrapper`).children[0].classList.add('clip')
|
||||
@ -1844,7 +1894,7 @@
|
||||
}
|
||||
function hideProcess(type) {
|
||||
getRef(`${type}_button_wrapper`).children[0].classList.remove('clip')
|
||||
getRef(`${type}_button_wrapper`).children[1]?.remove()
|
||||
getRef(`${type}_button_wrapper`).querySelector('sm-spinner')?.remove()
|
||||
}
|
||||
|
||||
function toggleUserSection() {
|
||||
@ -1925,20 +1975,20 @@
|
||||
|
||||
function checkIfAllowed(type) {
|
||||
const { isPending, uid } = utils.getLastTransaction()
|
||||
getRef(`${type}_button`).nextElementSibling?.remove()
|
||||
getRef(`${type}_button_wrapper`).querySelector('strong')?.remove()
|
||||
// check if there is a last transaction
|
||||
if (uid) {
|
||||
const { amount, rtype } = getRequestDetails(uid)
|
||||
if (isPending && (rtype === 'openDeposit' || rtype === 'openLoan')) {
|
||||
const action = rtype === 'openDeposit' ? 'Deposit' : 'Loan'
|
||||
getRef(`${type}_button`).classList.add('hide-completely')
|
||||
getRef(`${type}_button_wrapper`).firstElementChild.classList.add('hide-completely')
|
||||
getRef(`${type}_button`).after(createElement('strong', {
|
||||
className: 'warning',
|
||||
textContent: `${action} in process. You can't ${type === 'deposit' ? 'deposit' : 'request loan'} until that's completed.`,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
getRef(`${type}_button`).classList.remove('hide-completely')
|
||||
getRef(`${type}_button_wrapper`).firstElementChild.classList.remove('hide-completely')
|
||||
}
|
||||
}
|
||||
let requestResponsePairs
|
||||
@ -1996,6 +2046,7 @@
|
||||
document.querySelectorAll('.admin-option').forEach(option => option.classList.add('hide-completely'))
|
||||
document.querySelectorAll('.user-option').forEach(option => option.classList.remove('hide-completely'))
|
||||
}
|
||||
checkTouchSupport(window.matchMedia("(any-hover: hover)"))
|
||||
if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) {
|
||||
window.location.hash = ''
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user