diff --git a/public/css/offers.css b/public/css/offers.css
index 17ac4b5..6a4e706 100644
--- a/public/css/offers.css
+++ b/public/css/offers.css
@@ -11,11 +11,11 @@
max-width: 30%;
}
- #premium-selector-area {
+ .premium-selector-area {
width: 80px;
}
- #amount-area-content {
+ .amount-area-content {
width: 50%;
}
@@ -50,11 +50,11 @@
width: 50px;
}
- #premium-selector-area {
+ .premium-selector-area {
width: 100px;
}
- #amount-area-content {
+ .amount-area-content {
width: 33%;
}
@@ -221,10 +221,8 @@
font-size: 0.9em;
}
-#create-offer-controls {
+.create-offer-controls {
text-align: center;
- overflow-y: auto;
- max-height: 800px;
padding: 20px;
}
@@ -243,16 +241,16 @@
margin-top: 0;
}
-#close-offer-controls-area {
+.close-offer-controls-area {
display: flex;
justify-content: end;
}
-#premium-area > * {
+.premium-area > * {
display: block;
}
-#premium-content-area {
+.premium-content-area {
width: 80%;
height: 50px;
align-items: center;
@@ -261,19 +259,19 @@
display: flex;
}
-#premium-selector-area {
+.premium-selector-area {
margin-left: auto;
margin-right: 5%;
display: flex;
}
-#premium-value {
+.premium-value {
border: 1px solid gray;
width: 50%;
align-content: center;
}
-#premium-buttons-container {
+.premium-buttons-container {
width: 50%;
}
@@ -289,22 +287,22 @@
background: #fff8ce;
}
-#button-increase-premium {
+.button-increase-premium {
border-top-right-radius: 10px;
}
-#button-decrease-premium {
+.button-decrease-premium {
border-bottom-right-radius: 10px;
}
-#premium-price-display-area {
+.premium-price-display-area {
margin-left: 0;
margin-right: auto;
font-size: 1em;
text-align: start;
}
-#premium-price-display-area > * {
+.premium-price-display-area > * {
margin-top: 0;
margin-bottom: 0;
}
@@ -314,7 +312,7 @@
font-size: 0.8em;
}
-#amount-area-content {
+.amount-area-content {
margin-left: auto;
margin-right: auto;
}
@@ -377,7 +375,7 @@
width: 2em;
}
-#submit-button-area {
+.submit-button-area {
margin-top: 1em;
margin-bottom: 1em;
}
@@ -386,7 +384,7 @@
width: 33%;
}
-#close-offer {
+.close-offer {
margin-left: auto;
margin-right: auto;
}
diff --git a/public/css/seca.css b/public/css/seca.css
index 1cc3034..35b6217 100644
--- a/public/css/seca.css
+++ b/public/css/seca.css
@@ -141,6 +141,8 @@ h1 {
padding: 10px;
width: fit-content;
max-width: 95%;
+ max-height: 90vh;
+ overflow: auto;
margin: 20px auto;
}
diff --git a/src/constants.js b/src/constants.js
index 7af448b..a454e1b 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -2,14 +2,24 @@ const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;
const DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS = 60 * 60 * 24 * 30;
const DEFAULT_REDIRECT_DELAY = 3 * 1000; // 3seconds times milliseconds;
+const API_ROOT = '/api';
const API_PATHS = {
- createProfile: '/createProfile',
+ offer: API_ROOT + '/offer',
+ loginNostrChallenge: API_ROOT + '/login/nostr-challenge',
+ loginNostrVerify: API_ROOT + '/login/nostr-verify',
+ signupNostrChallenge: API_ROOT + '/signup/nostr-challenge',
+ signupNostrVerify: API_ROOT + '/signup/nostr-verify',
+};
+
+const WEB_PATHS = {
home: '/home',
+ createProfile: '/createProfile',
};
module.exports = {
DEFAULT_SESSION_DURATION_SECONDS,
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS,
API_PATHS,
+ WEB_PATHS,
DEFAULT_REDIRECT_DELAY,
};
diff --git a/src/front/components/AmountInput.js b/src/front/components/AmountInput.js
new file mode 100644
index 0000000..9badd2a
--- /dev/null
+++ b/src/front/components/AmountInput.js
@@ -0,0 +1,113 @@
+const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces');
+
+class AmountInput {
+ constructor({ parentElement, id }) {
+ this.element = null;
+ this.parentElement = parentElement;
+ this.id = id;
+
+ this.eurInput = null;
+ this.btcInput = null;
+ }
+
+ render() {
+ const amountArea = document.createElement('div');
+ amountArea.id = this.id;
+ amountArea.className = 'amount-area-content';
+
+ const eurAmount = document.createElement('div');
+ eurAmount.className = 'money-amount-input-area';
+
+ this.eurInput = document.createElement('input');
+ this.eurInput.type = 'text';
+ this.eurInput.className = 'money-input input-money-amount';
+ this.eurInput.value = '100';
+ this.eurInput.required = true;
+
+ const eurSymbol = document.createElement('div');
+ eurSymbol.className = 'curr-symbol';
+
+ const eurCharacter = document.createElement('span');
+ eurCharacter.className = 'curr-character';
+ eurCharacter.textContent = '€';
+
+ eurSymbol.appendChild(eurCharacter);
+ eurAmount.appendChild(this.eurInput);
+ eurAmount.appendChild(eurSymbol);
+
+ const btcAmount = document.createElement('div');
+ btcAmount.className = 'money-amount-input-area';
+
+ this.btcInput = document.createElement('input');
+ this.btcInput.type = 'text';
+ this.btcInput.className = 'money-input input-money-amount';
+ this.btcInput.disabled = true;
+
+ const satsSymbol = document.createElement('div');
+ satsSymbol.className = 'curr-symbol';
+
+ const satsCharacter = document.createElement('span');
+ satsCharacter.className = 'curr-character';
+ satsCharacter.textContent = 'SAT';
+
+ satsSymbol.appendChild(satsCharacter);
+ btcAmount.appendChild(this.btcInput);
+ btcAmount.appendChild(satsSymbol);
+
+ amountArea.appendChild(eurAmount);
+ amountArea.appendChild(btcAmount);
+
+ this.eurInput.addEventListener('blur', () => {
+ this.validateAndFormatEurAmountInput();
+ this.updateBtcInput();
+ });
+
+ this.eurInput.addEventListener('input', () => {
+ this.eurInput.value = this.eurInput.value.replace(/[^0-9]/g, '');
+ this.updateBtcInput();
+ });
+
+ this.updateBtcInput();
+
+ this.element = amountArea;
+ this.parentElement.appendChild(this.element);
+ }
+
+ get intEurAmount() {
+ const eurAmountFieldValue = this.eurInput.value;
+ const regularExpression = /([\d\s]+)/;
+ const matchResult = eurAmountFieldValue.match(regularExpression);
+
+ if (!matchResult) {
+ return null;
+ }
+
+ const numberString = matchResult[1];
+ const cleanInputNumber = parseInt(numberString.replace(/\s/gi, ''));
+
+ return cleanInputNumber;
+ }
+
+ validateAndFormatEurAmountInput() {
+ const cleanInputNumber = this.intEurAmount;
+ this.eurInput.classList.remove('input-is-valid', 'input-is-invalid');
+ if (cleanInputNumber) {
+ this.eurInput.value = formatNumberWithSpaces(cleanInputNumber);
+ this.eurInput.classList.add('input-is-valid');
+ return;
+ }
+
+ this.eurInput.classList.add('input-is-invalid');
+ }
+
+ updateBtcInput() {
+ const eurToSatRate = 1021;
+ const cleanEurAmount = this.intEurAmount;
+
+ const satsAmount = cleanEurAmount * eurToSatRate;
+ const formattedSatsAmount = formatNumberWithSpaces(satsAmount);
+ this.btcInput.value = formattedSatsAmount;
+ }
+}
+
+module.exports = AmountInput;
diff --git a/src/front/components/BigNotesCheckbox.js b/src/front/components/BigNotesCheckbox.js
new file mode 100644
index 0000000..6361a72
--- /dev/null
+++ b/src/front/components/BigNotesCheckbox.js
@@ -0,0 +1,35 @@
+class BigNotesCheckbox {
+ constructor({ parentElement }) {
+ this.bigNotesContainer = null;
+ this.bigNotesCheckboxElement = null;
+ this.parentElement = parentElement;
+ }
+
+ render() {
+ const container = document.createElement('div');
+ container.className = 'checkbox-row';
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.name = 'large-bills';
+ checkbox.id = 'large-bills-checkbox';
+
+ const label = document.createElement('label');
+ label.htmlFor = 'large-bills-checkbox';
+ label.textContent = 'Se pueden usar billetes grandes (100€, 200€, 500€)';
+
+ container.appendChild(checkbox);
+ container.appendChild(label);
+
+ this.bigNotesContainer = container;
+ this.bigNotesCheckboxElement = checkbox;
+
+ this.parentElement.append(this.bigNotesContainer);
+ }
+
+ get areBigNotesAccepted() {
+ return this.bigNotesCheckboxElement.checked;
+ }
+}
+
+module.exports = BigNotesCheckbox;
diff --git a/src/front/components/BitcoinMethodCheckboxes.js b/src/front/components/BitcoinMethodCheckboxes.js
new file mode 100644
index 0000000..27cb3f8
--- /dev/null
+++ b/src/front/components/BitcoinMethodCheckboxes.js
@@ -0,0 +1,74 @@
+class BitcoinMethodCheckboxes {
+ constructor({ parentElement }) {
+ this.onchainContainer = null;
+ this.onchainCheckboxElement = null;
+ this.lightningContainer = null;
+ this.lightningCheckboxElement = null;
+ this.parentElement = parentElement;
+ }
+
+ render() {
+ this.onchainContainer = this.buildCheckbox({
+ id: 'onchain',
+ label: 'Onchain',
+ });
+ this.onchainCheckboxElement = this.onchainContainer.querySelector('input');
+
+ this.lightningContainer = this.buildCheckbox({
+ id: 'lightning',
+ label: 'Lightning',
+ });
+ this.lightningCheckboxElement =
+ this.lightningContainer.querySelector('input');
+
+ for (const btcMethodCheckbox of [
+ this.onchainCheckboxElement,
+ this.lightningCheckboxElement,
+ ]) {
+ btcMethodCheckbox.addEventListener('click', () => {
+ this.validateBitcoinMethodCheckboxes(btcMethodCheckbox);
+ });
+ }
+
+ this.parentElement.appendChild(this.onchainContainer);
+ this.parentElement.appendChild(this.lightningContainer);
+ }
+
+ buildCheckbox({ label }) {
+ const checkboxContainer = document.createElement('div');
+ checkboxContainer.className = 'checkbox-row';
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.checked = true;
+
+ const labelElement = document.createElement('label');
+ labelElement.htmlFor = checkbox.id;
+ labelElement.textContent = label;
+
+ checkboxContainer.appendChild(checkbox);
+ checkboxContainer.appendChild(labelElement);
+
+ return checkboxContainer;
+ }
+
+ validateBitcoinMethodCheckboxes(clickedCheckbox) {
+ let checkedCount = [
+ this.onchainCheckboxElement,
+ this.lightningCheckboxElement,
+ ].filter((cb) => cb.checked).length;
+ if (checkedCount === 0) {
+ clickedCheckbox.checked = true;
+ }
+ }
+
+ get isOnchainAccepted() {
+ return this.onchainCheckboxElement.checked;
+ }
+
+ get isLightningAccepted() {
+ return this.lightningCheckboxElement.checked;
+ }
+}
+
+module.exports = BitcoinMethodCheckboxes;
diff --git a/src/front/components/CloseModalButton.js b/src/front/components/CloseModalButton.js
new file mode 100644
index 0000000..b857ea5
--- /dev/null
+++ b/src/front/components/CloseModalButton.js
@@ -0,0 +1,21 @@
+class CloseModalButton {
+ constructor({ parentElement, id, onClickCallback }) {
+ this.element = null;
+ this.parentElement = parentElement;
+ this.id = id;
+ this.onClickCallback = onClickCallback;
+ }
+
+ render() {
+ const closeButton = document.createElement('button');
+ closeButton.className = 'close-offer button-secondary button-medium';
+ closeButton.textContent = 'Volver';
+
+ closeButton.addEventListener('click', this.onClickCallback);
+
+ this.element = closeButton;
+ this.parentElement.appendChild(this.element);
+ }
+}
+
+module.exports = CloseModalButton;
diff --git a/src/front/components/CreateOfferModal.js b/src/front/components/CreateOfferModal.js
new file mode 100644
index 0000000..b5301d1
--- /dev/null
+++ b/src/front/components/CreateOfferModal.js
@@ -0,0 +1,239 @@
+const PublishOfferButton = require('./PublishOfferButton');
+const BuyOrSellButtonGroup = require('./BuyOrSellButtonGroup');
+const PremiumSelector = require('./PremiumSelector');
+const PriceDisplay = require('./PriceDisplay');
+const AmountInput = require('./AmountInput');
+const PlaceInput = require('./PlaceInput');
+const TimeInput = require('./TimeInput');
+const BitcoinMethodCheckboxes = require('./BitcoinMethodCheckboxes');
+const TrustCheckboxes = require('./TrustCheckboxes');
+const BigNotesCheckbox = require('./BigNotesCheckbox');
+const CloseModalButton = require('./CloseModalButton');
+
+class CreateOfferModal {
+ constructor({ parentElement, onCreationCallback, offerService }) {
+ this.element = null;
+ this.parentElement = parentElement;
+ this.onCreationCallback = onCreationCallback;
+ this.offerService = offerService;
+
+ this.publishOfferButton = null;
+ this.buyOrSellButtonGroup = null;
+ this.premiumSelector = null;
+ this.amountInput = null;
+ this.placeInput = null;
+ this.timeInput = null;
+ this.btcMethodCheckboxes = null;
+ this.trustCheckboxes = null;
+ this.bigNotesCheckbox = null;
+ }
+
+ render() {
+ const modalRoot = document.createElement('div');
+ this.element = modalRoot;
+ modalRoot.className = 'full-screen-modal-background';
+
+ const modal = document.createElement('div');
+ modal.className = 'full-screen-modal';
+
+ const controls = document.createElement('div');
+ controls.className = 'create-offer-controls';
+
+ const title = document.createElement('h2');
+ title.textContent = 'Añade los detalles de tu oferta';
+ controls.appendChild(title);
+
+ const buyOrSellDiv = document.createElement('div');
+ buyOrSellDiv.className = 'create-offer-step';
+ this.buyOrSellButtonGroup = new BuyOrSellButtonGroup({
+ parentElement: buyOrSellDiv,
+ });
+ this.buyOrSellButtonGroup.render();
+ controls.appendChild(buyOrSellDiv);
+
+ const premiumDiv = document.createElement('div');
+ premiumDiv.classList = 'premium-area';
+ premiumDiv.className = 'create-offer-step';
+ const premiumHeading = document.createElement('h3');
+ premiumHeading.textContent = 'Premium';
+ premiumDiv.appendChild(premiumHeading);
+ const premiumContentDiv = document.createElement('div');
+ premiumContentDiv.classList = 'premium-content-area';
+ premiumDiv.appendChild(premiumContentDiv);
+ controls.appendChild(premiumDiv);
+
+ const createOfferEventBus = new EventTarget();
+
+ const mockPriceProvidingCallback = () => {
+ return Math.floor(Math.random() * (95000 - 70000 + 1) + 70000);
+ };
+
+ this.premiumSelector = new PremiumSelector({
+ parentElement: premiumContentDiv,
+ eventSink: createOfferEventBus,
+ });
+ this.premiumSelector.render();
+
+ const priceDisplay = new PriceDisplay({
+ parentElement: premiumContentDiv,
+ id: 'premium-price-display-area',
+ premiumProvidingCallback: () => {
+ return this.premiumSelector.getPremium();
+ },
+ priceProvidingCallback: mockPriceProvidingCallback,
+ });
+ priceDisplay.render();
+ createOfferEventBus.addEventListener('premium-changed', () => {
+ priceDisplay.updatePrices();
+ });
+
+ const amountDiv = document.createElement('div');
+ amountDiv.className = 'create-offer-step';
+ const amountHeading = document.createElement('h3');
+ amountHeading.textContent = '¿Cuánto?';
+ amountDiv.appendChild(amountHeading);
+ controls.appendChild(amountDiv);
+
+ this.amountInput = new AmountInput({
+ parentElement: amountDiv,
+ });
+
+ this.amountInput.render();
+
+ const placeTimeDiv = document.createElement('div');
+ placeTimeDiv.className = 'create-offer-step';
+ const placeTimeHeading = document.createElement('h3');
+ placeTimeHeading.textContent = '¿Dónde y cuándo?';
+ placeTimeDiv.appendChild(placeTimeHeading);
+ const placeTimeContentDiv = document.createElement('div');
+ placeTimeDiv.appendChild(placeTimeContentDiv);
+ controls.appendChild(placeTimeDiv);
+
+ this.placeInput = new PlaceInput({
+ parentElement: placeTimeContentDiv,
+ });
+
+ this.placeInput.render();
+
+ this.timeInput = new TimeInput({
+ parentElement: placeTimeContentDiv,
+ });
+
+ this.timeInput.render();
+
+ const bitcoinMethodsDiv = document.createElement('div');
+ bitcoinMethodsDiv.className = 'create-offer-step';
+ const bitcoinMethodsHeading = document.createElement('h3');
+ bitcoinMethodsHeading.textContent = '¿Cómo se mueve el Bitcoin?';
+ bitcoinMethodsDiv.appendChild(bitcoinMethodsHeading);
+ const bitcoinMethodsContentDiv = document.createElement('div');
+ bitcoinMethodsDiv.appendChild(bitcoinMethodsContentDiv);
+ controls.appendChild(bitcoinMethodsDiv);
+
+ this.btcMethodCheckboxes = new BitcoinMethodCheckboxes({
+ parentElement: bitcoinMethodsContentDiv,
+ });
+
+ this.btcMethodCheckboxes.render();
+
+ const trustDiv = document.createElement('div');
+ trustDiv.className = 'create-offer-step';
+ const trustHeading = document.createElement('h3');
+ trustHeading.textContent = '¿Quién puede ver la oferta?';
+ trustDiv.appendChild(trustHeading);
+ const trustContentDiv = document.createElement('div');
+ trustDiv.appendChild(trustContentDiv);
+ controls.appendChild(trustDiv);
+
+ this.trustCheckboxes = new TrustCheckboxes({
+ parentElement: trustContentDiv,
+ });
+
+ this.trustCheckboxes.render();
+
+ const otherDiv = document.createElement('div');
+ otherDiv.className = 'create-offer-step';
+ const otherHeading = document.createElement('h3');
+ otherHeading.textContent = 'Extras';
+ otherDiv.appendChild(otherHeading);
+ controls.appendChild(otherDiv);
+
+ this.bigNotesCheckbox = new BigNotesCheckbox({
+ parentElement: otherDiv,
+ });
+
+ this.bigNotesCheckbox.render();
+ //Continue moving components up here
+
+ const submitButtonArea = document.createElement('div');
+ submitButtonArea.classList.add('submit-button-area');
+ this.publishOfferButton = new PublishOfferButton({
+ parentElement: submitButtonArea,
+ id: 'button-submit-offer',
+ onClickCallback: async () => {
+ await this.createOffer();
+ await this.onCreationCallback();
+ this.toggle();
+ },
+ });
+ this.publishOfferButton.render();
+
+ const closeButtonArea = document.createElement('div');
+ closeButtonArea.className = 'close-offer-controls-area';
+ const closeButton = new CloseModalButton({
+ parentElement: closeButtonArea,
+ onClickCallback: () => {
+ this.toggle();
+ },
+ });
+
+ closeButton.render();
+
+ controls.appendChild(submitButtonArea);
+ controls.appendChild(closeButtonArea);
+
+ modal.appendChild(controls);
+ modalRoot.appendChild(modal);
+
+ this.parentElement.appendChild(this.element);
+ }
+
+ toggle() {
+ this.element.classList.toggle('shown');
+ }
+
+ async createOffer() {
+ const wants = this.buyOrSellButtonGroup.wants();
+
+ const premium = this.premiumSelector.getPremium();
+ const trade_amount_eur = this.amountInput.intEurAmount;
+ const location_details = this.placeInput.inputText;
+ const time_availability_details = this.timeInput.inputText;
+ const is_onchain_accepted = this.btcMethodCheckboxes.isOnchainAccepted;
+ const is_lightning_accepted = this.btcMethodCheckboxes.isLightningAccepted;
+ const show_offer_to_trusted = this.trustCheckboxes.showOfferToTrusted;
+ const show_offer_to_trusted_trusted =
+ this.trustCheckboxes.showOfferToTrustedTrusted;
+ const show_offer_to_all_members =
+ this.trustCheckboxes.showOfferToAllMembers;
+ const are_big_notes_accepted = this.bigNotesCheckbox.areBigNotesAccepted;
+
+ const offerDetails = {
+ wants,
+ premium,
+ trade_amount_eur,
+ location_details,
+ time_availability_details,
+ is_onchain_accepted,
+ is_lightning_accepted,
+ show_offer_to_trusted,
+ show_offer_to_trusted_trusted,
+ show_offer_to_all_members,
+ are_big_notes_accepted,
+ };
+
+ await this.offerService.createOffer(offerDetails);
+ }
+}
+
+module.exports = CreateOfferModal;
diff --git a/src/front/components/OfferCard.js b/src/front/components/OfferCard.js
new file mode 100644
index 0000000..5c11452
--- /dev/null
+++ b/src/front/components/OfferCard.js
@@ -0,0 +1,374 @@
+class OfferCard {
+ constructor({ offerData, deleteButtonCallback }) {
+ this.uuid = offerData.uuid;
+ this.public_key = offerData.public_key;
+ this.wants = offerData.wants;
+ this.premium = offerData.premium;
+ this.trade_amount_eur = offerData.trade_amount_eur;
+ this.location_details = offerData.location_details;
+ this.time_availability_details = offerData.time_availability_details;
+ this.show_offer_to_trusted = offerData.show_offer_to_trusted;
+ this.show_offer_to_trusted_trusted =
+ offerData.show_offer_to_trusted_trusted;
+ this.show_offer_to_all_members = offerData.show_offer_to_all_members;
+ this.is_onchain_accepted = offerData.is_onchain_accepted;
+ this.is_lightning_accepted = offerData.is_lightning_accepted;
+ this.are_big_notes_accepted = offerData.are_big_notes_accepted;
+ this.created_at = offerData.created_at;
+ this.last_updated_at = offerData.last_updated_at;
+
+ this.deleteButtonCallback = deleteButtonCallback;
+ }
+
+ buildHTML() {
+ const offerCard = document.createElement('div');
+ offerCard.classList.add('myoffer-card');
+ offerCard.classList.add('shadowed-round-area');
+
+ const tradeDescDiv = document.createElement('div');
+ tradeDescDiv.classList.add('trade-desc');
+
+ const youBuyText = document.createElement('p');
+ youBuyText.classList.add('offer-card-content-title');
+ youBuyText.innerText = 'Compras';
+ tradeDescDiv.append(youBuyText);
+
+ const youBuyData = document.createElement('p');
+ youBuyData.classList.add('offer-card-content-data');
+ if (this.wants === 'BTC') {
+ youBuyData.innerText = `${this.trade_amount_eur * 1021} sats`;
+ }
+ if (this.wants === 'EUR') {
+ youBuyData.innerText = `${this.trade_amount_eur} €`;
+ }
+ tradeDescDiv.append(youBuyData);
+
+ const youSellText = document.createElement('p');
+ youSellText.classList.add('offer-card-content-title');
+ youSellText.innerText = 'Vendes';
+ tradeDescDiv.append(youSellText);
+
+ const youSellData = document.createElement('p');
+ youSellData.classList.add('offer-card-content-data');
+ if (this.wants === 'BTC') {
+ youSellData.innerText = `${this.trade_amount_eur} €`;
+ }
+ if (this.wants === 'EUR') {
+ youSellData.innerText = `${this.trade_amount_eur * 1021} sats`;
+ }
+ tradeDescDiv.append(youSellData);
+
+ const premiumDescDiv = document.createElement('div');
+ premiumDescDiv.classList.add('premium-desc');
+
+ const premiumTitle = document.createElement('p');
+ premiumTitle.classList.add('offer-card-content-title');
+ premiumTitle.innerText = 'Premium';
+ premiumDescDiv.append(premiumTitle);
+
+ const premiumData = document.createElement('p');
+ premiumData.classList.add('offer-card-content-data');
+ premiumData.innerText = `${this.premium * 100} %`;
+ premiumDescDiv.append(premiumData);
+
+ const offerPriceTitle = document.createElement('p');
+ offerPriceTitle.classList.add('offer-card-content-title');
+ offerPriceTitle.innerText = 'Precio oferta';
+ premiumDescDiv.append(offerPriceTitle);
+
+ const offerPriceData = document.createElement('p');
+ offerPriceData.classList.add('offer-card-content-data');
+ offerPriceData.innerText = `90000 €/BTC`;
+ premiumDescDiv.append(offerPriceData);
+
+ const marketPriceTitle = document.createElement('p');
+ marketPriceTitle.classList.add('offer-card-content-title');
+ marketPriceTitle.innerText = 'Precio mercado';
+ premiumDescDiv.append(marketPriceTitle);
+
+ const marketPriceData = document.createElement('p');
+ marketPriceData.innerText = `88000 €/BTC`;
+ premiumDescDiv.append(marketPriceData);
+
+ const whereDescDiv = document.createElement('div');
+ whereDescDiv.classList.add('where-desc');
+
+ const whereDescTitle = document.createElement('p');
+ whereDescTitle.classList.add('offer-card-content-title');
+ whereDescTitle.innerText = 'Dónde';
+ whereDescDiv.append(whereDescTitle);
+
+ const whereDescData = document.createElement('p');
+ whereDescData.classList.add('offer-long-text');
+ whereDescData.innerText = `${this.location_details}`;
+ whereDescDiv.append(whereDescData);
+
+ const whenDescDiv = document.createElement('div');
+ whenDescDiv.classList.add('when-desc');
+
+ const whenDescTitle = document.createElement('p');
+ whenDescTitle.classList.add('offer-card-content-title');
+ whenDescTitle.innerText = 'Cúando';
+ whenDescDiv.append(whenDescTitle);
+
+ const whenDescData = document.createElement('p');
+ whenDescData.classList.add('offer-long-text');
+ whenDescData.innerText = `${this.time_availability_details}`;
+ whenDescDiv.append(whenDescData);
+
+ const bitcoinMethodsDiv = document.createElement('div');
+ bitcoinMethodsDiv.classList.add('bitcoin-methods-desc');
+
+ const bitcoinMethodsTitle = document.createElement('p');
+ bitcoinMethodsTitle.classList.add('offer-card-content-title');
+ bitcoinMethodsTitle.innerText = 'Protocolos Bitcoin aceptados';
+ bitcoinMethodsDiv.append(bitcoinMethodsTitle);
+
+ const onchainAcceptedContainer = document.createElement('div');
+ onchainAcceptedContainer.classList.add('left-icon-checkboxed-field');
+ if (this.is_onchain_accepted) {
+ const onchainIcon = document.createElement('img');
+ onchainIcon.src = '/img/chains-lasecagold.svg';
+ const onchainText = document.createElement('p');
+ onchainText.innerText = 'Onchain';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon);
+ } else {
+ const onchainIcon = document.createElement('img');
+ onchainIcon.src = '/img/chains-gray.svg';
+ const onchainText = document.createElement('p');
+ onchainText.innerText = 'Onchain';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon);
+ }
+ const lightningAcceptedContainer = document.createElement('div');
+
+ lightningAcceptedContainer.classList.add('left-icon-checkboxed-field');
+ if (this.is_lightning_accepted) {
+ const lightningIcon = document.createElement('img');
+ lightningIcon.src = '/img/bolt-lightning-lasecagold.svg';
+ const lightningText = document.createElement('p');
+ lightningText.innerText = 'Lightning';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ lightningAcceptedContainer.append(
+ lightningIcon,
+ lightningText,
+ checkIcon
+ );
+ } else {
+ const lightningIcon = document.createElement('img');
+ lightningIcon.src = '/img/bolt-lightning-gray.svg';
+ const lightningText = document.createElement('p');
+ lightningText.innerText = 'Lightning';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ lightningAcceptedContainer.append(
+ lightningIcon,
+ lightningText,
+ checkIcon
+ );
+ }
+
+ bitcoinMethodsDiv.append(
+ onchainAcceptedContainer,
+ lightningAcceptedContainer
+ );
+
+ const visibilityDiv = document.createElement('div');
+ visibilityDiv.classList.add('visibility-desc');
+
+ const visibilityTitle = document.createElement('p');
+ visibilityTitle.classList.add('offer-card-content-title');
+ visibilityTitle.innerText = 'Visibilidad';
+ visibilityDiv.append(visibilityTitle);
+
+ const showOfferToTrustedContainer = document.createElement('div');
+ showOfferToTrustedContainer.classList.add('right-icon-checkboxed-field');
+
+ if (this.show_offer_to_trusted) {
+ const showOfferToTrustedIcon = document.createElement('img');
+ showOfferToTrustedIcon.src = '/img/user-lasecagold.svg';
+ const showOfferToTrustedText = document.createElement('p');
+ showOfferToTrustedText.innerText = 'Confiados';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ showOfferToTrustedContainer.append(
+ showOfferToTrustedIcon,
+ showOfferToTrustedText,
+ checkIcon
+ );
+ } else {
+ const showOfferToTrustedIcon = document.createElement('img');
+ showOfferToTrustedIcon.src = '/img/user-gray.svg';
+ const showOfferToTrustedText = document.createElement('p');
+ showOfferToTrustedText.innerText = 'Confiados';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ showOfferToTrustedContainer.append(
+ showOfferToTrustedIcon,
+ showOfferToTrustedText,
+ checkIcon
+ );
+ }
+
+ const showOfferToTrustedTrustedContainer = document.createElement('div');
+ showOfferToTrustedTrustedContainer.classList.add(
+ 'right-icon-checkboxed-field'
+ );
+
+ if (this.show_offer_to_trusted_trusted) {
+ const showOfferToTrustedTrustedIcon = document.createElement('img');
+ showOfferToTrustedTrustedIcon.src = '/img/user-group-lasecagold.svg';
+ const showOfferToTrustedTrustedText = document.createElement('p');
+ showOfferToTrustedTrustedText.innerText = 'Sus confiados';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ showOfferToTrustedTrustedContainer.append(
+ showOfferToTrustedTrustedIcon,
+ showOfferToTrustedTrustedText,
+ checkIcon
+ );
+ } else {
+ const showOfferToTrustedTrustedIcon = document.createElement('img');
+ showOfferToTrustedTrustedIcon.src = '/img/user-group-gray.svg';
+ const showOfferToTrustedTrustedText = document.createElement('p');
+ showOfferToTrustedTrustedText.innerText = 'Sus confiados';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ showOfferToTrustedTrustedContainer.append(
+ showOfferToTrustedTrustedIcon,
+ showOfferToTrustedTrustedText,
+ checkIcon
+ );
+ }
+
+ const showOfferToAllMembersContainer = document.createElement('div');
+ showOfferToAllMembersContainer.classList.add('right-icon-checkboxed-field');
+
+ if (this.show_offer_to_all_members) {
+ const showOfferToAllMembersIcon = document.createElement('img');
+ showOfferToAllMembersIcon.src = '/img/many-users-lasecagold.svg';
+ const showOfferToAllMembersText = document.createElement('p');
+ showOfferToAllMembersText.innerText = 'Todos';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ showOfferToAllMembersContainer.append(
+ showOfferToAllMembersIcon,
+ showOfferToAllMembersText,
+ checkIcon
+ );
+ } else {
+ const showOfferToAllMembersIcon = document.createElement('img');
+ showOfferToAllMembersIcon.src = '/img/many-users-gray.svg';
+ const showOfferToAllMembersText = document.createElement('p');
+ showOfferToAllMembersText.innerText = 'Todos';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ showOfferToAllMembersContainer.append(
+ showOfferToAllMembersIcon,
+ showOfferToAllMembersText,
+ checkIcon
+ );
+ }
+ visibilityDiv.append(
+ showOfferToTrustedContainer,
+ showOfferToTrustedTrustedContainer,
+ showOfferToAllMembersContainer
+ );
+
+ const otherOfferFeaturesDiv = document.createElement('div');
+ otherOfferFeaturesDiv.classList.add('other-desc');
+
+ const otherOfferFeaturesTitle = document.createElement('p');
+ otherOfferFeaturesTitle.classList.add('offer-card-content-title');
+ otherOfferFeaturesTitle.innerText = 'Otros';
+ otherOfferFeaturesDiv.append(otherOfferFeaturesTitle);
+
+ const areBigNotesAcceptedContainer = document.createElement('div');
+ areBigNotesAcceptedContainer.classList.add('left-icon-checkboxed-field');
+
+ if (this.are_big_notes_accepted) {
+ const areBigNotesAcceptedIcon = document.createElement('img');
+ areBigNotesAcceptedIcon.src = '/img/eur-bill-lasecagold.svg';
+ const areBigNotesAcceptedText = document.createElement('p');
+ areBigNotesAcceptedText.innerText = 'Billetes grandes';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-check-green.svg';
+
+ areBigNotesAcceptedContainer.append(
+ areBigNotesAcceptedIcon,
+ areBigNotesAcceptedText,
+ checkIcon
+ );
+ } else {
+ const areBigNotesAcceptedIcon = document.createElement('img');
+ areBigNotesAcceptedIcon.src = '/img/eur-bill-gray.svg';
+ const areBigNotesAcceptedText = document.createElement('p');
+ areBigNotesAcceptedText.innerText = 'Billetes grandes';
+ const checkIcon = document.createElement('img');
+ checkIcon.src = '/img/circle-xmark-gray.svg';
+
+ areBigNotesAcceptedContainer.append(
+ areBigNotesAcceptedIcon,
+ areBigNotesAcceptedText,
+ checkIcon
+ );
+ }
+
+ otherOfferFeaturesDiv.append(areBigNotesAcceptedContainer);
+
+ const actionButtonsArea = document.createElement('p');
+ actionButtonsArea.classList.add('offer-action-buttons-area');
+
+ const editActionArea = document.createElement('div');
+ editActionArea.classList.add('offer-action-area');
+ editActionArea.classList.add('subtle-box');
+ const editActionIcon = document.createElement('img');
+ editActionIcon.src = '/img/edit.svg';
+ const editActionText = document.createElement('p');
+ editActionText.innerText = 'Editar';
+ editActionArea.append(editActionIcon, editActionText);
+
+ const deleteActionArea = document.createElement('div');
+ deleteActionArea.classList.add('offer-action-area');
+ deleteActionArea.classList.add('subtle-box');
+ const deleteActionIcon = document.createElement('img');
+ deleteActionIcon.src = '/img/trash-can-darkred.svg';
+ const deleteActionText = document.createElement('p');
+ deleteActionText.innerText = 'Eliminar';
+ deleteActionArea.append(deleteActionIcon, deleteActionText);
+ deleteActionArea.addEventListener('click', async () => {
+ await this.deleteButtonCallback();
+ });
+
+ actionButtonsArea.append(editActionArea, deleteActionArea);
+
+ offerCard.append(
+ tradeDescDiv,
+ premiumDescDiv,
+ whereDescDiv,
+ whenDescDiv,
+ bitcoinMethodsDiv,
+ visibilityDiv,
+ otherOfferFeaturesDiv,
+ actionButtonsArea
+ );
+
+ return offerCard;
+ }
+}
+
+module.exports = OfferCard;
diff --git a/src/front/components/PlaceInput.js b/src/front/components/PlaceInput.js
new file mode 100644
index 0000000..3d573b2
--- /dev/null
+++ b/src/front/components/PlaceInput.js
@@ -0,0 +1,26 @@
+class PlaceInput {
+ constructor({ parentElement, id }) {
+ this.element = null;
+ this.parentElement = parentElement;
+ this.id = id;
+ }
+
+ render() {
+ const placeInput = document.createElement('textarea');
+ placeInput.id = this.id;
+ placeInput.className = 'place-and-time-box';
+ placeInput.autocomplete = 'on';
+ placeInput.maxLength = 140;
+ placeInput.placeholder =
+ "¿Dónde? Ej.'Eixample', 'La Maquinista', 'Cualquier lugar en BCN', 'Meetup BBO'";
+
+ this.element = placeInput;
+ this.parentElement.appendChild(this.element);
+ }
+
+ get inputText() {
+ return this.element.value;
+ }
+}
+
+module.exports = PlaceInput;
diff --git a/src/front/components/PopupNotification.js b/src/front/components/PopupNotification.js
index e2a0588..8604184 100644
--- a/src/front/components/PopupNotification.js
+++ b/src/front/components/PopupNotification.js
@@ -9,7 +9,7 @@ class PopupNotification {
render() {
const div = document.createElement('div');
div.id = this.id;
- div.className = 'top-notification-good';
+ div.className = 'top-notification-good max-size-zero';
div.innerHTML = `
${this.text}
`; @@ -21,5 +21,19 @@ class PopupNotification { display() { this.element.classList.add('revealed'); } + + displayTemporarily(milliseconds) { + if (!milliseconds) { + milliseconds = 1000; + } + this.element.classList.remove('max-size-zero'); + this.element.classList.add('revealed'); + setTimeout(() => { + this.element.classList.remove('revealed'); + }, milliseconds); + setTimeout(() => { + this.element.classList.add('max-size-zero'); + }, milliseconds + 1000); + } } module.exports = PopupNotification; diff --git a/src/front/components/PremiumSelector.js b/src/front/components/PremiumSelector.js index d2defdd..68f994d 100644 --- a/src/front/components/PremiumSelector.js +++ b/src/front/components/PremiumSelector.js @@ -11,9 +11,10 @@ class PremiumSelector { render() { const premiumSelectorArea = document.createElement('div'); premiumSelectorArea.id = this.id; + premiumSelectorArea.classList = 'premium-selector-area'; const premiumValue = document.createElement('div'); - premiumValue.id = 'premium-value'; + premiumValue.className = 'premium-value'; premiumValue.textContent = '0%'; this.premiumValue = premiumValue; @@ -22,12 +23,12 @@ class PremiumSelector { const increaseButton = document.createElement('button'); increaseButton.classList.add('premium-button'); - increaseButton.id = 'button-increase-premium'; + increaseButton.classList.add('button-increase-premium'); increaseButton.textContent = '+'; const decreaseButton = document.createElement('button'); decreaseButton.classList.add('premium-button'); - decreaseButton.id = 'button-decrease-premium'; + decreaseButton.classList.add('button-decrease-premium'); decreaseButton.textContent = '-'; premiumButtonsContainer.appendChild(increaseButton); diff --git a/src/front/components/PriceDisplay.js b/src/front/components/PriceDisplay.js index 39eb16d..be3055d 100644 --- a/src/front/components/PriceDisplay.js +++ b/src/front/components/PriceDisplay.js @@ -16,10 +16,10 @@ class PriceDisplay { render() { const container = document.createElement('div'); - container.id = 'premium-price-display-area'; + container.id = this.id; + container.classList = 'premium-price-display-area'; const offerParagraph = document.createElement('p'); - offerParagraph.id = 'offer-price-paragraph'; offerParagraph.textContent = 'Tu precio: '; const offerSpan = document.createElement('span'); @@ -30,11 +30,9 @@ class PriceDisplay { offerParagraph.append('€/BTC'); const marketParagraph = document.createElement('p'); - marketParagraph.id = 'market-price-paragraph'; marketParagraph.textContent = '(Precio mercado: '; const marketSpan = document.createElement('span'); - marketSpan.id = 'market-price'; this.marketPriceSpan = marketSpan; marketParagraph.appendChild(marketSpan); diff --git a/src/front/components/TimeInput.js b/src/front/components/TimeInput.js new file mode 100644 index 0000000..68f0451 --- /dev/null +++ b/src/front/components/TimeInput.js @@ -0,0 +1,26 @@ +class TimeInput { + constructor({ parentElement, id }) { + this.element = null; + this.parentElement = parentElement; + this.id = id; + } + + render() { + const timeInput = document.createElement('textarea'); + timeInput.id = this.id; + timeInput.className = 'place-and-time-box'; + timeInput.autocomplete = 'on'; + timeInput.maxLength = 140; + timeInput.placeholder = + '¿Cuándo? Ej."Cualquier hora", "19:00-21:00", "Finde"'; + + this.element = timeInput; + this.parentElement.appendChild(this.element); + } + + get inputText() { + return this.element.value; + } +} + +module.exports = TimeInput; diff --git a/src/front/components/TrustCheckboxes.js b/src/front/components/TrustCheckboxes.js new file mode 100644 index 0000000..f1b8768 --- /dev/null +++ b/src/front/components/TrustCheckboxes.js @@ -0,0 +1,111 @@ +class TrustCheckboxes { + constructor({ parentElement }) { + this.myTrustedContainer = null; + this.myTrustedCheckboxElement = null; + this.myTrustedTrustedContainer = null; + this.myTrustedTrustedCheckboxElement = null; + this.allMembersContainer = null; + this.allMembersCheckboxElement = null; + this.parentElement = parentElement; + } + + render() { + const checkboxesDetails = [ + { + label: 'Mis confiados', + containerProperty: 'myTrustedContainer', + checkboxProperty: 'myTrustedCheckboxElement', + defaultChecked: true, + isDisabled: true, + }, + { + label: 'Los confiados de mis confiados', + containerProperty: 'myTrustedTrustedContainer', + checkboxProperty: 'myTrustedTrustedCheckboxElement', + defaultChecked: true, + isDisabled: false, + }, + { + label: 'Todos los miembros', + containerProperty: 'allMembersContainer', + checkboxProperty: 'allMembersCheckboxElement', + defaultChecked: false, + isDisabled: false, + }, + ]; + + for (const checkboxDetails of checkboxesDetails) { + this[checkboxDetails.containerProperty] = this.buildCheckbox({ + label: checkboxDetails.label, + }); + + this[checkboxDetails.checkboxProperty] = + this[checkboxDetails.containerProperty].querySelector('input'); + + this[checkboxDetails.checkboxProperty].addEventListener('click', () => { + this.applyTrustCheckboxConstraints( + this[checkboxDetails.checkboxProperty] + ); + }); + + this[checkboxDetails.checkboxProperty].checked = + checkboxDetails.defaultChecked; + this[checkboxDetails.checkboxProperty].disabled = + checkboxDetails.isDisabled; + + this.parentElement.appendChild(this[checkboxDetails.containerProperty]); + } + } + + buildCheckbox({ label }) { + const checkboxContainer = document.createElement('div'); + checkboxContainer.className = 'checkbox-row'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + + const labelElement = document.createElement('label'); + labelElement.htmlFor = checkbox.id; + labelElement.textContent = label; + + checkboxContainer.appendChild(checkbox); + checkboxContainer.appendChild(labelElement); + + return checkboxContainer; + } + + applyTrustCheckboxConstraints(pressedCheckbox) { + if (pressedCheckbox === this.myTrustedTrustedCheckboxElement) { + if ( + !this.myTrustedTrustedCheckboxElement.checked && + this.allMembersCheckboxElement.checked + ) { + this.allMembersCheckboxElement.checked = false; + } + } + + if (pressedCheckbox === this.allMembersCheckboxElement) { + if ( + !this.myTrustedTrustedCheckboxElement.checked && + this.allMembersCheckboxElement.checked + ) { + this.myTrustedTrustedCheckboxElement.checked = true; + } + } + } + + get showOfferToTrusted() { + return this.myTrustedCheckboxElement.checked; + } + + get showOfferToTrustedTrusted() { + return this.myTrustedTrustedCheckboxElement.checked; + } + + get showOfferToAllMembers() { + return this.allMembersCheckboxElement.checked; + } +} + +module.exports = TrustCheckboxes; diff --git a/src/front/pages/invite.js b/src/front/pages/invite.js index 24bc3b5..d756ca3 100644 --- a/src/front/pages/invite.js +++ b/src/front/pages/invite.js @@ -24,7 +24,7 @@ const invitesFunction = () => { if (verifyResponse.ok) { signUpSuccessPopup.display(); setTimeout(() => { - window.location.href = constants.API_PATHS.createProfile; + window.location.href = constants.WEB_PATHS.createProfile; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/login.js b/src/front/pages/login.js index 5620a99..4b304e2 100644 --- a/src/front/pages/login.js +++ b/src/front/pages/login.js @@ -38,7 +38,7 @@ const loginFunction = () => { nostrLoginButton.disable(); successPopup.display(); setTimeout(() => { - window.location.href = constants.API_PATHS.home; + window.location.href = constants.WEB_PATHS.home; }, constants.DEFAULT_REDIRECT_DELAY); } }, diff --git a/src/front/pages/offers.js b/src/front/pages/offers.js index 1913e7a..01c6ab3 100644 --- a/src/front/pages/offers.js +++ b/src/front/pages/offers.js @@ -1,51 +1,32 @@ -const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces'); -const PublishOfferButton = require('../components/PublishOfferButton'); -const BuyOrSellButtonGroup = require('../components/BuyOrSellButtonGroup'); -const PremiumSelector = require('../components/PremiumSelector'); -const PriceDisplay = require('../components/PriceDisplay'); +const PopupNotification = require('../components/PopupNotification'); +const CreateOfferModal = require('../components/CreateOfferModal'); +const OfferCard = require('../components/OfferCard'); + +const offerService = require('../services/offerService'); function offersPage() { - const createOfferEventBus = new EventTarget(); + const offerCreatedPopup = new PopupNotification({ + parentElement: document.body, + text: '¡Oferta creada! Puedes verla en tus ofertas.', + }); + offerCreatedPopup.render(); - const publishOfferButton = new PublishOfferButton({ - parentElement: document.getElementById('submit-button-area'), - id: 'button-submit-offer', - onClickCallback: async () => { - await publishOffer(); + const offerDeletedPopup = new PopupNotification({ + parentElement: document.body, + text: '¡Oferta eliminada!', + }); + offerDeletedPopup.render(); + + const createOfferModal = new CreateOfferModal({ + parentElement: document.body, + onCreationCallback: async () => { await myOffers.getOffersFromApi(); await myOffers.render(); + offerCreatedPopup.displayTemporarily(3000); }, + offerService: offerService, }); - publishOfferButton.render(); - - const buyOrSellButtonGroup = new BuyOrSellButtonGroup({ - parentElement: document.getElementById('buy-or-sell-area'), - id: 'button-group-buy-or-sell', - }); - buyOrSellButtonGroup.render(); - - const premiumSelector = new PremiumSelector({ - parentElement: document.getElementById('premium-content-area'), - id: 'premium-selector-area', - eventSink: createOfferEventBus, - }); - premiumSelector.render(); - - const priceDisplay = new PriceDisplay({ - parentElement: document.getElementById('premium-content-area'), - id: 'premium-price-display-area', - premiumProvidingCallback: () => { - return premiumSelector.getPremium(); - }, - priceProvidingCallback: () => { - return Math.floor(Math.random() * (95000 - 70000 + 1) + 70000); - }, - }); - priceDisplay.render(); - createOfferEventBus.addEventListener('premium-changed', () => { - priceDisplay.updatePrices(); - }); - + createOfferModal.render(); // ----------- const navbuttonHome = document.getElementById('navbutton-home'); const navbuttonOffers = document.getElementById('navbutton-offers'); @@ -62,550 +43,15 @@ function offersPage() { 'button-start-create-offer' ); const buttonViewMyOffers = document.getElementById('button-view-my-offers'); - const closeOffer = document.getElementById('close-offer'); - const createOfferModalRoot = document.getElementById( - 'create-offer-modal-root' - ); const viewMyOffersRoot = document.getElementById('view-my-offers-root'); - const eurAmountInput = document.getElementById('input-eur-amount'); - const btcAmountInput = document.getElementById('input-btc-amount'); - - const placeInput = document.getElementById('place-input'); - const timeInput = document.getElementById('time-input'); - - const onchainCheckbox = document.getElementById('onchain-checkbox'); - const lightningCheckbox = document.getElementById('lightning-checkbox'); - - const btcMethodCheckboxes = [onchainCheckbox, lightningCheckbox]; - - const myTrustedCheckbox = document.getElementById('my-trusted-checkbox'); - const myTrustedTrustedCheckbox = document.getElementById( - 'my-trusted-trusted-checkbox' - ); - const allMembersCheckbox = document.getElementById('all-members-checkbox'); - - const bigNotesAcceptedCheckbox = document.getElementById( - 'large-bills-checkbox' - ); - - const offerCreatedPopup = document.getElementById( - 'offer-created-confirmation' - ); - const offerDeletedPopup = document.getElementById( - 'offer-deleted-confirmation' - ); - const ownOffersContainer = document.getElementById('own-offers-container'); - function toggleCreateOfferModal() { - createOfferModalRoot.classList.toggle('shown'); - } - function toggleViewMyOffersPanel() { viewMyOffersRoot.style.display = viewMyOffersRoot.style.display === 'block' ? 'none' : 'block'; } - function readIntFromEurAmountInput() { - const eurAmountFieldValue = eurAmountInput.value; - const regularExpression = /([\d\s]+)/; - const matchResult = eurAmountFieldValue.match(regularExpression); - - if (!matchResult) { - return null; - } - - const numberString = matchResult[1]; - const cleanInputNumber = parseInt(numberString.replace(/\s/gi, '')); - - return cleanInputNumber; - } - - function validateAndFormatEurAmountInput() { - const cleanInputNumber = readIntFromEurAmountInput(); - eurAmountInput.classList.remove('input-is-valid', 'input-is-invalid'); - if (cleanInputNumber) { - eurAmountInput.value = formatNumberWithSpaces(cleanInputNumber); - eurAmountInput.classList.add('input-is-valid'); - return; - } - - eurAmountInput.classList.add('input-is-invalid'); - } - - function updateBtcInput() { - const eurToSatRate = 1021; - const cleanEurAmount = readIntFromEurAmountInput(); - - const satsAmount = cleanEurAmount * eurToSatRate; - const formattedSatsAmount = formatNumberWithSpaces(satsAmount); - btcAmountInput.value = formattedSatsAmount; - } - - function validateBitcoinMethodCheckboxes(clickedCheckbox) { - let checkedCount = btcMethodCheckboxes.filter((cb) => cb.checked).length; - if (checkedCount === 0) { - clickedCheckbox.checked = true; - } - } - - function applyTrustCheckboxConstraints(pressedCheckbox) { - if (pressedCheckbox === myTrustedTrustedCheckbox) { - console.log('first case!'); - if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) { - allMembersCheckbox.checked = false; - } - } - - if (pressedCheckbox === allMembersCheckbox) { - console.log('second case!'); - if (!myTrustedTrustedCheckbox.checked && allMembersCheckbox.checked) { - myTrustedTrustedCheckbox.checked = true; - } - } - } - - async function publishOffer() { - const wants = buyOrSellButtonGroup.wants(); - - const premium = premiumSelector.getPremium(); - const trade_amount_eur = eurAmountInput.value; - const location_details = placeInput.value; - const time_availability_details = timeInput.value; - const is_onchain_accepted = onchainCheckbox.checked; - const is_lightning_accepted = lightningCheckbox.checked; - const show_offer_to_trusted = myTrustedCheckbox.checked; - const show_offer_to_trusted_trusted = myTrustedTrustedCheckbox.checked; - const show_offer_to_all_members = allMembersCheckbox.checked; - const are_big_notes_accepted = bigNotesAcceptedCheckbox.checked; - - const offerDetails = { - wants, - premium, - trade_amount_eur, - location_details, - time_availability_details, - is_onchain_accepted, - is_lightning_accepted, - show_offer_to_trusted, - show_offer_to_trusted_trusted, - show_offer_to_all_members, - are_big_notes_accepted, - }; - - await fetch('/api/offer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ offerDetails }), - }); - - toggleOfferCreatedAlert(); - - toggleCreateOfferModal(); - } - - function toggleOfferCreatedAlert() { - offerCreatedPopup.classList.remove('max-size-zero'); - offerCreatedPopup.classList.add('revealed'); - setTimeout(() => { - offerCreatedPopup.classList.remove('revealed'); - }, 3000); - setTimeout(() => { - offerCreatedPopup.classList.add('max-size-zero'); - }, 4000); - } - - function toggleOfferDeletedAlert() { - offerDeletedPopup.classList.remove('max-size-zero'); - offerDeletedPopup.classList.add('revealed'); - setTimeout(() => { - offerDeletedPopup.classList.remove('revealed'); - }, 3000); - setTimeout(() => { - offerDeletedPopup.classList.add('max-size-zero'); - }, 4000); - } - - class Offer { - constructor(offerData) { - this.uuid = offerData.uuid; - this.public_key = offerData.public_key; - this.wants = offerData.wants; - this.premium = offerData.premium; - this.trade_amount_eur = offerData.trade_amount_eur; - this.location_details = offerData.location_details; - this.time_availability_details = offerData.time_availability_details; - this.show_offer_to_trusted = offerData.show_offer_to_trusted; - this.show_offer_to_trusted_trusted = - offerData.show_offer_to_trusted_trusted; - this.show_offer_to_all_members = offerData.show_offer_to_all_members; - this.is_onchain_accepted = offerData.is_onchain_accepted; - this.is_lightning_accepted = offerData.is_lightning_accepted; - this.are_big_notes_accepted = offerData.are_big_notes_accepted; - this.created_at = offerData.created_at; - this.last_updated_at = offerData.last_updated_at; - } - - buildHTML() { - const offerCard = document.createElement('div'); - offerCard.classList.add('myoffer-card'); - offerCard.classList.add('shadowed-round-area'); - - const tradeDescDiv = document.createElement('div'); - tradeDescDiv.classList.add('trade-desc'); - - const youBuyText = document.createElement('p'); - youBuyText.classList.add('offer-card-content-title'); - youBuyText.innerText = 'Compras'; - tradeDescDiv.append(youBuyText); - - const youBuyData = document.createElement('p'); - youBuyData.classList.add('offer-card-content-data'); - if (this.wants === 'BTC') { - youBuyData.innerText = `${this.trade_amount_eur * 1021} sats`; - } - if (this.wants === 'EUR') { - youBuyData.innerText = `${this.trade_amount_eur} €`; - } - tradeDescDiv.append(youBuyData); - - const youSellText = document.createElement('p'); - youSellText.id = 'you-sell-title'; - youSellText.classList.add('offer-card-content-title'); - youSellText.innerText = 'Vendes'; - tradeDescDiv.append(youSellText); - - const youSellData = document.createElement('p'); - youSellData.classList.add('offer-card-content-data'); - if (this.wants === 'BTC') { - youSellData.innerText = `${this.trade_amount_eur} €`; - } - if (this.wants === 'EUR') { - youSellData.innerText = `${this.trade_amount_eur * 1021} sats`; - } - tradeDescDiv.append(youSellData); - - const premiumDescDiv = document.createElement('div'); - premiumDescDiv.classList.add('premium-desc'); - - const premiumTitle = document.createElement('p'); - premiumTitle.classList.add('offer-card-content-title'); - premiumTitle.innerText = 'Premium'; - premiumDescDiv.append(premiumTitle); - - const premiumData = document.createElement('p'); - premiumData.classList.add('offer-card-content-data'); - premiumData.innerText = `${this.premium * 100} %`; - premiumDescDiv.append(premiumData); - - const offerPriceTitle = document.createElement('p'); - offerPriceTitle.classList.add('offer-card-content-title'); - offerPriceTitle.innerText = 'Precio oferta'; - premiumDescDiv.append(offerPriceTitle); - - const offerPriceData = document.createElement('p'); - offerPriceData.classList.add('offer-card-content-data'); - offerPriceData.innerText = `90000 €/BTC`; - premiumDescDiv.append(offerPriceData); - - const marketPriceTitle = document.createElement('p'); - marketPriceTitle.classList.add('offer-card-content-title'); - marketPriceTitle.innerText = 'Precio mercado'; - premiumDescDiv.append(marketPriceTitle); - - const marketPriceData = document.createElement('p'); - marketPriceData.innerText = `88000 €/BTC`; - premiumDescDiv.append(marketPriceData); - - const whereDescDiv = document.createElement('div'); - whereDescDiv.classList.add('where-desc'); - - const whereDescTitle = document.createElement('p'); - whereDescTitle.classList.add('offer-card-content-title'); - whereDescTitle.innerText = 'Dónde'; - whereDescDiv.append(whereDescTitle); - - const whereDescData = document.createElement('p'); - whereDescData.classList.add('offer-long-text'); - whereDescData.innerText = `${this.location_details}`; - whereDescDiv.append(whereDescData); - - const whenDescDiv = document.createElement('div'); - whenDescDiv.classList.add('when-desc'); - - const whenDescTitle = document.createElement('p'); - whenDescTitle.classList.add('offer-card-content-title'); - whenDescTitle.innerText = 'Cúando'; - whenDescDiv.append(whenDescTitle); - - const whenDescData = document.createElement('p'); - whenDescData.classList.add('offer-long-text'); - whenDescData.innerText = `${this.time_availability_details}`; - whenDescDiv.append(whenDescData); - - const bitcoinMethodsDiv = document.createElement('div'); - bitcoinMethodsDiv.classList.add('bitcoin-methods-desc'); - - const bitcoinMethodsTitle = document.createElement('p'); - bitcoinMethodsTitle.classList.add('offer-card-content-title'); - bitcoinMethodsTitle.innerText = 'Protocolos Bitcoin aceptados'; - bitcoinMethodsDiv.append(bitcoinMethodsTitle); - - const onchainAcceptedContainer = document.createElement('div'); - onchainAcceptedContainer.classList.add('left-icon-checkboxed-field'); - if (this.is_onchain_accepted) { - const onchainIcon = document.createElement('img'); - onchainIcon.src = '/img/chains-lasecagold.svg'; - const onchainText = document.createElement('p'); - onchainText.innerText = 'Onchain'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); - } else { - const onchainIcon = document.createElement('img'); - onchainIcon.src = '/img/chains-gray.svg'; - const onchainText = document.createElement('p'); - onchainText.innerText = 'Onchain'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - onchainAcceptedContainer.append(onchainIcon, onchainText, checkIcon); - } - const lightningAcceptedContainer = document.createElement('div'); - - lightningAcceptedContainer.classList.add('left-icon-checkboxed-field'); - if (this.is_lightning_accepted) { - const lightningIcon = document.createElement('img'); - lightningIcon.src = '/img/bolt-lightning-lasecagold.svg'; - const lightningText = document.createElement('p'); - lightningText.innerText = 'Lightning'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - lightningAcceptedContainer.append( - lightningIcon, - lightningText, - checkIcon - ); - } else { - const lightningIcon = document.createElement('img'); - lightningIcon.src = '/img/bolt-lightning-gray.svg'; - const lightningText = document.createElement('p'); - lightningText.innerText = 'Lightning'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - lightningAcceptedContainer.append( - lightningIcon, - lightningText, - checkIcon - ); - } - - bitcoinMethodsDiv.append( - onchainAcceptedContainer, - lightningAcceptedContainer - ); - - const visibilityDiv = document.createElement('div'); - visibilityDiv.classList.add('visibility-desc'); - - const visibilityTitle = document.createElement('p'); - visibilityTitle.classList.add('offer-card-content-title'); - visibilityTitle.innerText = 'Visibilidad'; - visibilityDiv.append(visibilityTitle); - - const showOfferToTrustedContainer = document.createElement('div'); - showOfferToTrustedContainer.classList.add('right-icon-checkboxed-field'); - - if (this.show_offer_to_trusted) { - const showOfferToTrustedIcon = document.createElement('img'); - showOfferToTrustedIcon.src = '/img/user-lasecagold.svg'; - const showOfferToTrustedText = document.createElement('p'); - showOfferToTrustedText.innerText = 'Confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToTrustedContainer.append( - showOfferToTrustedIcon, - showOfferToTrustedText, - checkIcon - ); - } else { - const showOfferToTrustedIcon = document.createElement('img'); - showOfferToTrustedIcon.src = '/img/user-gray.svg'; - const showOfferToTrustedText = document.createElement('p'); - showOfferToTrustedText.innerText = 'Confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToTrustedContainer.append( - showOfferToTrustedIcon, - showOfferToTrustedText, - checkIcon - ); - } - - const showOfferToTrustedTrustedContainer = document.createElement('div'); - showOfferToTrustedTrustedContainer.classList.add( - 'right-icon-checkboxed-field' - ); - - if (this.show_offer_to_trusted_trusted) { - const showOfferToTrustedTrustedIcon = document.createElement('img'); - showOfferToTrustedTrustedIcon.src = '/img/user-group-lasecagold.svg'; - const showOfferToTrustedTrustedText = document.createElement('p'); - showOfferToTrustedTrustedText.innerText = 'Sus confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToTrustedTrustedContainer.append( - showOfferToTrustedTrustedIcon, - showOfferToTrustedTrustedText, - checkIcon - ); - } else { - const showOfferToTrustedTrustedIcon = document.createElement('img'); - showOfferToTrustedTrustedIcon.src = '/img/user-group-gray.svg'; - const showOfferToTrustedTrustedText = document.createElement('p'); - showOfferToTrustedTrustedText.innerText = 'Sus confiados'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToTrustedTrustedContainer.append( - showOfferToTrustedTrustedIcon, - showOfferToTrustedTrustedText, - checkIcon - ); - } - - const showOfferToAllMembersContainer = document.createElement('div'); - showOfferToAllMembersContainer.classList.add( - 'right-icon-checkboxed-field' - ); - - if (this.show_offer_to_all_members) { - const showOfferToAllMembersIcon = document.createElement('img'); - showOfferToAllMembersIcon.src = '/img/many-users-lasecagold.svg'; - const showOfferToAllMembersText = document.createElement('p'); - showOfferToAllMembersText.innerText = 'Todos'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - showOfferToAllMembersContainer.append( - showOfferToAllMembersIcon, - showOfferToAllMembersText, - checkIcon - ); - } else { - const showOfferToAllMembersIcon = document.createElement('img'); - showOfferToAllMembersIcon.src = '/img/many-users-gray.svg'; - const showOfferToAllMembersText = document.createElement('p'); - showOfferToAllMembersText.innerText = 'Todos'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - showOfferToAllMembersContainer.append( - showOfferToAllMembersIcon, - showOfferToAllMembersText, - checkIcon - ); - } - visibilityDiv.append( - showOfferToTrustedContainer, - showOfferToTrustedTrustedContainer, - showOfferToAllMembersContainer - ); - - const otherOfferFeaturesDiv = document.createElement('div'); - otherOfferFeaturesDiv.classList.add('other-desc'); - - const otherOfferFeaturesTitle = document.createElement('p'); - otherOfferFeaturesTitle.classList.add('offer-card-content-title'); - otherOfferFeaturesTitle.innerText = 'Otros'; - otherOfferFeaturesDiv.append(otherOfferFeaturesTitle); - - const areBigNotesAcceptedContainer = document.createElement('div'); - areBigNotesAcceptedContainer.classList.add('left-icon-checkboxed-field'); - - if (this.are_big_notes_accepted) { - const areBigNotesAcceptedIcon = document.createElement('img'); - areBigNotesAcceptedIcon.src = '/img/eur-bill-lasecagold.svg'; - const areBigNotesAcceptedText = document.createElement('p'); - areBigNotesAcceptedText.innerText = 'Billetes grandes'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-check-green.svg'; - - areBigNotesAcceptedContainer.append( - areBigNotesAcceptedIcon, - areBigNotesAcceptedText, - checkIcon - ); - } else { - const areBigNotesAcceptedIcon = document.createElement('img'); - areBigNotesAcceptedIcon.src = '/img/eur-bill-gray.svg'; - const areBigNotesAcceptedText = document.createElement('p'); - areBigNotesAcceptedText.innerText = 'Billetes grandes'; - const checkIcon = document.createElement('img'); - checkIcon.src = '/img/circle-xmark-gray.svg'; - - areBigNotesAcceptedContainer.append( - areBigNotesAcceptedIcon, - areBigNotesAcceptedText, - checkIcon - ); - } - - otherOfferFeaturesDiv.append(areBigNotesAcceptedContainer); - - const actionButtonsArea = document.createElement('p'); - actionButtonsArea.classList.add('offer-action-buttons-area'); - - const editActionArea = document.createElement('div'); - editActionArea.classList.add('offer-action-area'); - editActionArea.classList.add('subtle-box'); - const editActionIcon = document.createElement('img'); - editActionIcon.src = '/img/edit.svg'; - const editActionText = document.createElement('p'); - editActionText.innerText = 'Editar'; - editActionArea.append(editActionIcon, editActionText); - - const deleteActionArea = document.createElement('div'); - deleteActionArea.classList.add('offer-action-area'); - deleteActionArea.classList.add('subtle-box'); - const deleteActionIcon = document.createElement('img'); - deleteActionIcon.src = '/img/trash-can-darkred.svg'; - const deleteActionText = document.createElement('p'); - deleteActionText.innerText = 'Eliminar'; - deleteActionArea.append(deleteActionIcon, deleteActionText); - deleteActionArea.addEventListener('click', async () => { - await deleteOfferByUuid(this.uuid); - await myOffers.getOffersFromApi(); - await myOffers.render(); - toggleOfferDeletedAlert(); - }); - - actionButtonsArea.append(editActionArea, deleteActionArea); - - offerCard.append( - tradeDescDiv, - premiumDescDiv, - whereDescDiv, - whenDescDiv, - bitcoinMethodsDiv, - visibilityDiv, - otherOfferFeaturesDiv, - actionButtonsArea - ); - - return offerCard; - } - } - class MyOffers { constructor(ownOffersContainerElement) { this.ownOffersContainerElement = ownOffersContainerElement; @@ -620,7 +66,17 @@ function offersPage() { const offersData = (await offersResponse.json()).data; if (offersResponse.ok) { for (const record of offersData) { - this.offers.push(new Offer(record)); + this.offers.push( + new OfferCard({ + offerData: record, + deleteButtonCallback: async () => { + await offerService.deleteOffer(record.uuid); + await this.getOffersFromApi(); + await this.render(); + offerDeletedPopup.displayTemporarily(3000); + }, + }) + ); } } } @@ -639,17 +95,8 @@ function offersPage() { } } - async function deleteOfferByUuid(offerUuid) { - await fetch(`/api/offer/${offerUuid}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }); - } - buttonStartCreateOffer.addEventListener('click', () => { - toggleCreateOfferModal(); + createOfferModal.toggle(); }); buttonViewMyOffers.addEventListener('click', async () => { @@ -658,36 +105,6 @@ function offersPage() { toggleViewMyOffersPanel(); }); - closeOffer.addEventListener('click', () => { - toggleCreateOfferModal(); - }); - - eurAmountInput.addEventListener('blur', () => { - validateAndFormatEurAmountInput(); - updateBtcInput(); - }); - - eurAmountInput.addEventListener('input', () => { - eurAmountInput.value = eurAmountInput.value.replace(/[^0-9]/g, ''); - updateBtcInput(); - }); - - for (const btcMethodCheckbox of btcMethodCheckboxes) { - btcMethodCheckbox.addEventListener('click', () => { - validateBitcoinMethodCheckboxes(btcMethodCheckbox); - }); - } - - myTrustedTrustedCheckbox.addEventListener('click', () => { - applyTrustCheckboxConstraints(myTrustedTrustedCheckbox); - }); - - allMembersCheckbox.addEventListener('click', () => { - applyTrustCheckboxConstraints(allMembersCheckbox); - }); - - updateBtcInput(); - const myOffers = new MyOffers(ownOffersContainer); } diff --git a/src/front/services/inviteService.js b/src/front/services/inviteService.js index d494c6c..590913e 100644 --- a/src/front/services/inviteService.js +++ b/src/front/services/inviteService.js @@ -1,7 +1,9 @@ +const constants = require('../../constants'); + const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let challengeResponse; try { - challengeResponse = await fetch('/api/signup/nostr-challenge', { + challengeResponse = await fetch(constants.API_PATHS.signupNostrChallenge, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -39,7 +41,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => { let verifyResponse; try { - verifyResponse = await fetch('/api/signup/nostr-verify', { + verifyResponse = await fetch(constants.API_PATHS.signupNostrVerify, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(signedEvent), diff --git a/src/front/services/loginService.js b/src/front/services/loginService.js index d1f5815..852b438 100644 --- a/src/front/services/loginService.js +++ b/src/front/services/loginService.js @@ -1,15 +1,12 @@ +const constants = require('../../constants'); + const requestAndRespondLoginChallenge = async ({ onRejectedPubKeyCallback, onRejectedSignatureCallback, }) => { - onRejectedPubKeyCallback = () => { - document.querySelector('#rejected-nostr-nudges').style.display = 'block'; - }; - onRejectedSignatureCallback = onRejectedPubKeyCallback; - let challengeResponse; try { - challengeResponse = await fetch('/api/login/nostr-challenge', { + challengeResponse = await fetch(constants.API_PATHS.loginNostrChallenge, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -47,7 +44,7 @@ const requestAndRespondLoginChallenge = async ({ let verifyResponse; try { - verifyResponse = await fetch('/api/login/nostr-verify', { + verifyResponse = await fetch(constants.API_PATHS.loginNostrVerify, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(signedEvent), diff --git a/src/front/services/offerService.js b/src/front/services/offerService.js new file mode 100644 index 0000000..591a0f0 --- /dev/null +++ b/src/front/services/offerService.js @@ -0,0 +1,25 @@ +const constants = require('../../constants'); + +const createOffer = async (offerDetails) => { + await fetch(constants.API_PATHS.offer, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ offerDetails }), + }); +}; + +const deleteOffer = async (offerUuid) => { + await fetch(`${constants.API_PATHS.offer}/${offerUuid}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +module.exports = { + createOffer, + deleteOffer, +}; diff --git a/src/views/offers.ejs b/src/views/offers.ejs index aba756c..6fa2e9f 100644 --- a/src/views/offers.ejs +++ b/src/views/offers.ejs @@ -31,145 +31,6 @@Vaya, no hay nada por aquí...
-¡Oferta creada! Puedes verla en tus ofertas.
-¡Oferta eliminada!
-