diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fa244c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# secajs + +## Architecture + +This repository contains a webapp. It covers the full stack which consists of: +- The client side code, in `src/front/`. +- The backend service in `src/`, except for `src/front/`. +- A Postgres database. Database connections and migrations are in `src/database/`. +- Besides, there is an admin CLI, with entrypoint in `src/cli.js` and commands in `src/commands/`. + diff --git a/Dockerfile b/Dockerfile index 10706ec..42d2c5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:latest +FROM debian:12 # Install dependencies RUN apt-get update diff --git a/package.json b/package.json index a1170cb..568a109 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "start": "node src/app.js", "start:container": "docker compose up -d --build", "stop:container": "docker compose down", + "migrate": "npx sequelize-cli db:migrate", "build": "webpack", "watch": "webpack --watch", "cli": "node src/cli.js", - "test": "playwright test", "lint": "eslint . --fix", - "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\"" + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\"", + "test": "playwright test" }, "keywords": [], "author": "", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..c7f57c8 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,13 @@ +const { defineConfig } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: './tests', + use: { + baseURL: 'http://localhost:3000', + }, + webServer: { + command: 'npm start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/public/css/offers.css b/public/css/offers.css index 6a4e706..17ac4b5 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,8 +221,10 @@ font-size: 0.9em; } -.create-offer-controls { +#create-offer-controls { text-align: center; + overflow-y: auto; + max-height: 800px; padding: 20px; } @@ -241,16 +243,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; @@ -259,19 +261,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%; } @@ -287,22 +289,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; } @@ -312,7 +314,7 @@ font-size: 0.8em; } -.amount-area-content { +#amount-area-content { margin-left: auto; margin-right: auto; } @@ -375,7 +377,7 @@ width: 2em; } -.submit-button-area { +#submit-button-area { margin-top: 1em; margin-bottom: 1em; } @@ -384,7 +386,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 35b6217..1cc3034 100644 --- a/public/css/seca.css +++ b/public/css/seca.css @@ -141,8 +141,6 @@ 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 a454e1b..7af448b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,24 +2,14 @@ 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 = { - 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', + home: '/home', }; 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 deleted file mode 100644 index 9badd2a..0000000 --- a/src/front/components/AmountInput.js +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 6361a72..0000000 --- a/src/front/components/BigNotesCheckbox.js +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 27cb3f8..0000000 --- a/src/front/components/BitcoinMethodCheckboxes.js +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index b857ea5..0000000 --- a/src/front/components/CloseModalButton.js +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index b5301d1..0000000 --- a/src/front/components/CreateOfferModal.js +++ /dev/null @@ -1,239 +0,0 @@ -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 deleted file mode 100644 index 5c11452..0000000 --- a/src/front/components/OfferCard.js +++ /dev/null @@ -1,374 +0,0 @@ -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 deleted file mode 100644 index 3d573b2..0000000 --- a/src/front/components/PlaceInput.js +++ /dev/null @@ -1,26 +0,0 @@ -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 8604184..e2a0588 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 max-size-zero'; + div.className = 'top-notification-good'; div.innerHTML = `

${this.text}

