diff --git a/public/css/offers.css b/public/css/offers.css index 17ac4b5..6a4e706 100644 --- a/public/css/offers.css +++ b/public/css/offers.css @@ -11,11 +11,11 @@ max-width: 30%; } - #premium-selector-area { + .premium-selector-area { width: 80px; } - #amount-area-content { + .amount-area-content { width: 50%; } @@ -50,11 +50,11 @@ width: 50px; } - #premium-selector-area { + .premium-selector-area { width: 100px; } - #amount-area-content { + .amount-area-content { width: 33%; } @@ -221,10 +221,8 @@ font-size: 0.9em; } -#create-offer-controls { +.create-offer-controls { text-align: center; - overflow-y: auto; - max-height: 800px; padding: 20px; } @@ -243,16 +241,16 @@ margin-top: 0; } -#close-offer-controls-area { +.close-offer-controls-area { display: flex; justify-content: end; } -#premium-area > * { +.premium-area > * { display: block; } -#premium-content-area { +.premium-content-area { width: 80%; height: 50px; align-items: center; @@ -261,19 +259,19 @@ display: flex; } -#premium-selector-area { +.premium-selector-area { margin-left: auto; margin-right: 5%; display: flex; } -#premium-value { +.premium-value { border: 1px solid gray; width: 50%; align-content: center; } -#premium-buttons-container { +.premium-buttons-container { width: 50%; } @@ -289,22 +287,22 @@ background: #fff8ce; } -#button-increase-premium { +.button-increase-premium { border-top-right-radius: 10px; } -#button-decrease-premium { +.button-decrease-premium { border-bottom-right-radius: 10px; } -#premium-price-display-area { +.premium-price-display-area { margin-left: 0; margin-right: auto; font-size: 1em; text-align: start; } -#premium-price-display-area > * { +.premium-price-display-area > * { margin-top: 0; margin-bottom: 0; } @@ -314,7 +312,7 @@ font-size: 0.8em; } -#amount-area-content { +.amount-area-content { margin-left: auto; margin-right: auto; } @@ -377,7 +375,7 @@ width: 2em; } -#submit-button-area { +.submit-button-area { margin-top: 1em; margin-bottom: 1em; } @@ -386,7 +384,7 @@ width: 33%; } -#close-offer { +.close-offer { margin-left: auto; margin-right: auto; } diff --git a/public/css/seca.css b/public/css/seca.css index 1cc3034..35b6217 100644 --- a/public/css/seca.css +++ b/public/css/seca.css @@ -141,6 +141,8 @@ h1 { padding: 10px; width: fit-content; max-width: 95%; + max-height: 90vh; + overflow: auto; margin: 20px auto; } diff --git a/src/constants.js b/src/constants.js index 7af448b..a454e1b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,14 +2,24 @@ const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30; const DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS = 60 * 60 * 24 * 30; const DEFAULT_REDIRECT_DELAY = 3 * 1000; // 3seconds times milliseconds; +const API_ROOT = '/api'; const API_PATHS = { - createProfile: '/createProfile', + offer: API_ROOT + '/offer', + loginNostrChallenge: API_ROOT + '/login/nostr-challenge', + loginNostrVerify: API_ROOT + '/login/nostr-verify', + signupNostrChallenge: API_ROOT + '/signup/nostr-challenge', + signupNostrVerify: API_ROOT + '/signup/nostr-verify', +}; + +const WEB_PATHS = { home: '/home', + createProfile: '/createProfile', }; module.exports = { DEFAULT_SESSION_DURATION_SECONDS, DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS, API_PATHS, + WEB_PATHS, DEFAULT_REDIRECT_DELAY, }; diff --git a/src/front/components/AmountInput.js b/src/front/components/AmountInput.js new file mode 100644 index 0000000..9badd2a --- /dev/null +++ b/src/front/components/AmountInput.js @@ -0,0 +1,113 @@ +const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces'); + +class AmountInput { + constructor({ parentElement, id }) { + this.element = null; + this.parentElement = parentElement; + this.id = id; + + this.eurInput = null; + this.btcInput = null; + } + + render() { + const amountArea = document.createElement('div'); + amountArea.id = this.id; + amountArea.className = 'amount-area-content'; + + const eurAmount = document.createElement('div'); + eurAmount.className = 'money-amount-input-area'; + + this.eurInput = document.createElement('input'); + this.eurInput.type = 'text'; + this.eurInput.className = 'money-input input-money-amount'; + this.eurInput.value = '100'; + this.eurInput.required = true; + + const eurSymbol = document.createElement('div'); + eurSymbol.className = 'curr-symbol'; + + const eurCharacter = document.createElement('span'); + eurCharacter.className = 'curr-character'; + eurCharacter.textContent = '€'; + + eurSymbol.appendChild(eurCharacter); + eurAmount.appendChild(this.eurInput); + eurAmount.appendChild(eurSymbol); + + const btcAmount = document.createElement('div'); + btcAmount.className = 'money-amount-input-area'; + + this.btcInput = document.createElement('input'); + this.btcInput.type = 'text'; + this.btcInput.className = 'money-input input-money-amount'; + this.btcInput.disabled = true; + + const satsSymbol = document.createElement('div'); + satsSymbol.className = 'curr-symbol'; + + const satsCharacter = document.createElement('span'); + satsCharacter.className = 'curr-character'; + satsCharacter.textContent = 'SAT'; + + satsSymbol.appendChild(satsCharacter); + btcAmount.appendChild(this.btcInput); + btcAmount.appendChild(satsSymbol); + + amountArea.appendChild(eurAmount); + amountArea.appendChild(btcAmount); + + this.eurInput.addEventListener('blur', () => { + this.validateAndFormatEurAmountInput(); + this.updateBtcInput(); + }); + + this.eurInput.addEventListener('input', () => { + this.eurInput.value = this.eurInput.value.replace(/[^0-9]/g, ''); + this.updateBtcInput(); + }); + + this.updateBtcInput(); + + this.element = amountArea; + this.parentElement.appendChild(this.element); + } + + get intEurAmount() { + const eurAmountFieldValue = this.eurInput.value; + const regularExpression = /([\d\s]+)/; + const matchResult = eurAmountFieldValue.match(regularExpression); + + if (!matchResult) { + return null; + } + + const numberString = matchResult[1]; + const cleanInputNumber = parseInt(numberString.replace(/\s/gi, '')); + + return cleanInputNumber; + } + + validateAndFormatEurAmountInput() { + const cleanInputNumber = this.intEurAmount; + this.eurInput.classList.remove('input-is-valid', 'input-is-invalid'); + if (cleanInputNumber) { + this.eurInput.value = formatNumberWithSpaces(cleanInputNumber); + this.eurInput.classList.add('input-is-valid'); + return; + } + + this.eurInput.classList.add('input-is-invalid'); + } + + updateBtcInput() { + const eurToSatRate = 1021; + const cleanEurAmount = this.intEurAmount; + + const satsAmount = cleanEurAmount * eurToSatRate; + const formattedSatsAmount = formatNumberWithSpaces(satsAmount); + this.btcInput.value = formattedSatsAmount; + } +} + +module.exports = AmountInput; diff --git a/src/front/components/BigNotesCheckbox.js b/src/front/components/BigNotesCheckbox.js new file mode 100644 index 0000000..6361a72 --- /dev/null +++ b/src/front/components/BigNotesCheckbox.js @@ -0,0 +1,35 @@ +class BigNotesCheckbox { + constructor({ parentElement }) { + this.bigNotesContainer = null; + this.bigNotesCheckboxElement = null; + this.parentElement = parentElement; + } + + render() { + const container = document.createElement('div'); + container.className = 'checkbox-row'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = 'large-bills'; + checkbox.id = 'large-bills-checkbox'; + + const label = document.createElement('label'); + label.htmlFor = 'large-bills-checkbox'; + label.textContent = 'Se pueden usar billetes grandes (100€, 200€, 500€)'; + + container.appendChild(checkbox); + container.appendChild(label); + + this.bigNotesContainer = container; + this.bigNotesCheckboxElement = checkbox; + + this.parentElement.append(this.bigNotesContainer); + } + + get areBigNotesAccepted() { + return this.bigNotesCheckboxElement.checked; + } +} + +module.exports = BigNotesCheckbox; diff --git a/src/front/components/BitcoinMethodCheckboxes.js b/src/front/components/BitcoinMethodCheckboxes.js new file mode 100644 index 0000000..27cb3f8 --- /dev/null +++ b/src/front/components/BitcoinMethodCheckboxes.js @@ -0,0 +1,74 @@ +class BitcoinMethodCheckboxes { + constructor({ parentElement }) { + this.onchainContainer = null; + this.onchainCheckboxElement = null; + this.lightningContainer = null; + this.lightningCheckboxElement = null; + this.parentElement = parentElement; + } + + render() { + this.onchainContainer = this.buildCheckbox({ + id: 'onchain', + label: 'Onchain', + }); + this.onchainCheckboxElement = this.onchainContainer.querySelector('input'); + + this.lightningContainer = this.buildCheckbox({ + id: 'lightning', + label: 'Lightning', + }); + this.lightningCheckboxElement = + this.lightningContainer.querySelector('input'); + + for (const btcMethodCheckbox of [ + this.onchainCheckboxElement, + this.lightningCheckboxElement, + ]) { + btcMethodCheckbox.addEventListener('click', () => { + this.validateBitcoinMethodCheckboxes(btcMethodCheckbox); + }); + } + + this.parentElement.appendChild(this.onchainContainer); + this.parentElement.appendChild(this.lightningContainer); + } + + buildCheckbox({ label }) { + const checkboxContainer = document.createElement('div'); + checkboxContainer.className = 'checkbox-row'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + + const labelElement = document.createElement('label'); + labelElement.htmlFor = checkbox.id; + labelElement.textContent = label; + + checkboxContainer.appendChild(checkbox); + checkboxContainer.appendChild(labelElement); + + return checkboxContainer; + } + + validateBitcoinMethodCheckboxes(clickedCheckbox) { + let checkedCount = [ + this.onchainCheckboxElement, + this.lightningCheckboxElement, + ].filter((cb) => cb.checked).length; + if (checkedCount === 0) { + clickedCheckbox.checked = true; + } + } + + get isOnchainAccepted() { + return this.onchainCheckboxElement.checked; + } + + get isLightningAccepted() { + return this.lightningCheckboxElement.checked; + } +} + +module.exports = BitcoinMethodCheckboxes; diff --git a/src/front/components/CloseModalButton.js b/src/front/components/CloseModalButton.js new file mode 100644 index 0000000..b857ea5 --- /dev/null +++ b/src/front/components/CloseModalButton.js @@ -0,0 +1,21 @@ +class CloseModalButton { + constructor({ parentElement, id, onClickCallback }) { + this.element = null; + this.parentElement = parentElement; + this.id = id; + this.onClickCallback = onClickCallback; + } + + render() { + const closeButton = document.createElement('button'); + closeButton.className = 'close-offer button-secondary button-medium'; + closeButton.textContent = 'Volver'; + + closeButton.addEventListener('click', this.onClickCallback); + + this.element = closeButton; + this.parentElement.appendChild(this.element); + } +} + +module.exports = CloseModalButton; diff --git a/src/front/components/CreateOfferModal.js b/src/front/components/CreateOfferModal.js new file mode 100644 index 0000000..b5301d1 --- /dev/null +++ b/src/front/components/CreateOfferModal.js @@ -0,0 +1,239 @@ +const PublishOfferButton = require('./PublishOfferButton'); +const BuyOrSellButtonGroup = require('./BuyOrSellButtonGroup'); +const PremiumSelector = require('./PremiumSelector'); +const PriceDisplay = require('./PriceDisplay'); +const AmountInput = require('./AmountInput'); +const PlaceInput = require('./PlaceInput'); +const TimeInput = require('./TimeInput'); +const BitcoinMethodCheckboxes = require('./BitcoinMethodCheckboxes'); +const TrustCheckboxes = require('./TrustCheckboxes'); +const BigNotesCheckbox = require('./BigNotesCheckbox'); +const CloseModalButton = require('./CloseModalButton'); + +class CreateOfferModal { + constructor({ parentElement, onCreationCallback, offerService }) { + this.element = null; + this.parentElement = parentElement; + this.onCreationCallback = onCreationCallback; + this.offerService = offerService; + + this.publishOfferButton = null; + this.buyOrSellButtonGroup = null; + this.premiumSelector = null; + this.amountInput = null; + this.placeInput = null; + this.timeInput = null; + this.btcMethodCheckboxes = null; + this.trustCheckboxes = null; + this.bigNotesCheckbox = null; + } + + render() { + const modalRoot = document.createElement('div'); + this.element = modalRoot; + modalRoot.className = 'full-screen-modal-background'; + + const modal = document.createElement('div'); + modal.className = 'full-screen-modal'; + + const controls = document.createElement('div'); + controls.className = 'create-offer-controls'; + + const title = document.createElement('h2'); + title.textContent = 'Añade los detalles de tu oferta'; + controls.appendChild(title); + + const buyOrSellDiv = document.createElement('div'); + buyOrSellDiv.className = 'create-offer-step'; + this.buyOrSellButtonGroup = new BuyOrSellButtonGroup({ + parentElement: buyOrSellDiv, + }); + this.buyOrSellButtonGroup.render(); + controls.appendChild(buyOrSellDiv); + + const premiumDiv = document.createElement('div'); + premiumDiv.classList = 'premium-area'; + premiumDiv.className = 'create-offer-step'; + const premiumHeading = document.createElement('h3'); + premiumHeading.textContent = 'Premium'; + premiumDiv.appendChild(premiumHeading); + const premiumContentDiv = document.createElement('div'); + premiumContentDiv.classList = 'premium-content-area'; + premiumDiv.appendChild(premiumContentDiv); + controls.appendChild(premiumDiv); + + const createOfferEventBus = new EventTarget(); + + const mockPriceProvidingCallback = () => { + return Math.floor(Math.random() * (95000 - 70000 + 1) + 70000); + }; + + this.premiumSelector = new PremiumSelector({ + parentElement: premiumContentDiv, + eventSink: createOfferEventBus, + }); + this.premiumSelector.render(); + + const priceDisplay = new PriceDisplay({ + parentElement: premiumContentDiv, + id: 'premium-price-display-area', + premiumProvidingCallback: () => { + return this.premiumSelector.getPremium(); + }, + priceProvidingCallback: mockPriceProvidingCallback, + }); + priceDisplay.render(); + createOfferEventBus.addEventListener('premium-changed', () => { + priceDisplay.updatePrices(); + }); + + const amountDiv = document.createElement('div'); + amountDiv.className = 'create-offer-step'; + const amountHeading = document.createElement('h3'); + amountHeading.textContent = '¿Cuánto?'; + amountDiv.appendChild(amountHeading); + controls.appendChild(amountDiv); + + this.amountInput = new AmountInput({ + parentElement: amountDiv, + }); + + this.amountInput.render(); + + const placeTimeDiv = document.createElement('div'); + placeTimeDiv.className = 'create-offer-step'; + const placeTimeHeading = document.createElement('h3'); + placeTimeHeading.textContent = '¿Dónde y cuándo?'; + placeTimeDiv.appendChild(placeTimeHeading); + const placeTimeContentDiv = document.createElement('div'); + placeTimeDiv.appendChild(placeTimeContentDiv); + controls.appendChild(placeTimeDiv); + + this.placeInput = new PlaceInput({ + parentElement: placeTimeContentDiv, + }); + + this.placeInput.render(); + + this.timeInput = new TimeInput({ + parentElement: placeTimeContentDiv, + }); + + this.timeInput.render(); + + const bitcoinMethodsDiv = document.createElement('div'); + bitcoinMethodsDiv.className = 'create-offer-step'; + const bitcoinMethodsHeading = document.createElement('h3'); + bitcoinMethodsHeading.textContent = '¿Cómo se mueve el Bitcoin?'; + bitcoinMethodsDiv.appendChild(bitcoinMethodsHeading); + const bitcoinMethodsContentDiv = document.createElement('div'); + bitcoinMethodsDiv.appendChild(bitcoinMethodsContentDiv); + controls.appendChild(bitcoinMethodsDiv); + + this.btcMethodCheckboxes = new BitcoinMethodCheckboxes({ + parentElement: bitcoinMethodsContentDiv, + }); + + this.btcMethodCheckboxes.render(); + + const trustDiv = document.createElement('div'); + trustDiv.className = 'create-offer-step'; + const trustHeading = document.createElement('h3'); + trustHeading.textContent = '¿Quién puede ver la oferta?'; + trustDiv.appendChild(trustHeading); + const trustContentDiv = document.createElement('div'); + trustDiv.appendChild(trustContentDiv); + controls.appendChild(trustDiv); + + this.trustCheckboxes = new TrustCheckboxes({ + parentElement: trustContentDiv, + }); + + this.trustCheckboxes.render(); + + const otherDiv = document.createElement('div'); + otherDiv.className = 'create-offer-step'; + const otherHeading = document.createElement('h3'); + otherHeading.textContent = 'Extras'; + otherDiv.appendChild(otherHeading); + controls.appendChild(otherDiv); + + this.bigNotesCheckbox = new BigNotesCheckbox({ + parentElement: otherDiv, + }); + + this.bigNotesCheckbox.render(); + //Continue moving components up here + + const submitButtonArea = document.createElement('div'); + submitButtonArea.classList.add('submit-button-area'); + this.publishOfferButton = new PublishOfferButton({ + parentElement: submitButtonArea, + id: 'button-submit-offer', + onClickCallback: async () => { + await this.createOffer(); + await this.onCreationCallback(); + this.toggle(); + }, + }); + this.publishOfferButton.render(); + + const closeButtonArea = document.createElement('div'); + closeButtonArea.className = 'close-offer-controls-area'; + const closeButton = new CloseModalButton({ + parentElement: closeButtonArea, + onClickCallback: () => { + this.toggle(); + }, + }); + + closeButton.render(); + + controls.appendChild(submitButtonArea); + controls.appendChild(closeButtonArea); + + modal.appendChild(controls); + modalRoot.appendChild(modal); + + this.parentElement.appendChild(this.element); + } + + toggle() { + this.element.classList.toggle('shown'); + } + + async createOffer() { + const wants = this.buyOrSellButtonGroup.wants(); + + const premium = this.premiumSelector.getPremium(); + const trade_amount_eur = this.amountInput.intEurAmount; + const location_details = this.placeInput.inputText; + const time_availability_details = this.timeInput.inputText; + const is_onchain_accepted = this.btcMethodCheckboxes.isOnchainAccepted; + const is_lightning_accepted = this.btcMethodCheckboxes.isLightningAccepted; + const show_offer_to_trusted = this.trustCheckboxes.showOfferToTrusted; + const show_offer_to_trusted_trusted = + this.trustCheckboxes.showOfferToTrustedTrusted; + const show_offer_to_all_members = + this.trustCheckboxes.showOfferToAllMembers; + const are_big_notes_accepted = this.bigNotesCheckbox.areBigNotesAccepted; + + const offerDetails = { + wants, + premium, + trade_amount_eur, + location_details, + time_availability_details, + is_onchain_accepted, + is_lightning_accepted, + show_offer_to_trusted, + show_offer_to_trusted_trusted, + show_offer_to_all_members, + are_big_notes_accepted, + }; + + await this.offerService.createOffer(offerDetails); + } +} + +module.exports = CreateOfferModal; diff --git a/src/front/components/OfferCard.js b/src/front/components/OfferCard.js new file mode 100644 index 0000000..5c11452 --- /dev/null +++ b/src/front/components/OfferCard.js @@ -0,0 +1,374 @@ +class OfferCard { + constructor({ offerData, deleteButtonCallback }) { + this.uuid = offerData.uuid; + this.public_key = offerData.public_key; + this.wants = offerData.wants; + this.premium = offerData.premium; + this.trade_amount_eur = offerData.trade_amount_eur; + this.location_details = offerData.location_details; + this.time_availability_details = offerData.time_availability_details; + this.show_offer_to_trusted = offerData.show_offer_to_trusted; + this.show_offer_to_trusted_trusted = + offerData.show_offer_to_trusted_trusted; + this.show_offer_to_all_members = offerData.show_offer_to_all_members; + this.is_onchain_accepted = offerData.is_onchain_accepted; + this.is_lightning_accepted = offerData.is_lightning_accepted; + this.are_big_notes_accepted = offerData.are_big_notes_accepted; + this.created_at = offerData.created_at; + this.last_updated_at = offerData.last_updated_at; + + this.deleteButtonCallback = deleteButtonCallback; + } + + buildHTML() { + const offerCard = document.createElement('div'); + offerCard.classList.add('myoffer-card'); + offerCard.classList.add('shadowed-round-area'); + + const tradeDescDiv = document.createElement('div'); + tradeDescDiv.classList.add('trade-desc'); + + const youBuyText = document.createElement('p'); + youBuyText.classList.add('offer-card-content-title'); + youBuyText.innerText = 'Compras'; + tradeDescDiv.append(youBuyText); + + const youBuyData = document.createElement('p'); + youBuyData.classList.add('offer-card-content-data'); + if (this.wants === 'BTC') { + youBuyData.innerText = `${this.trade_amount_eur * 1021} sats`; + } + if (this.wants === 'EUR') { + youBuyData.innerText = `${this.trade_amount_eur} €`; + } + tradeDescDiv.append(youBuyData); + + const youSellText = document.createElement('p'); + youSellText.classList.add('offer-card-content-title'); + youSellText.innerText = 'Vendes'; + tradeDescDiv.append(youSellText); + + const youSellData = document.createElement('p'); + youSellData.classList.add('offer-card-content-data'); + if (this.wants === 'BTC') { + youSellData.innerText = `${this.trade_amount_eur} €`; + } + if (this.wants === 'EUR') { + youSellData.innerText = `${this.trade_amount_eur * 1021} sats`; + } + tradeDescDiv.append(youSellData); + + const premiumDescDiv = document.createElement('div'); + premiumDescDiv.classList.add('premium-desc'); + + const premiumTitle = document.createElement('p'); + premiumTitle.classList.add('offer-card-content-title'); + premiumTitle.innerText = 'Premium'; + premiumDescDiv.append(premiumTitle); + + const premiumData = document.createElement('p'); + premiumData.classList.add('offer-card-content-data'); + premiumData.innerText = `${this.premium * 100} %`; + premiumDescDiv.append(premiumData); + + const offerPriceTitle = document.createElement('p'); + offerPriceTitle.classList.add('offer-card-content-title'); + offerPriceTitle.innerText = 'Precio oferta'; + premiumDescDiv.append(offerPriceTitle); + + const offerPriceData = document.createElement('p'); + offerPriceData.classList.add('offer-card-content-data'); + offerPriceData.innerText = `90000 €/BTC`; + premiumDescDiv.append(offerPriceData); + + const marketPriceTitle = document.createElement('p'); + marketPriceTitle.classList.add('offer-card-content-title'); + marketPriceTitle.innerText = 'Precio mercado'; + premiumDescDiv.append(marketPriceTitle); + + const marketPriceData = document.createElement('p'); + marketPriceData.innerText = `88000 €/BTC`; + premiumDescDiv.append(marketPriceData); + + const whereDescDiv = document.createElement('div'); + whereDescDiv.classList.add('where-desc'); + + const whereDescTitle = document.createElement('p'); + whereDescTitle.classList.add('offer-card-content-title'); + whereDescTitle.innerText = 'Dónde'; + whereDescDiv.append(whereDescTitle); + + const whereDescData = document.createElement('p'); + whereDescData.classList.add('offer-long-text'); + whereDescData.innerText = `${this.location_details}`; + whereDescDiv.append(whereDescData); + + const whenDescDiv = document.createElement('div'); + whenDescDiv.classList.add('when-desc'); + + const whenDescTitle = document.createElement('p'); + whenDescTitle.classList.add('offer-card-content-title'); + whenDescTitle.innerText = 'Cúando'; + whenDescDiv.append(whenDescTitle); + + const whenDescData = document.createElement('p'); + whenDescData.classList.add('offer-long-text'); + whenDescData.innerText = `${this.time_availability_details}`; + whenDescDiv.append(whenDescData); + + const bitcoinMethodsDiv = document.createElement('div'); + bitcoinMethodsDiv.classList.add('bitcoin-methods-desc'); + + const bitcoinMethodsTitle = document.createElement('p'); + bitcoinMethodsTitle.classList.add('offer-card-content-title'); + bitcoinMethodsTitle.innerText = 'Protocolos Bitcoin aceptados'; + bitcoinMethodsDiv.append(bitcoinMethodsTitle); + + const onchainAcceptedContainer = document.createElement('div'); + onchainAcceptedContainer.classList.add('left-icon-checkboxed-field'); + if (this.is_onchain_accepted) { + const onchainIcon = document.createElement('img'); + onchainIcon.src = '/img/chains-lasecagold.svg'; + const onchainText = document.createElement('p'); + onchainText.innerText = 'Onchain'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); + } else { + const onchainIcon = document.createElement('img'); + onchainIcon.src = '/img/chains-gray.svg'; + const onchainText = document.createElement('p'); + onchainText.innerText = 'Onchain'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); + } + const lightningAcceptedContainer = document.createElement('div'); + + lightningAcceptedContainer.classList.add('left-icon-checkboxed-field'); + if (this.is_lightning_accepted) { + const lightningIcon = document.createElement('img'); + lightningIcon.src = '/img/bolt-lightning-lasecagold.svg'; + const lightningText = document.createElement('p'); + lightningText.innerText = 'Lightning'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + lightningAcceptedContainer.append( + lightningIcon, + lightningText, + checkIcon + ); + } else { + const lightningIcon = document.createElement('img'); + lightningIcon.src = '/img/bolt-lightning-gray.svg'; + const lightningText = document.createElement('p'); + lightningText.innerText = 'Lightning'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + lightningAcceptedContainer.append( + lightningIcon, + lightningText, + checkIcon + ); + } + + bitcoinMethodsDiv.append( + onchainAcceptedContainer, + lightningAcceptedContainer + ); + + const visibilityDiv = document.createElement('div'); + visibilityDiv.classList.add('visibility-desc'); + + const visibilityTitle = document.createElement('p'); + visibilityTitle.classList.add('offer-card-content-title'); + visibilityTitle.innerText = 'Visibilidad'; + visibilityDiv.append(visibilityTitle); + + const showOfferToTrustedContainer = document.createElement('div'); + showOfferToTrustedContainer.classList.add('right-icon-checkboxed-field'); + + if (this.show_offer_to_trusted) { + const showOfferToTrustedIcon = document.createElement('img'); + showOfferToTrustedIcon.src = '/img/user-lasecagold.svg'; + const showOfferToTrustedText = document.createElement('p'); + showOfferToTrustedText.innerText = 'Confiados'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + showOfferToTrustedContainer.append( + showOfferToTrustedIcon, + showOfferToTrustedText, + checkIcon + ); + } else { + const showOfferToTrustedIcon = document.createElement('img'); + showOfferToTrustedIcon.src = '/img/user-gray.svg'; + const showOfferToTrustedText = document.createElement('p'); + showOfferToTrustedText.innerText = 'Confiados'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + showOfferToTrustedContainer.append( + showOfferToTrustedIcon, + showOfferToTrustedText, + checkIcon + ); + } + + const showOfferToTrustedTrustedContainer = document.createElement('div'); + showOfferToTrustedTrustedContainer.classList.add( + 'right-icon-checkboxed-field' + ); + + if (this.show_offer_to_trusted_trusted) { + const showOfferToTrustedTrustedIcon = document.createElement('img'); + showOfferToTrustedTrustedIcon.src = '/img/user-group-lasecagold.svg'; + const showOfferToTrustedTrustedText = document.createElement('p'); + showOfferToTrustedTrustedText.innerText = 'Sus confiados'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + showOfferToTrustedTrustedContainer.append( + showOfferToTrustedTrustedIcon, + showOfferToTrustedTrustedText, + checkIcon + ); + } else { + const showOfferToTrustedTrustedIcon = document.createElement('img'); + showOfferToTrustedTrustedIcon.src = '/img/user-group-gray.svg'; + const showOfferToTrustedTrustedText = document.createElement('p'); + showOfferToTrustedTrustedText.innerText = 'Sus confiados'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + showOfferToTrustedTrustedContainer.append( + showOfferToTrustedTrustedIcon, + showOfferToTrustedTrustedText, + checkIcon + ); + } + + const showOfferToAllMembersContainer = document.createElement('div'); + showOfferToAllMembersContainer.classList.add('right-icon-checkboxed-field'); + + if (this.show_offer_to_all_members) { + const showOfferToAllMembersIcon = document.createElement('img'); + showOfferToAllMembersIcon.src = '/img/many-users-lasecagold.svg'; + const showOfferToAllMembersText = document.createElement('p'); + showOfferToAllMembersText.innerText = 'Todos'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + showOfferToAllMembersContainer.append( + showOfferToAllMembersIcon, + showOfferToAllMembersText, + checkIcon + ); + } else { + const showOfferToAllMembersIcon = document.createElement('img'); + showOfferToAllMembersIcon.src = '/img/many-users-gray.svg'; + const showOfferToAllMembersText = document.createElement('p'); + showOfferToAllMembersText.innerText = 'Todos'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + showOfferToAllMembersContainer.append( + showOfferToAllMembersIcon, + showOfferToAllMembersText, + checkIcon + ); + } + visibilityDiv.append( + showOfferToTrustedContainer, + showOfferToTrustedTrustedContainer, + showOfferToAllMembersContainer + ); + + const otherOfferFeaturesDiv = document.createElement('div'); + otherOfferFeaturesDiv.classList.add('other-desc'); + + const otherOfferFeaturesTitle = document.createElement('p'); + otherOfferFeaturesTitle.classList.add('offer-card-content-title'); + otherOfferFeaturesTitle.innerText = 'Otros'; + otherOfferFeaturesDiv.append(otherOfferFeaturesTitle); + + const areBigNotesAcceptedContainer = document.createElement('div'); + areBigNotesAcceptedContainer.classList.add('left-icon-checkboxed-field'); + + if (this.are_big_notes_accepted) { + const areBigNotesAcceptedIcon = document.createElement('img'); + areBigNotesAcceptedIcon.src = '/img/eur-bill-lasecagold.svg'; + const areBigNotesAcceptedText = document.createElement('p'); + areBigNotesAcceptedText.innerText = 'Billetes grandes'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-check-green.svg'; + + areBigNotesAcceptedContainer.append( + areBigNotesAcceptedIcon, + areBigNotesAcceptedText, + checkIcon + ); + } else { + const areBigNotesAcceptedIcon = document.createElement('img'); + areBigNotesAcceptedIcon.src = '/img/eur-bill-gray.svg'; + const areBigNotesAcceptedText = document.createElement('p'); + areBigNotesAcceptedText.innerText = 'Billetes grandes'; + const checkIcon = document.createElement('img'); + checkIcon.src = '/img/circle-xmark-gray.svg'; + + areBigNotesAcceptedContainer.append( + areBigNotesAcceptedIcon, + areBigNotesAcceptedText, + checkIcon + ); + } + + otherOfferFeaturesDiv.append(areBigNotesAcceptedContainer); + + const actionButtonsArea = document.createElement('p'); + actionButtonsArea.classList.add('offer-action-buttons-area'); + + const editActionArea = document.createElement('div'); + editActionArea.classList.add('offer-action-area'); + editActionArea.classList.add('subtle-box'); + const editActionIcon = document.createElement('img'); + editActionIcon.src = '/img/edit.svg'; + const editActionText = document.createElement('p'); + editActionText.innerText = 'Editar'; + editActionArea.append(editActionIcon, editActionText); + + const deleteActionArea = document.createElement('div'); + deleteActionArea.classList.add('offer-action-area'); + deleteActionArea.classList.add('subtle-box'); + const deleteActionIcon = document.createElement('img'); + deleteActionIcon.src = '/img/trash-can-darkred.svg'; + const deleteActionText = document.createElement('p'); + deleteActionText.innerText = 'Eliminar'; + deleteActionArea.append(deleteActionIcon, deleteActionText); + deleteActionArea.addEventListener('click', async () => { + await this.deleteButtonCallback(); + }); + + actionButtonsArea.append(editActionArea, deleteActionArea); + + offerCard.append( + tradeDescDiv, + premiumDescDiv, + whereDescDiv, + whenDescDiv, + bitcoinMethodsDiv, + visibilityDiv, + otherOfferFeaturesDiv, + actionButtonsArea + ); + + return offerCard; + } +} + +module.exports = OfferCard; diff --git a/src/front/components/PlaceInput.js b/src/front/components/PlaceInput.js new file mode 100644 index 0000000..3d573b2 --- /dev/null +++ b/src/front/components/PlaceInput.js @@ -0,0 +1,26 @@ +class PlaceInput { + constructor({ parentElement, id }) { + this.element = null; + this.parentElement = parentElement; + this.id = id; + } + + render() { + const placeInput = document.createElement('textarea'); + placeInput.id = this.id; + placeInput.className = 'place-and-time-box'; + placeInput.autocomplete = 'on'; + placeInput.maxLength = 140; + placeInput.placeholder = + "¿Dónde? Ej.'Eixample', 'La Maquinista', 'Cualquier lugar en BCN', 'Meetup BBO'"; + + this.element = placeInput; + this.parentElement.appendChild(this.element); + } + + get inputText() { + return this.element.value; + } +} + +module.exports = PlaceInput; diff --git a/src/front/components/PopupNotification.js b/src/front/components/PopupNotification.js index e2a0588..8604184 100644 --- a/src/front/components/PopupNotification.js +++ b/src/front/components/PopupNotification.js @@ -9,7 +9,7 @@ class PopupNotification { render() { const div = document.createElement('div'); div.id = this.id; - div.className = 'top-notification-good'; + div.className = 'top-notification-good max-size-zero'; div.innerHTML = `

