Compare commits

..

4 commits

Author SHA1 Message Date
1a5ef88c55
basic tests 2025-08-28 22:57:20 +02:00
cd9c7678ee
add AGENTS.md 2025-08-28 22:49:07 +02:00
f444bd792f
remove tests 2025-08-28 22:45:36 +02:00
74263b1e1c
add migration command to package.json 2025-08-28 22:43:21 +02:00
31 changed files with 871 additions and 1348 deletions

10
AGENTS.md Normal file
View file

@ -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/`.

View file

@ -1,4 +1,4 @@
FROM debian:latest
FROM debian:12
# Install dependencies
RUN apt-get update

View file

@ -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": "",

13
playwright.config.js Normal file
View file

@ -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,
},
});

View file

@ -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;
}

View file

@ -141,8 +141,6 @@ h1 {
padding: 10px;
width: fit-content;
max-width: 95%;
max-height: 90vh;
overflow: auto;
margin: 20px auto;
}

View file

@ -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,
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 = `<img src="/img/circle-check-white.svg" />
<p>${this.text}</p>`;
@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
},

View file

@ -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);
}
},

View file

@ -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);
}

View file

@ -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),

View file

@ -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),

View file

@ -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,
};

View file

@ -31,6 +31,145 @@
<p>Vaya, no hay nada por aquí...</p>
</div>
</div>
<div id="create-offer-modal-root" class="full-screen-modal-background">
<div class="full-screen-modal" id="create-offer-root">
<div id="create-offer-controls">
<h2>Añade los detalles de tu oferta</h2>
<div id="buy-or-sell-area" class="create-offer-step"></div>
<div id="premium-area" class="create-offer-step">
<h3>Premium</h3>
<div id="premium-content-area"></div>
</div>
<div id="amount-area" class="create-offer-step">
<h3>¿Cuánto?</h3>
<div id="amount-area-content">
<div id="eur-amount" class="money-amount-input-area">
<input
id="input-eur-amount"
type="text"
class="money-input input-money-amount"
value="100"
required
/>
<div id="eur-symbol" class="curr-symbol">
<span id="eur-character" class="curr-character">€</span>
</div>
</div>
<div id="btc-amount" class="money-amount-input-area">
<input
id="input-btc-amount"
type="text"
class="money-input input-money-amount"
disabled
/>
<div id="sats-symbol" class="curr-symbol">
<span id="sats-character" class="curr-character">SAT</span>
</div>
</div>
</div>
</div>
<div id="place-and-time-area" class="create-offer-step">
<h3>¿Dónde y cuándo?</h3>
<div id="place-and-time-boxes">
<textarea
id="place-input"
class="place-and-time-box"
autocomplete="on"
maxlength="140"
placeholder='¿Dónde? Ej."Eixample", "La Maquinista", "Cualquier lugar en BCN", "Meetup BBO"'
></textarea>
<textarea
id="time-input"
class="place-and-time-box"
autocomplete="on"
maxlength="140"
placeholder='¿Cuándo? Ej."Cualquier hora", "19:00-21:00", "Finde"'
></textarea>
</div>
</div>
<div id="bitcoin-methods-area" class="create-offer-step">
<h3>¿Cómo se mueve el Bitcoin?</h3>
<div id="onchain-checkbox-area" class="checkbox-row">
<input
type="checkbox"
name="onchain"
id="onchain-checkbox"
checked
/><label for="onchain">Onchain</label>
</div>
<div id="lightning-checkbox-area" class="checkbox-row">
<input
type="checkbox"
name="lightning"
id="lightning-checkbox"
checked
/><label for="lightning">Lightning</label>
</div>
</div>
<div id="trust-area" class="create-offer-step">
<h3>¿Quién puede ver la oferta?</h3>
<div id="my-trusted-area" class="checkbox-row">
<input
type="checkbox"
name="my-trusted"
id="my-trusted-checkbox"
checked
disabled
/><label for="my-trusted">Mis confiados</label>
</div>
<div id="my-trusted-trusted-area" class="checkbox-row">
<input
type="checkbox"
name="my-trusted-trusted"
id="my-trusted-trusted-checkbox"
checked
/><label for="my-trusted-trusted"
>Los confiados de mis confiados</label
>
</div>
<div id="all-members-area" class="checkbox-row">
<input
type="checkbox"
name="all-members"
id="all-members-checkbox"
/><label for="all-members">Todos los miembros</label>
</div>
</div>
<div id="other-area" class="create-offer-step">
<h3>Extras</h3>
<div id="large-bills-area" class="checkbox-row">
<input
type="checkbox"
name="large-bills"
id="large-bills-checkbox"
/><label for="large-bills"
>Se pueden usar billetes grandes (100€, 200€, 500€)</label
>
</div>
</div>
<div id="submit-button-area"></div>
<div id="close-offer-controls-area">
<button id="close-offer" class="button-secondary button-medium">
Volver
</button>
</div>
</div>
</div>
</div>
<div
id="offer-created-confirmation"
class="top-notification-good max-size-zero"
>
<img src="/img/circle-check-white.svg" />
<p>¡Oferta creada! Puedes verla en tus ofertas.</p>
</div>
<div
id="offer-deleted-confirmation"
class="top-notification-good max-size-zero"
>
<img src="/img/circle-check-white.svg" />
<p>¡Oferta eliminada!</p>
</div>
<script src="/javascript/offers.bundle.js"></script>
</body>

12
tests/basic.spec.js Normal file
View file

@ -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();
});

View file

@ -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);
});

36
tests/invite.spec.js Normal file
View file

@ -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;
}
});

View file

@ -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');
});
*/

View file

@ -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 };