`; @@ -21,19 +21,5 @@ 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 68f994d..d2defdd 100644 --- a/src/front/components/PremiumSelector.js +++ b/src/front/components/PremiumSelector.js @@ -11,10 +11,9 @@ class PremiumSelector { render() { const premiumSelectorArea = document.createElement('div'); premiumSelectorArea.id = this.id; - premiumSelectorArea.classList = 'premium-selector-area'; const premiumValue = document.createElement('div'); - premiumValue.className = 'premium-value'; + premiumValue.id = 'premium-value'; premiumValue.textContent = '0%'; this.premiumValue = premiumValue; @@ -23,12 +22,12 @@ class PremiumSelector { const increaseButton = document.createElement('button'); increaseButton.classList.add('premium-button'); - increaseButton.classList.add('button-increase-premium'); + increaseButton.id = 'button-increase-premium'; increaseButton.textContent = '+'; const decreaseButton = document.createElement('button'); decreaseButton.classList.add('premium-button'); - decreaseButton.classList.add('button-decrease-premium'); + decreaseButton.id = 'button-decrease-premium'; decreaseButton.textContent = '-'; premiumButtonsContainer.appendChild(increaseButton); diff --git a/src/front/components/PriceDisplay.js b/src/front/components/PriceDisplay.js index be3055d..39eb16d 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 = this.id; - container.classList = 'premium-price-display-area'; + container.id = 'premium-price-display-area'; const offerParagraph = document.createElement('p'); + offerParagraph.id = 'offer-price-paragraph'; offerParagraph.textContent = 'Tu precio: '; const offerSpan = document.createElement('span'); @@ -30,9 +30,11 @@ 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 deleted file mode 100644 index 68f0451..0000000 --- a/src/front/components/TimeInput.js +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index f1b8768..0000000 --- a/src/front/components/TrustCheckboxes.js +++ /dev/null @@ -1,111 +0,0 @@ -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 d756ca3..24bc3b5 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.WEB_PATHS.createProfile; + window.location.href = constants.API_PATHS.createProfile; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/login.js b/src/front/pages/login.js index 4b304e2..5620a99 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.WEB_PATHS.home; + window.location.href = constants.API_PATHS.home; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/offers.js b/src/front/pages/offers.js index 01c6ab3..1913e7a 100644 --- a/src/front/pages/offers.js +++ b/src/front/pages/offers.js @@ -1,32 +1,51 @@ -const PopupNotification = require('../components/PopupNotification'); -const CreateOfferModal = require('../components/CreateOfferModal'); -const OfferCard = require('../components/OfferCard'); - -const offerService = require('../services/offerService'); +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'); function offersPage() { - const offerCreatedPopup = new PopupNotification({ - parentElement: document.body, - text: '¡Oferta creada! Puedes verla en tus ofertas.', - }); - offerCreatedPopup.render(); + const createOfferEventBus = new EventTarget(); - const offerDeletedPopup = new PopupNotification({ - parentElement: document.body, - text: '¡Oferta eliminada!', - }); - offerDeletedPopup.render(); - - const createOfferModal = new CreateOfferModal({ - parentElement: document.body, - onCreationCallback: async () => { + const publishOfferButton = new PublishOfferButton({ + parentElement: document.getElementById('submit-button-area'), + id: 'button-submit-offer', + onClickCallback: async () => { + await publishOffer(); await myOffers.getOffersFromApi(); await myOffers.render(); - offerCreatedPopup.displayTemporarily(3000); }, - offerService: offerService, }); - createOfferModal.render(); + 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(); + }); + // ----------- const navbuttonHome = document.getElementById('navbutton-home'); const navbuttonOffers = document.getElementById('navbutton-offers'); @@ -43,15 +62,550 @@ 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; @@ -66,17 +620,7 @@ function offersPage() { const offersData = (await offersResponse.json()).data; if (offersResponse.ok) { for (const record of offersData) { - this.offers.push( - new OfferCard({ - offerData: record, - deleteButtonCallback: async () => { - await offerService.deleteOffer(record.uuid); - await this.getOffersFromApi(); - await this.render(); - offerDeletedPopup.displayTemporarily(3000); - }, - }) - ); + this.offers.push(new Offer(record)); } } } @@ -95,8 +639,17 @@ function offersPage() { } } + async function deleteOfferByUuid(offerUuid) { + await fetch(`/api/offer/${offerUuid}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + } + buttonStartCreateOffer.addEventListener('click', () => { - createOfferModal.toggle(); + toggleCreateOfferModal(); }); buttonViewMyOffers.addEventListener('click', async () => { @@ -105,6 +658,36 @@ 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 590913e..d494c6c 100644 --- a/src/front/services/inviteService.js +++ b/src/front/services/inviteService.js @@ -1,9 +1,7 @@ -const constants = require('../../constants'); - const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let challengeResponse; try { - challengeResponse = await fetch(constants.API_PATHS.signupNostrChallenge, { + challengeResponse = await fetch('/api/signup/nostr-challenge', { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -41,7 +39,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let verifyResponse; try { - verifyResponse = await fetch(constants.API_PATHS.signupNostrVerify, { + verifyResponse = await fetch('/api/signup/nostr-verify', { 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 852b438..d1f5815 100644 --- a/src/front/services/loginService.js +++ b/src/front/services/loginService.js @@ -1,12 +1,15 @@ -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(constants.API_PATHS.loginNostrChallenge, { + challengeResponse = await fetch('/api/login/nostr-challenge', { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -44,7 +47,7 @@ const requestAndRespondLoginChallenge = async ({ let verifyResponse; try { - verifyResponse = await fetch(constants.API_PATHS.loginNostrVerify, { + verifyResponse = await fetch('/api/login/nostr-verify', { 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 deleted file mode 100644 index 591a0f0..0000000 --- a/src/front/services/offerService.js +++ /dev/null @@ -1,25 +0,0 @@ -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 6fa2e9f..aba756c 100644 --- a/src/views/offers.ejs +++ b/src/views/offers.ejs @@ -31,6 +31,145 @@

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!

+
diff --git a/tests/basic.spec.js b/tests/basic.spec.js new file mode 100644 index 0000000..3c51e92 --- /dev/null +++ b/tests/basic.spec.js @@ -0,0 +1,12 @@ +const { test, expect } = require('@playwright/test'); + +test('app starts and public page is reachable', async ({ page }) => { + // Navigate to the root page + await page.goto('/'); + + // Check that the page loads (should redirect to login) + await expect(page).toHaveURL('/login'); + + // Verify we can see some content on the login page + await expect(page.locator('body')).toBeVisible(); +}); diff --git a/tests/createOffer.spec.js b/tests/createOffer.spec.js deleted file mode 100644 index 2820b3a..0000000 --- a/tests/createOffer.spec.js +++ /dev/null @@ -1,106 +0,0 @@ -const { test, expect, hardcodedSessionUuid } = require('./test-setup'); - -const SessionCreated = require('../src/models/SessionCreated'); -const SessionRelatedToPublickey = require('../src/models/SessionRelatedToPublickey'); -const NymSet = require('../src/models/NymSet'); -const ContactDetailsSet = require('../src/models/ContactDetailsSet'); -const OfferDetailsSet = require('../src/models/OfferDetailsSet'); - -test('Mock records are present', async () => { - for (const someModel of [ - SessionCreated, - SessionRelatedToPublickey, - NymSet, - ContactDetailsSet, - ]) { - expect(await someModel.findOne()).toBeTruthy(); - } -}); - -test('Hardcoded session cookie is there', async ({ context }) => { - const page = await context.newPage(); - const cookiesInPage = await page.context().cookies(); - expect(cookiesInPage).toHaveLength(1); - expect(cookiesInPage[0].name).toBe('sessionUuid'); - expect(cookiesInPage[0].value).toBe(hardcodedSessionUuid); -}); - -test('Offers is reachable', async ({ context }) => { - const page = await context.newPage(); - - await page.goto('http://localhost/offers'); - const createOfferButton = page.locator('#button-start-create-offer'); - await expect(createOfferButton).toBeVisible(); - await expect(createOfferButton).toContainText('Crear nueva oferta'); -}); - -test('Create an offer with a few options creates in DB', async ({ - context, -}) => { - const page = await context.newPage(); - await page.goto('http://localhost/offers'); - - await page.getByRole('button', { name: 'Crear nueva oferta' }).click(); - await expect(page.locator('#close-offer-controls-area')).toBeVisible(); - await page.getByRole('button', { name: 'Quiero vender Bitcoin' }).click(); - await page.getByRole('button', { name: 'Quiero comprar Bitcoin' }).click(); - await page.getByRole('button', { name: '+' }).click(); - await page.getByRole('button', { name: '+' }).click(); - await page.getByRole('button', { name: '+' }).click(); - await page.getByRole('button', { name: '-' }).click(); - await expect(page.locator('#premium-value')).toContainText('2%'); - await page.locator('#input-eur-amount').click(); - await page.locator('#input-eur-amount').press('ControlOrMeta+a'); - await page.locator('#input-eur-amount').fill('50'); - await expect(page.locator('#input-eur-amount')).toHaveValue('50'); - await page - .getByText( - 'Añade los detalles de tu oferta Quiero comprar Bitcoin Quiero vender Bitcoin' - ) - .click(); - await page - .getByRole('textbox', { name: '¿Dónde? Ej."Eixample", "La' }) - .click(); - await page - .getByRole('textbox', { name: '¿Dónde? Ej."Eixample", "La' }) - .fill('En algún lugar'); - await page - .getByRole('textbox', { name: '¿Cuándo? Ej."Cualquier hora' }) - .click(); - await page - .getByRole('textbox', { name: '¿Cuándo? Ej."Cualquier hora' }) - .fill('En algún momento'); - await page.locator('#onchain-checkbox').uncheck(); - await expect(page.locator('#onchain-checkbox')).not.toBeChecked(); - await expect(page.locator('#lightning-checkbox')).toBeChecked(); - await page.locator('#my-trusted-trusted-checkbox').uncheck(); - await page.locator('#all-members-checkbox').check(); - await page.locator('#my-trusted-trusted-checkbox').check(); - await expect(page.locator('#my-trusted-trusted-checkbox')).toBeChecked(); - await page.locator('#all-members-checkbox').uncheck(); - await expect(page.locator('#all-members-checkbox')).not.toBeChecked(); - await page.locator('#large-bills-checkbox').check(); - await expect(page.locator('#large-bills-checkbox')).toBeChecked(); - - await page.getByRole('button', { name: 'Publicar oferta' }).click(); - await expect(page.locator('#offer-created-confirmation')).toBeInViewport(); - await expect( - page.locator('#offer-created-confirmation') - ).not.toBeInViewport(); - await expect(page.locator('#close-offer-controls-area')).not.toBeVisible(); - - const createdOfferDetailsSetRecord = await OfferDetailsSet.findOne(); - expect(createdOfferDetailsSetRecord.wants).toBe('BTC'); - expect(createdOfferDetailsSetRecord.premium).toBe('0.02'); - expect(createdOfferDetailsSetRecord.trade_amount_eur).toBe(50); - expect(createdOfferDetailsSetRecord.location_details).toBe('En algún lugar'); - expect(createdOfferDetailsSetRecord.time_availability_details).toBe( - 'En algún momento' - ); - expect(createdOfferDetailsSetRecord.show_offer_to_trusted).toBe(true); - expect(createdOfferDetailsSetRecord.show_offer_to_trusted_trusted).toBe(true); - expect(createdOfferDetailsSetRecord.show_offer_to_all_members).toBe(false); - expect(createdOfferDetailsSetRecord.is_onchain_accepted).toBe(false); - expect(createdOfferDetailsSetRecord.is_lightning_accepted).toBe(true); - expect(createdOfferDetailsSetRecord.are_big_notes_accepted).toBe(true); -}); diff --git a/tests/invite.spec.js b/tests/invite.spec.js new file mode 100644 index 0000000..d49cde1 --- /dev/null +++ b/tests/invite.spec.js @@ -0,0 +1,36 @@ +const { test, expect } = require('@playwright/test'); +const { execSync } = require('child_process'); + +test('can create invite with CLI and access invite page', async ({ page }) => { + // Create an invite using the CLI + const inviterNpub = 'npub1test1234567890abcdefghijklmnopqrstuvwxyz'; + + try { + const output = execSync(`npm run cli createAppInvite ${inviterNpub}`, { + encoding: 'utf8', + cwd: process.cwd() + }); + + // Extract the invite UUID from the CLI output + const match = output.match(/http:\/\/localhost\/invite\/([a-f0-9-]+)/); + if (!match) { + throw new Error('Could not extract invite UUID from CLI output'); + } + + const inviteUuid = match[1]; + console.log(`Created invite with UUID: ${inviteUuid}`); + + // Navigate to the invite page + await page.goto(`/invite/${inviteUuid}`); + + // Check that the invite page loads correctly + await expect(page).toHaveTitle('Invite Details'); + await expect(page.locator('h1')).toContainText('¡Has sido invitado a la seca!'); + await expect(page.locator('#laseca-logo')).toBeVisible(); + await expect(page.locator('#nostr-signup-button')).toBeVisible(); + + } catch (error) { + console.error('Error creating invite or accessing page:', error); + throw error; + } +}); diff --git a/tests/recorderHelper.spec.js b/tests/recorderHelper.spec.js deleted file mode 100644 index 2c87c21..0000000 --- a/tests/recorderHelper.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -// You can uncomment this below to open a recorder page - -/* -const { chromium } = require('playwright'); -test('Mock records are present', async () => { - const browser = await chromium.launch({ headless: false }); - const context = await browser.newContext(); - await context.addCookies([ - { - name: 'sessionUuid', - value: hardcodedSessionUuid, - domain: 'localhost', - path: '/', - expires: Math.floor( - new Date(new Date().setMonth(new Date().getMonth() + 1)).getTime() / - 1000 - ), //This monster is this day next month, turned into epoch format - httpOnly: true, - secure: false, - sameSite: 'Lax', - }, - ]); - - const page = await context.newPage(); - await page.goto('http://localhost'); -}); -*/ diff --git a/tests/test-setup.js b/tests/test-setup.js deleted file mode 100644 index 3d4b669..0000000 --- a/tests/test-setup.js +++ /dev/null @@ -1,72 +0,0 @@ -const { test, expect } = require('@playwright/test'); - -const SessionCreated = require('../src/models/SessionCreated'); -const SessionRelatedToPublickey = require('../src/models/SessionRelatedToPublickey'); -const NymSet = require('../src/models/NymSet'); -const ContactDetailsSet = require('../src/models/ContactDetailsSet'); - -const hardcodedSessionUuid = '0195423c-33d7-75f8-921b-a06e6d3cb8c5'; -const hardcodedPublicKey = - 'd3d4c49e7bdbbbf3082151add080e92f9a458d5dec993b371fe6d02cd394d57a'; - -test.beforeEach(async () => { - for (const someModel of [ - SessionCreated, - SessionRelatedToPublickey, - NymSet, - ContactDetailsSet, - ]) { - someModel.truncate(); - } - - const currentTimestamp = new Date(); - const expiryTimestamp = new Date(currentTimestamp.getTime()); - expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + 60); - await SessionCreated.create({ - uuid: hardcodedSessionUuid, - created_at: currentTimestamp.toISOString(), - expires_at: expiryTimestamp.toISOString(), - }); - - await SessionRelatedToPublickey.create({ - uuid: '0195423b-f9ae-737e-98f3-880f6563ed8a', - session_uuid: hardcodedSessionUuid, - public_key: hardcodedPublicKey, - created_at: new Date().toISOString(), - }); - - await NymSet.create({ - uuid: '01954240-ddbb-7d01-9017-efb3e500d333', - public_key: hardcodedPublicKey, - nym: 'test_nym', - created_at: new Date().toISOString(), - }); - - await ContactDetailsSet.create({ - uuid: '01954240-ddbb-7d01-9017-efb3e500d333', - public_key: hardcodedPublicKey, - encrypted_contact_details: - '+OD0/Y2IkJ99/E0KAJL/mp3kxQo4DFp1deSPnqiejlyGoeWzBiipemPVSTT/Jg/fCQbN9Pd/GJ6shxuwWECOVyB5PnMZOVJ1MPQ7I8A+63XZ0gKnSnJgry6F69f3MhEjH49JbeVJ37TbruFu/Woevo24VWz2gPXGBuyHLzeg1tyT9+7ZSygkcCrh+bchvymCoF1nNOm/UQKnwecH1wWzo8a+rNokazD1/3iey6iKmKewi+yGCgmljrB866akqBAl?iv=PAKhqTeBfYVX/muhM8xaEA==', - created_at: new Date().toISOString(), - }); -}); - -test.beforeEach(async ({ context }) => { - await context.addCookies([ - { - name: 'sessionUuid', - value: hardcodedSessionUuid, - domain: 'localhost', - path: '/', - expires: Math.floor( - new Date(new Date().setMonth(new Date().getMonth() + 1)).getTime() / - 1000 - ), //This monster is this day next month, turned into epoch format - httpOnly: true, - secure: false, - sameSite: 'Lax', - }, - ]); -}); - -module.exports = { test, expect, hardcodedSessionUuid };