${this.text}

`; @@ -21,5 +21,19 @@ class PopupNotification { display() { this.element.classList.add('revealed'); } + + displayTemporarily(milliseconds) { + if (!milliseconds) { + milliseconds = 1000; + } + this.element.classList.remove('max-size-zero'); + this.element.classList.add('revealed'); + setTimeout(() => { + this.element.classList.remove('revealed'); + }, milliseconds); + setTimeout(() => { + this.element.classList.add('max-size-zero'); + }, milliseconds + 1000); + } } module.exports = PopupNotification; diff --git a/src/front/components/PremiumSelector.js b/src/front/components/PremiumSelector.js index d2defdd..68f994d 100644 --- a/src/front/components/PremiumSelector.js +++ b/src/front/components/PremiumSelector.js @@ -11,9 +11,10 @@ class PremiumSelector { render() { const premiumSelectorArea = document.createElement('div'); premiumSelectorArea.id = this.id; + premiumSelectorArea.classList = 'premium-selector-area'; const premiumValue = document.createElement('div'); - premiumValue.id = 'premium-value'; + premiumValue.className = 'premium-value'; premiumValue.textContent = '0%'; this.premiumValue = premiumValue; @@ -22,12 +23,12 @@ class PremiumSelector { const increaseButton = document.createElement('button'); increaseButton.classList.add('premium-button'); - increaseButton.id = 'button-increase-premium'; + increaseButton.classList.add('button-increase-premium'); increaseButton.textContent = '+'; const decreaseButton = document.createElement('button'); decreaseButton.classList.add('premium-button'); - decreaseButton.id = 'button-decrease-premium'; + decreaseButton.classList.add('button-decrease-premium'); decreaseButton.textContent = '-'; premiumButtonsContainer.appendChild(increaseButton); diff --git a/src/front/components/PriceDisplay.js b/src/front/components/PriceDisplay.js index 39eb16d..be3055d 100644 --- a/src/front/components/PriceDisplay.js +++ b/src/front/components/PriceDisplay.js @@ -16,10 +16,10 @@ class PriceDisplay { render() { const container = document.createElement('div'); - container.id = 'premium-price-display-area'; + container.id = this.id; + container.classList = 'premium-price-display-area'; const offerParagraph = document.createElement('p'); - offerParagraph.id = 'offer-price-paragraph'; offerParagraph.textContent = 'Tu precio: '; const offerSpan = document.createElement('span'); @@ -30,11 +30,9 @@ class PriceDisplay { offerParagraph.append('€/BTC'); const marketParagraph = document.createElement('p'); - marketParagraph.id = 'market-price-paragraph'; marketParagraph.textContent = '(Precio mercado: '; const marketSpan = document.createElement('span'); - marketSpan.id = 'market-price'; this.marketPriceSpan = marketSpan; marketParagraph.appendChild(marketSpan); diff --git a/src/front/components/TimeInput.js b/src/front/components/TimeInput.js new file mode 100644 index 0000000..68f0451 --- /dev/null +++ b/src/front/components/TimeInput.js @@ -0,0 +1,26 @@ +class TimeInput { + constructor({ parentElement, id }) { + this.element = null; + this.parentElement = parentElement; + this.id = id; + } + + render() { + const timeInput = document.createElement('textarea'); + timeInput.id = this.id; + timeInput.className = 'place-and-time-box'; + timeInput.autocomplete = 'on'; + timeInput.maxLength = 140; + timeInput.placeholder = + '¿Cuándo? Ej."Cualquier hora", "19:00-21:00", "Finde"'; + + this.element = timeInput; + this.parentElement.appendChild(this.element); + } + + get inputText() { + return this.element.value; + } +} + +module.exports = TimeInput; diff --git a/src/front/components/TrustCheckboxes.js b/src/front/components/TrustCheckboxes.js new file mode 100644 index 0000000..f1b8768 --- /dev/null +++ b/src/front/components/TrustCheckboxes.js @@ -0,0 +1,111 @@ +class TrustCheckboxes { + constructor({ parentElement }) { + this.myTrustedContainer = null; + this.myTrustedCheckboxElement = null; + this.myTrustedTrustedContainer = null; + this.myTrustedTrustedCheckboxElement = null; + this.allMembersContainer = null; + this.allMembersCheckboxElement = null; + this.parentElement = parentElement; + } + + render() { + const checkboxesDetails = [ + { + label: 'Mis confiados', + containerProperty: 'myTrustedContainer', + checkboxProperty: 'myTrustedCheckboxElement', + defaultChecked: true, + isDisabled: true, + }, + { + label: 'Los confiados de mis confiados', + containerProperty: 'myTrustedTrustedContainer', + checkboxProperty: 'myTrustedTrustedCheckboxElement', + defaultChecked: true, + isDisabled: false, + }, + { + label: 'Todos los miembros', + containerProperty: 'allMembersContainer', + checkboxProperty: 'allMembersCheckboxElement', + defaultChecked: false, + isDisabled: false, + }, + ]; + + for (const checkboxDetails of checkboxesDetails) { + this[checkboxDetails.containerProperty] = this.buildCheckbox({ + label: checkboxDetails.label, + }); + + this[checkboxDetails.checkboxProperty] = + this[checkboxDetails.containerProperty].querySelector('input'); + + this[checkboxDetails.checkboxProperty].addEventListener('click', () => { + this.applyTrustCheckboxConstraints( + this[checkboxDetails.checkboxProperty] + ); + }); + + this[checkboxDetails.checkboxProperty].checked = + checkboxDetails.defaultChecked; + this[checkboxDetails.checkboxProperty].disabled = + checkboxDetails.isDisabled; + + this.parentElement.appendChild(this[checkboxDetails.containerProperty]); + } + } + + buildCheckbox({ label }) { + const checkboxContainer = document.createElement('div'); + checkboxContainer.className = 'checkbox-row'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + + const labelElement = document.createElement('label'); + labelElement.htmlFor = checkbox.id; + labelElement.textContent = label; + + checkboxContainer.appendChild(checkbox); + checkboxContainer.appendChild(labelElement); + + return checkboxContainer; + } + + applyTrustCheckboxConstraints(pressedCheckbox) { + if (pressedCheckbox === this.myTrustedTrustedCheckboxElement) { + if ( + !this.myTrustedTrustedCheckboxElement.checked && + this.allMembersCheckboxElement.checked + ) { + this.allMembersCheckboxElement.checked = false; + } + } + + if (pressedCheckbox === this.allMembersCheckboxElement) { + if ( + !this.myTrustedTrustedCheckboxElement.checked && + this.allMembersCheckboxElement.checked + ) { + this.myTrustedTrustedCheckboxElement.checked = true; + } + } + } + + get showOfferToTrusted() { + return this.myTrustedCheckboxElement.checked; + } + + get showOfferToTrustedTrusted() { + return this.myTrustedTrustedCheckboxElement.checked; + } + + get showOfferToAllMembers() { + return this.allMembersCheckboxElement.checked; + } +} + +module.exports = TrustCheckboxes; diff --git a/src/front/pages/invite.js b/src/front/pages/invite.js index 24bc3b5..d756ca3 100644 --- a/src/front/pages/invite.js +++ b/src/front/pages/invite.js @@ -24,7 +24,7 @@ const invitesFunction = () => { if (verifyResponse.ok) { signUpSuccessPopup.display(); setTimeout(() => { - window.location.href = constants.API_PATHS.createProfile; + window.location.href = constants.WEB_PATHS.createProfile; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/login.js b/src/front/pages/login.js index 5620a99..4b304e2 100644 --- a/src/front/pages/login.js +++ b/src/front/pages/login.js @@ -38,7 +38,7 @@ const loginFunction = () => { nostrLoginButton.disable(); successPopup.display(); setTimeout(() => { - window.location.href = constants.API_PATHS.home; + window.location.href = constants.WEB_PATHS.home; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/offers.js b/src/front/pages/offers.js index 1913e7a..01c6ab3 100644 --- a/src/front/pages/offers.js +++ b/src/front/pages/offers.js @@ -1,51 +1,32 @@ -const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces'); -const PublishOfferButton = require('../components/PublishOfferButton'); -const BuyOrSellButtonGroup = require('../components/BuyOrSellButtonGroup'); -const PremiumSelector = require('../components/PremiumSelector'); -const PriceDisplay = require('../components/PriceDisplay'); +const PopupNotification = require('../components/PopupNotification'); +const CreateOfferModal = require('../components/CreateOfferModal'); +const OfferCard = require('../components/OfferCard'); + +const offerService = require('../services/offerService'); function offersPage() { - const createOfferEventBus = new EventTarget(); + const offerCreatedPopup = new PopupNotification({ + parentElement: document.body, + text: '¡Oferta creada! Puedes verla en tus ofertas.', + }); + offerCreatedPopup.render(); - const publishOfferButton = new PublishOfferButton({ - parentElement: document.getElementById('submit-button-area'), - id: 'button-submit-offer', - onClickCallback: async () => { - await publishOffer(); + const offerDeletedPopup = new PopupNotification({ + parentElement: document.body, + text: '¡Oferta eliminada!', + }); + offerDeletedPopup.render(); + + const createOfferModal = new CreateOfferModal({ + parentElement: document.body, + onCreationCallback: async () => { await myOffers.getOffersFromApi(); await myOffers.render(); + offerCreatedPopup.displayTemporarily(3000); }, + offerService: offerService, }); - publishOfferButton.render(); - - const buyOrSellButtonGroup = new BuyOrSellButtonGroup({ - parentElement: document.getElementById('buy-or-sell-area'), - id: 'button-group-buy-or-sell', - }); - buyOrSellButtonGroup.render(); - - const premiumSelector = new PremiumSelector({ - parentElement: document.getElementById('premium-content-area'), - id: 'premium-selector-area', - eventSink: createOfferEventBus, - }); - premiumSelector.render(); - - const priceDisplay = new PriceDisplay({ - parentElement: document.getElementById('premium-content-area'), - id: 'premium-price-display-area', - premiumProvidingCallback: () => { - return premiumSelector.getPremium(); - }, - priceProvidingCallback: () => { - return Math.floor(Math.random() * (95000 - 70000 + 1) + 70000); - }, - }); - priceDisplay.render(); - createOfferEventBus.addEventListener('premium-changed', () => { - priceDisplay.updatePrices(); - }); - + createOfferModal.render(); // ----------- const navbuttonHome = document.getElementById('navbutton-home'); const navbuttonOffers = document.getElementById('navbutton-offers'); @@ -62,550 +43,15 @@ function offersPage() { 'button-start-create-offer' ); const buttonViewMyOffers = document.getElementById('button-view-my-offers'); - const closeOffer = document.getElementById('close-offer'); - const createOfferModalRoot = document.getElementById( - 'create-offer-modal-root' - ); const viewMyOffersRoot = document.getElementById('view-my-offers-root'); - const eurAmountInput = document.getElementById('input-eur-amount'); - const btcAmountInput = document.getElementById('input-btc-amount'); - - const placeInput = document.getElementById('place-input'); - const timeInput = document.getElementById('time-input'); - - const onchainCheckbox = document.getElementById('onchain-checkbox'); - const lightningCheckbox = document.getElementById('lightning-checkbox'); - - const btcMethodCheckboxes = [onchainCheckbox, lightningCheckbox]; - - const myTrustedCheckbox = document.getElementById('my-trusted-checkbox'); - const myTrustedTrustedCheckbox = document.getElementById( - 'my-trusted-trusted-checkbox' - ); - const allMembersCheckbox = document.getElementById('all-members-checkbox'); - - const bigNotesAcceptedCheckbox = document.getElementById( - 'large-bills-checkbox' - ); - - const offerCreatedPopup = document.getElementById( - 'offer-created-confirmation' - ); - const offerDeletedPopup = document.getElementById( - 'offer-deleted-confirmation' - ); - const ownOffersContainer = document.getElementById('own-offers-container'); - function toggleCreateOfferModal() { - createOfferModalRoot.classList.toggle('shown'); - } - function toggleViewMyOffersPanel() { viewMyOffersRoot.style.display = viewMyOffersRoot.style.display === 'block' ? 'none' : 'block'; } - function readIntFromEurAmountInput() { - const eurAmountFieldValue = eurAmountInput.value; - const regularExpression = /([\d\s]+)/; - const matchResult = eurAmountFieldValue.match(regularExpression); - - if (!matchResult) { - return null; - } - - const numberString = matchResult[1]; - const cleanInputNumber = parseInt(numberString.replace(/\s/gi, '')); - - return cleanInputNumber; - } - - function validateAndFormatEurAmountInput() { - const cleanInputNumber = readIntFromEurAmountInput(); - eurAmountInput.classList.remove('input-is-valid', 'input-is-invalid'); - if (cleanInputNumber) { - eurAmountInput.value = formatNumberWithSpaces(cleanInputNumber); - eurAmountInput.classList.add('input-is-valid'); - return; - } - - eurAmountInput.classList.add('input-is-invalid'); - } - - function updateBtcInput() { - const eurToSatRate = 1021; - const cleanEurAmount = readIntFromEurAmountInput(); - - const satsAmount = cleanEurAmount * eurToSatRate; - const formattedSatsAmount = formatNumberWithSpaces(satsAmount); - btcAmountInput.value = formattedSatsAmount; - } - - function validateBitcoinMethodCheckboxes(clickedCheckbox) { - let checkedCount = btcMethodCheckboxes.filter((cb) => cb.checked).length; - if (checkedCount === 0) { - clickedCheckbox.checked = true; - } - } - - function applyTrustCheckboxConstraints(pressedCheckbox) { - if (pressedCheckbox === myTrustedTrustedCheckbox) { - console.log('first case!'); - if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) { - allMembersCheckbox.checked = false; - } - } - - if (pressedCheckbox === allMembersCheckbox) { - console.log('second case!'); - if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) { - myTrustedTrustedCheckbox.checked = true; - } - } - } - - async function publishOffer() { - const wants = buyOrSellButtonGroup.wants(); - - const premium = premiumSelector.getPremium(); - const trade_amount_eur = eurAmountInput.value; - const location_details = placeInput.value; - const time_availability_details = timeInput.value; - const is_onchain_accepted = onchainCheckbox.checked; - const is_lightning_accepted = lightningCheckbox.checked; - const show_offer_to_trusted = myTrustedCheckbox.checked; - const show_offer_to_trusted_trusted = myTrustedTrustedCheckbox.checked; - const show_offer_to_all_members = allMembersCheckbox.checked; - const are_big_notes_accepted = bigNotesAcceptedCheckbox.checked; - - const offerDetails = { - wants, - premium, - trade_amount_eur, - location_details, - time_availability_details, - is_onchain_accepted, - is_lightning_accepted, - show_offer_to_trusted, - show_offer_to_trusted_trusted, - show_offer_to_all_members, - are_big_notes_accepted, - }; - - await fetch('/api/offer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ offerDetails }), - }); - - toggleOfferCreatedAlert(); - - toggleCreateOfferModal(); - } - - function toggleOfferCreatedAlert() { - offerCreatedPopup.classList.remove('max-size-zero'); - offerCreatedPopup.classList.add('revealed'); - setTimeout(() => { - offerCreatedPopup.classList.remove('revealed'); - }, 3000); - setTimeout(() => { - offerCreatedPopup.classList.add('max-size-zero'); - }, 4000); - } - - function toggleOfferDeletedAlert() { - offerDeletedPopup.classList.remove('max-size-zero'); - offerDeletedPopup.classList.add('revealed'); - setTimeout(() => { - offerDeletedPopup.classList.remove('revealed'); - }, 3000); - setTimeout(() => { - offerDeletedPopup.classList.add('max-size-zero'); - }, 4000); - } - - class Offer { - constructor(offerData) { - this.uuid = offerData.uuid; - this.public_key = offerData.public_key; - this.wants = offerData.wants; - this.premium = offerData.premium; - this.trade_amount_eur = offerData.trade_amount_eur; - this.location_details = offerData.location_details; - this.time_availability_details = offerData.time_availability_details; - this.show_offer_to_trusted = offerData.show_offer_to_trusted; - this.show_offer_to_trusted_trusted = - offerData.show_offer_to_trusted_trusted; - this.show_offer_to_all_members = offerData.show_offer_to_all_members; - this.is_onchain_accepted = offerData.is_onchain_accepted; - this.is_lightning_accepted = offerData.is_lightning_accepted; - this.are_big_notes_accepted = offerData.are_big_notes_accepted; - this.created_at = offerData.created_at; - this.last_updated_at = offerData.last_updated_at; - } - - buildHTML() { - const offerCard = document.createElement('div'); - offerCard.classList.add('myoffer-card'); - offerCard.classList.add('shadowed-round-area'); - - const tradeDescDiv = document.createElement('div'); - tradeDescDiv.classList.add('trade-desc'); - - const youBuyText = document.createElement('p'); - youBuyText.classList.add('offer-card-content-title'); - youBuyText.innerText = 'Compras'; - tradeDescDiv.append(youBuyText); - - const youBuyData = document.createElement('p'); - youBuyData.classList.add('offer-card-content-data'); - if (this.wants === 'BTC') { - youBuyData.innerText = `${this.trade_amount_eur * 1021} sats`; - } - if (this.wants === 'EUR') { - youBuyData.innerText = `${this.trade_amount_eur} €`; - } - tradeDescDiv.append(youBuyData); - - const youSellText = document.createElement('p'); - youSellText.id = 'you-sell-title'; - youSellText.classList.add('offer-card-content-title'); - youSellText.innerText = 'Vendes'; - tradeDescDiv.append(youSellText); - - const youSellData = document.createElement('p'); - youSellData.classList.add('offer-card-content-data'); - if (this.wants === 'BTC') { - youSellData.innerText = `${this.trade_amount_eur} €`; - } - if (this.wants === 'EUR') { - youSellData.innerText = `${this.trade_amount_eur * 1021} sats`; - } - tradeDescDiv.append(youSellData); - - const premiumDescDiv = document.createElement('div'); - premiumDescDiv.classList.add('premium-desc'); - - const premiumTitle = document.createElement('p'); - premiumTitle.classList.add('offer-card-content-title'); - premiumTitle.innerText = 'Premium'; - premiumDescDiv.append(premiumTitle); - - const premiumData = document.createElement('p'); - premiumData.classList.add('offer-card-content-data'); - premiumData.innerText = `${this.premium * 100} %`; - premiumDescDiv.append(premiumData); - - const offerPriceTitle = document.createElement('p'); - offerPriceTitle.classList.add('offer-card-content-title'); - offerPriceTitle.innerText = 'Precio oferta'; - premiumDescDiv.append(offerPriceTitle); - - const offerPriceData = document.createElement('p'); - offerPriceData.classList.add('offer-card-content-data'); - offerPriceData.innerText = `90000 €/BTC`; - premiumDescDiv.append(offerPriceData); - - const marketPriceTitle = document.createElement('p'); - marketPriceTitle.classList.add('offer-card-content-title'); - marketPriceTitle.innerText = 'Precio mercado'; - premiumDescDiv.append(marketPriceTitle); - - const marketPriceData = document.createElement('p'); - marketPriceData.innerText = `88000 €/BTC`; - premiumDescDiv.append(marketPriceData); - - const whereDescDiv = document.createElement('div'); - whereDescDiv.classList.add('where-desc'); - - const whereDescTitle = document.createElement('p'); - whereDescTitle.classList.add('offer-card-content-title'); - whereDescTitle.innerText = 'Dónde'; - whereDescDiv.append(whereDescTitle); - - const whereDescData = document.createElement('p'); - whereDescData.classList.add('offer-long-text'); - whereDescData.innerText = `${this.location_details}`; - whereDescDiv.append(whereDescData); - - const whenDescDiv = document.createElement('div'); - whenDescDiv.classList.add('when-desc'); - - const whenDescTitle = document.createElement('p'); - whenDescTitle.classList.add('offer-card-content-title'); - whenDescTitle.innerText = 'Cúando'; - whenDescDiv.append(whenDescTitle); - - const whenDescData = document.createElement('p'); - whenDescData.classList.add('offer-long-text'); - whenDescData.innerText = `${this.time_availability_details}`; - whenDescDiv.append(whenDescData); - - const bitcoinMethodsDiv = document.createElement('div'); - bitcoinMethodsDiv.classList.add('bitcoin-methods-desc'); - - const bitcoinMethodsTitle = document.createElement('p'); - bitcoinMethodsTitle.classList.add('offer-card-content-title'); - bitcoinMethodsTitle.innerText = 'Protocolos Bitcoin aceptados'; - bitcoinMethodsDiv.append(bitcoinMethodsTitle); - - const onchainAcceptedContainer = document.createElement('div'); - onchainAcceptedContainer.classList.add('left-icon-checkboxed-field'); - if (this.is_onchain_accepted) { - const onchainIcon = document.createElement('img'); - onchainIcon.src = '/img/chains-lasecagold.svg'; - const onchainText = document.createElement('p'); - onchainText.innerText = 'Onchain'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); - } else { - const onchainIcon = document.createElement('img'); - onchainIcon.src = '/img/chains-gray.svg'; - const onchainText = document.createElement('p'); - onchainText.innerText = 'Onchain'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); - } - const lightningAcceptedContainer = document.createElement('div'); - - lightningAcceptedContainer.classList.add('left-icon-checkboxed-field'); - if (this.is_lightning_accepted) { - const lightningIcon = document.createElement('img'); - lightningIcon.src = '/img/bolt-lightning-lasecagold.svg'; - const lightningText = document.createElement('p'); - lightningText.innerText = 'Lightning'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - lightningAcceptedContainer.append( - lightningIcon, - lightningText, - checkIcon - ); - } else { - const lightningIcon = document.createElement('img'); - lightningIcon.src = '/img/bolt-lightning-gray.svg'; - const lightningText = document.createElement('p'); - lightningText.innerText = 'Lightning'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - lightningAcceptedContainer.append( - lightningIcon, - lightningText, - checkIcon - ); - } - - bitcoinMethodsDiv.append( - onchainAcceptedContainer, - lightningAcceptedContainer - ); - - const visibilityDiv = document.createElement('div'); - visibilityDiv.classList.add('visibility-desc'); - - const visibilityTitle = document.createElement('p'); - visibilityTitle.classList.add('offer-card-content-title'); - visibilityTitle.innerText = 'Visibilidad'; - visibilityDiv.append(visibilityTitle); - - const showOfferToTrustedContainer = document.createElement('div'); - showOfferToTrustedContainer.classList.add('right-icon-checkboxed-field'); - - if (this.show_offer_to_trusted) { - const showOfferToTrustedIcon = document.createElement('img'); - showOfferToTrustedIcon.src = '/img/user-lasecagold.svg'; - const showOfferToTrustedText = document.createElement('p'); - showOfferToTrustedText.innerText = 'Confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToTrustedContainer.append( - showOfferToTrustedIcon, - showOfferToTrustedText, - checkIcon - ); - } else { - const showOfferToTrustedIcon = document.createElement('img'); - showOfferToTrustedIcon.src = '/img/user-gray.svg'; - const showOfferToTrustedText = document.createElement('p'); - showOfferToTrustedText.innerText = 'Confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToTrustedContainer.append( - showOfferToTrustedIcon, - showOfferToTrustedText, - checkIcon - ); - } - - const showOfferToTrustedTrustedContainer = document.createElement('div'); - showOfferToTrustedTrustedContainer.classList.add( - 'right-icon-checkboxed-field' - ); - - if (this.show_offer_to_trusted_trusted) { - const showOfferToTrustedTrustedIcon = document.createElement('img'); - showOfferToTrustedTrustedIcon.src = '/img/user-group-lasecagold.svg'; - const showOfferToTrustedTrustedText = document.createElement('p'); - showOfferToTrustedTrustedText.innerText = 'Sus confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToTrustedTrustedContainer.append( - showOfferToTrustedTrustedIcon, - showOfferToTrustedTrustedText, - checkIcon - ); - } else { - const showOfferToTrustedTrustedIcon = document.createElement('img'); - showOfferToTrustedTrustedIcon.src = '/img/user-group-gray.svg'; - const showOfferToTrustedTrustedText = document.createElement('p'); - showOfferToTrustedTrustedText.innerText = 'Sus confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToTrustedTrustedContainer.append( - showOfferToTrustedTrustedIcon, - showOfferToTrustedTrustedText, - checkIcon - ); - } - - const showOfferToAllMembersContainer = document.createElement('div'); - showOfferToAllMembersContainer.classList.add( - 'right-icon-checkboxed-field' - ); - - if (this.show_offer_to_all_members) { - const showOfferToAllMembersIcon = document.createElement('img'); - showOfferToAllMembersIcon.src = '/img/many-users-lasecagold.svg'; - const showOfferToAllMembersText = document.createElement('p'); - showOfferToAllMembersText.innerText = 'Todos'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToAllMembersContainer.append( - showOfferToAllMembersIcon, - showOfferToAllMembersText, - checkIcon - ); - } else { - const showOfferToAllMembersIcon = document.createElement('img'); - showOfferToAllMembersIcon.src = '/img/many-users-gray.svg'; - const showOfferToAllMembersText = document.createElement('p'); - showOfferToAllMembersText.innerText = 'Todos'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToAllMembersContainer.append( - showOfferToAllMembersIcon, - showOfferToAllMembersText, - checkIcon - ); - } - visibilityDiv.append( - showOfferToTrustedContainer, - showOfferToTrustedTrustedContainer, - showOfferToAllMembersContainer - ); - - const otherOfferFeaturesDiv = document.createElement('div'); - otherOfferFeaturesDiv.classList.add('other-desc'); - - const otherOfferFeaturesTitle = document.createElement('p'); - otherOfferFeaturesTitle.classList.add('offer-card-content-title'); - otherOfferFeaturesTitle.innerText = 'Otros'; - otherOfferFeaturesDiv.append(otherOfferFeaturesTitle); - - const areBigNotesAcceptedContainer = document.createElement('div'); - areBigNotesAcceptedContainer.classList.add('left-icon-checkboxed-field'); - - if (this.are_big_notes_accepted) { - const areBigNotesAcceptedIcon = document.createElement('img'); - areBigNotesAcceptedIcon.src = '/img/eur-bill-lasecagold.svg'; - const areBigNotesAcceptedText = document.createElement('p'); - areBigNotesAcceptedText.innerText = 'Billetes grandes'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - areBigNotesAcceptedContainer.append( - areBigNotesAcceptedIcon, - areBigNotesAcceptedText, - checkIcon - ); - } else { - const areBigNotesAcceptedIcon = document.createElement('img'); - areBigNotesAcceptedIcon.src = '/img/eur-bill-gray.svg'; - const areBigNotesAcceptedText = document.createElement('p'); - areBigNotesAcceptedText.innerText = 'Billetes grandes'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - areBigNotesAcceptedContainer.append( - areBigNotesAcceptedIcon, - areBigNotesAcceptedText, - checkIcon - ); - } - - otherOfferFeaturesDiv.append(areBigNotesAcceptedContainer); - - const actionButtonsArea = document.createElement('p'); - actionButtonsArea.classList.add('offer-action-buttons-area'); - - const editActionArea = document.createElement('div'); - editActionArea.classList.add('offer-action-area'); - editActionArea.classList.add('subtle-box'); - const editActionIcon = document.createElement('img'); - editActionIcon.src = '/img/edit.svg'; - const editActionText = document.createElement('p'); - editActionText.innerText = 'Editar'; - editActionArea.append(editActionIcon, editActionText); - - const deleteActionArea = document.createElement('div'); - deleteActionArea.classList.add('offer-action-area'); - deleteActionArea.classList.add('subtle-box'); - const deleteActionIcon = document.createElement('img'); - deleteActionIcon.src = '/img/trash-can-darkred.svg'; - const deleteActionText = document.createElement('p'); - deleteActionText.innerText = 'Eliminar'; - deleteActionArea.append(deleteActionIcon, deleteActionText); - deleteActionArea.addEventListener('click', async () => { - await deleteOfferByUuid(this.uuid); - await myOffers.getOffersFromApi(); - await myOffers.render(); - toggleOfferDeletedAlert(); - }); - - actionButtonsArea.append(editActionArea, deleteActionArea); - - offerCard.append( - tradeDescDiv, - premiumDescDiv, - whereDescDiv, - whenDescDiv, - bitcoinMethodsDiv, - visibilityDiv, - otherOfferFeaturesDiv, - actionButtonsArea - ); - - return offerCard; - } - } - class MyOffers { constructor(ownOffersContainerElement) { this.ownOffersContainerElement = ownOffersContainerElement; @@ -620,7 +66,17 @@ function offersPage() { const offersData = (await offersResponse.json()).data; if (offersResponse.ok) { for (const record of offersData) { - this.offers.push(new Offer(record)); + this.offers.push( + new OfferCard({ + offerData: record, + deleteButtonCallback: async () => { + await offerService.deleteOffer(record.uuid); + await this.getOffersFromApi(); + await this.render(); + offerDeletedPopup.displayTemporarily(3000); + }, + }) + ); } } } @@ -639,17 +95,8 @@ function offersPage() { } } - async function deleteOfferByUuid(offerUuid) { - await fetch(`/api/offer/${offerUuid}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }); - } - buttonStartCreateOffer.addEventListener('click', () => { - toggleCreateOfferModal(); + createOfferModal.toggle(); }); buttonViewMyOffers.addEventListener('click', async () => { @@ -658,36 +105,6 @@ function offersPage() { toggleViewMyOffersPanel(); }); - closeOffer.addEventListener('click', () => { - toggleCreateOfferModal(); - }); - - eurAmountInput.addEventListener('blur', () => { - validateAndFormatEurAmountInput(); - updateBtcInput(); - }); - - eurAmountInput.addEventListener('input', () => { - eurAmountInput.value = eurAmountInput.value.replace(/[^0-9]/g, ''); - updateBtcInput(); - }); - - for (const btcMethodCheckbox of btcMethodCheckboxes) { - btcMethodCheckbox.addEventListener('click', () => { - validateBitcoinMethodCheckboxes(btcMethodCheckbox); - }); - } - - myTrustedTrustedCheckbox.addEventListener('click', () => { - applyTrustCheckboxConstraints(myTrustedTrustedCheckbox); - }); - - allMembersCheckbox.addEventListener('click', () => { - applyTrustCheckboxConstraints(allMembersCheckbox); - }); - - updateBtcInput(); - const myOffers = new MyOffers(ownOffersContainer); } diff --git a/src/front/services/inviteService.js b/src/front/services/inviteService.js index d494c6c..590913e 100644 --- a/src/front/services/inviteService.js +++ b/src/front/services/inviteService.js @@ -1,7 +1,9 @@ +const constants = require('../../constants'); + const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let challengeResponse; try { - challengeResponse = await fetch('/api/signup/nostr-challenge', { + challengeResponse = await fetch(constants.API_PATHS.signupNostrChallenge, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -39,7 +41,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let verifyResponse; try { - verifyResponse = await fetch('/api/signup/nostr-verify', { + verifyResponse = await fetch(constants.API_PATHS.signupNostrVerify, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(signedEvent), diff --git a/src/front/services/loginService.js b/src/front/services/loginService.js index d1f5815..852b438 100644 --- a/src/front/services/loginService.js +++ b/src/front/services/loginService.js @@ -1,15 +1,12 @@ +const constants = require('../../constants'); + const requestAndRespondLoginChallenge = async ({ onRejectedPubKeyCallback, onRejectedSignatureCallback, }) => { - onRejectedPubKeyCallback = () => { - document.querySelector('#rejected-nostr-nudges').style.display = 'block'; - }; - onRejectedSignatureCallback = onRejectedPubKeyCallback; - let challengeResponse; try { - challengeResponse = await fetch('/api/login/nostr-challenge', { + challengeResponse = await fetch(constants.API_PATHS.loginNostrChallenge, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -47,7 +44,7 @@ const requestAndRespondLoginChallenge = async ({ let verifyResponse; try { - verifyResponse = await fetch('/api/login/nostr-verify', { + verifyResponse = await fetch(constants.API_PATHS.loginNostrVerify, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(signedEvent), diff --git a/src/front/services/offerService.js b/src/front/services/offerService.js new file mode 100644 index 0000000..591a0f0 --- /dev/null +++ b/src/front/services/offerService.js @@ -0,0 +1,25 @@ +const constants = require('../../constants'); + +const createOffer = async (offerDetails) => { + await fetch(constants.API_PATHS.offer, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ offerDetails }), + }); +}; + +const deleteOffer = async (offerUuid) => { + await fetch(`${constants.API_PATHS.offer}/${offerUuid}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +module.exports = { + createOffer, + deleteOffer, +}; diff --git a/src/views/offers.ejs b/src/views/offers.ejs index aba756c..6fa2e9f 100644 --- a/src/views/offers.ejs +++ b/src/views/offers.ejs @@ -31,145 +31,6 @@

Vaya, no hay nada por aquí...

-
-
-
-

Añade los detalles de tu oferta

-
-
-

Premium

-
-
-
-

¿Cuánto?

-
-
- -
- -
-
-
- -
- SAT -
-
-
-
-
-

¿Dónde y cuándo?

-
- - -
-
-
-

¿Cómo se mueve el Bitcoin?

-
- -
-
- -
-
-
-

¿Quién puede ver la oferta?

-
- -
-
- -
-
- -
-
-
-

Extras

-
- -
-
-
-
- -
-
-
-
-
- -

¡Oferta creada! Puedes verla en tus ofertas.

-
-
- -

¡Oferta eliminada!

-