Compare commits
No commits in common. "master" and "without-awilix" have entirely different histories.
master
...
without-aw
|
|
@ -1 +0,0 @@
|
||||||
public/javascript/*
|
|
||||||
6
.gitignore
vendored
|
|
@ -136,10 +136,10 @@ dist
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
|
||||||
|
test-results/*
|
||||||
|
|
||||||
|
# Playwright
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
|
||||||
# webpack bundles
|
|
||||||
/public/javascript/*
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
config: path.resolve('src', 'database', 'config.js'),
|
|
||||||
'migrations-path': path.resolve('src', 'database', 'migrations'),
|
|
||||||
};
|
|
||||||
10
AGENTS.md
|
|
@ -1,10 +0,0 @@
|
||||||
# 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/`.
|
|
||||||
|
|
||||||
22
Dockerfile
|
|
@ -1,22 +1,10 @@
|
||||||
FROM debian:12
|
FROM debian:latest
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN apt-get update
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl gnupg2 ca-certificates lsb-release apt-transport-https \
|
||||||
RUN apt-get install -y \
|
postgresql caddy nodejs npm && \
|
||||||
curl gnupg2 ca-certificates lsb-release apt-transport-https
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN apt-get install -y \
|
|
||||||
postgresql
|
|
||||||
|
|
||||||
RUN apt-get install -y \
|
|
||||||
caddy
|
|
||||||
|
|
||||||
RUN apt-get install -y \
|
|
||||||
nodejs npm
|
|
||||||
|
|
||||||
|
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN echo "listen_addresses='*'" >> /etc/postgresql/15/main/postgresql.conf && \
|
RUN echo "listen_addresses='*'" >> /etc/postgresql/15/main/postgresql.conf && \
|
||||||
echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/15/main/pg_hba.conf && \
|
echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/15/main/pg_hba.conf && \
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,11 @@ laseca is a social bitcoin to cash exchange, implemented as a webapp.
|
||||||
* Installing
|
* Installing
|
||||||
+ Run `npm install`
|
+ Run `npm install`
|
||||||
+ You can now start the app in a container by running `npm run start:container` (and shut it down with `npm run stop:container`).
|
+ You can now start the app in a container by running `npm run start:container` (and shut it down with `npm run stop:container`).
|
||||||
* Building
|
|
||||||
+ The front-end code gets built with webpack. You can build it anytime with `npm run build`.
|
|
||||||
+ For development, it's useful to build continuously. You can run `npm run watch` and webpack will build every time you edit a monitored file.
|
|
||||||
* Running
|
* Running
|
||||||
+ Copy the `.env.dist` file into `.env` and set any values you like.
|
+ Copy the `.env.dist` file into `.env` and set any values you like.
|
||||||
+ The app will run in a single container, with a Postgres database, a caddy webserver and the nodejs app.
|
+ The app will run in a single container, with a Postgres database, a caddy webserver and the nodejs app.
|
||||||
+ Note that the container doesn't come with a volume for Postgres: default behaviour is to start from scratch every time you create the container, delete everything every time you delete the container.
|
+ Note that the container doesn't come with a volume for Postgres: default behaviour is to start from scratch every time you create the container, delete everything every time you delete the container.
|
||||||
+ You probably want to run migrations to get the database into proper state. You can do so with `npx sequelize-cli db:migrate`.
|
|
||||||
+ The docker image launches the nodejs app with nodemon, so changes to the code will be available immediately.
|
+ The docker image launches the nodejs app with nodemon, so changes to the code will be available immediately.
|
||||||
+ Furthermore, since the git repository gets mounted live into the docker container, the live changes made by webpack watch mode will also be available as you work on front end files.
|
|
||||||
+ The Postgres database is reachable from the host, so you can use your favourite SQL client to access it.
|
+ The Postgres database is reachable from the host, so you can use your favourite SQL client to access it.
|
||||||
+ You can format with `npm run format` and lint with `npm run lint`.
|
+ You can format with `npm run format` and lint with `npm run lint`.
|
||||||
|
|
||||||
|
|
|
||||||
1948
package-lock.json
generated
12
package.json
|
|
@ -20,13 +20,10 @@
|
||||||
"start": "node src/app.js",
|
"start": "node src/app.js",
|
||||||
"start:container": "docker compose up -d --build",
|
"start:container": "docker compose up -d --build",
|
||||||
"stop:container": "docker compose down",
|
"stop:container": "docker compose down",
|
||||||
"migrate": "npx sequelize-cli db:migrate",
|
|
||||||
"build": "webpack",
|
|
||||||
"watch": "webpack --watch",
|
|
||||||
"cli": "node src/cli.js",
|
"cli": "node src/cli.js",
|
||||||
|
"test": "playwright test",
|
||||||
"lint": "eslint . --fix",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
@ -39,9 +36,6 @@
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"playwright": "^1.50.1",
|
"playwright": "^1.50.1",
|
||||||
"prettier": "^3.5.1",
|
"prettier": "^3.5.1",
|
||||||
"prettier-plugin-ejs": "^1.0.3",
|
"prettier-plugin-ejs": "^1.0.3"
|
||||||
"sequelize-cli": "^6.6.2",
|
|
||||||
"webpack": "^5.98.0",
|
|
||||||
"webpack-cli": "^6.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
@media (max-width: 768px) {
|
|
||||||
#login-button {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-card-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
#login-button {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-card-content {
|
|
||||||
width: 40%;
|
|
||||||
min-width: min-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-card-content {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-card-content > * {
|
|
||||||
margin: 1vh auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
@ -21,7 +21,7 @@ function createApp(dependencies) {
|
||||||
app.use('/', dependencies.webRoutes);
|
app.use('/', dependencies.webRoutes);
|
||||||
app.use('/api', dependencies.apiRoutes);
|
app.use('/api', dependencies.apiRoutes);
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
app.disable('etag'); //avoids 304 responses
|
app.disable('etag'); //avoids 304 responses
|
||||||
|
|
||||||
|
|
|
||||||
19
src/associations.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
class AssociationsDefiner {
|
||||||
|
constructor({ models, DataTypes }) {
|
||||||
|
this.models = models;
|
||||||
|
this.DataTypes = DataTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
define() {
|
||||||
|
this.models.OfferCreated.hasOne(this.models.OfferDeleted);
|
||||||
|
this.models.OfferDeleted.belongsTo(this.models.OfferCreated, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'offer_uuid',
|
||||||
|
type: this.DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AssociationsDefiner;
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;
|
const DEFAULT_SESSION_DURATION_SECONDS = 60 * 60 * 24 * 30;
|
||||||
const DEFAULT_NOSTR_CHALLENGE_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_PATHS = {
|
|
||||||
createProfile: '/createProfile',
|
|
||||||
home: '/home',
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_SESSION_DURATION_SECONDS,
|
DEFAULT_SESSION_DURATION_SECONDS,
|
||||||
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS,
|
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS,
|
||||||
API_PATHS,
|
|
||||||
DEFAULT_REDIRECT_DELAY,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
class AssociationsDefiner {
|
|
||||||
constructor({ models, DataTypes }) {
|
|
||||||
this.models = models;
|
|
||||||
this.DataTypes = DataTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
define() {
|
|
||||||
this.models.NostrChallengeCreated.hasOne(
|
|
||||||
this.models.NostrChallengeCompleted,
|
|
||||||
{
|
|
||||||
foreignKey: 'challenge',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.models.NostrChallengeCompleted.belongsTo(
|
|
||||||
this.models.NostrChallengeCreated,
|
|
||||||
{
|
|
||||||
foreignKey: {
|
|
||||||
name: 'challenge',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.models.OfferCreated.hasOne(this.models.OfferDeleted, {
|
|
||||||
foreignKey: 'offer_uuid',
|
|
||||||
});
|
|
||||||
this.models.OfferDeleted.belongsTo(this.models.OfferCreated, {
|
|
||||||
foreignKey: {
|
|
||||||
name: 'offer_uuid',
|
|
||||||
type: this.DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = AssociationsDefiner;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
const dotenv = require('dotenv');
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
development: {
|
|
||||||
dialect: 'postgres',
|
|
||||||
host: process.env.POSTGRES_HOST,
|
|
||||||
port: 5432,
|
|
||||||
database: process.env.POSTGRES_DB,
|
|
||||||
username: process.env.POSTGRES_USER,
|
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
|
||||||
logging: console.log,
|
|
||||||
define: {
|
|
||||||
timestamps: false,
|
|
||||||
freezeTableName: true,
|
|
||||||
underscored: true,
|
|
||||||
quoteIdentifiers: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,379 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
up: (queryInterface, Sequelize) => {
|
|
||||||
return queryInterface.sequelize.transaction((t) => {
|
|
||||||
return Promise.all([
|
|
||||||
queryInterface.createTable(
|
|
||||||
'app_invite_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
inviter_pub_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'contact_details_set',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
encrypted_contact_details: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'nostr_challenge_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
challenge: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
expires_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'login_challenge_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
nostr_challenge_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'nostr_challenge_completed',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
challenge: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
signed_event: {
|
|
||||||
type: Sequelize.JSONB,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'login_challenge_completed',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
nostr_challenge_completed_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'nym_set',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
nym: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'offer_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'offer_deleted',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
offer_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'offer_details_set',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
offer_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
wants: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
premium: {
|
|
||||||
type: Sequelize.DECIMAL(5, 2),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
trade_amount_eur: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
location_details: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
time_availability_details: {
|
|
||||||
type: Sequelize.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
show_offer_to_trusted: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
show_offer_to_trusted_trusted: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
show_offer_to_all_members: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
is_onchain_accepted: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
is_lightning_accepted: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
are_big_notes_accepted: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'session_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
expires_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'session_related_to_public_key',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
session_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'sign_up_challenge_created',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
nostr_challenge_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
app_invite_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.createTable(
|
|
||||||
'sign_up_challenge_completed',
|
|
||||||
{
|
|
||||||
uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
nostr_challenge_completed_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
app_invite_uuid: {
|
|
||||||
type: Sequelize.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
public_key: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
down: (queryInterface, Sequelize) => {
|
|
||||||
return queryInterface.dropTable('Users');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
'use strict';
|
|
||||||
module.exports = {
|
|
||||||
up: (queryInterface, Sequelize) => {
|
|
||||||
return queryInterface.sequelize.transaction((t) => {
|
|
||||||
return Promise.all([
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'login_challenge_created',
|
|
||||||
{
|
|
||||||
fields: ['nostr_challenge_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'nostr_challenge_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'nostr_challenge_completed',
|
|
||||||
{
|
|
||||||
fields: ['challenge'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'nostr_challenge_created',
|
|
||||||
field: 'challenge',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'login_challenge_completed',
|
|
||||||
{
|
|
||||||
fields: ['nostr_challenge_completed_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'nostr_challenge_completed',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'offer_deleted',
|
|
||||||
{
|
|
||||||
fields: ['offer_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'offer_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'offer_details_set',
|
|
||||||
{
|
|
||||||
fields: ['offer_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'offer_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'session_related_to_public_key',
|
|
||||||
{
|
|
||||||
fields: ['session_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'session_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'sign_up_challenge_created',
|
|
||||||
{
|
|
||||||
fields: ['nostr_challenge_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'nostr_challenge_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'sign_up_challenge_created',
|
|
||||||
{
|
|
||||||
fields: ['app_invite_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'app_invite_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'sign_up_challenge_completed',
|
|
||||||
{
|
|
||||||
fields: ['nostr_challenge_completed_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'nostr_challenge_completed',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
queryInterface.addConstraint(
|
|
||||||
'sign_up_challenge_completed',
|
|
||||||
{
|
|
||||||
fields: ['app_invite_uuid'],
|
|
||||||
type: 'foreign key',
|
|
||||||
references: {
|
|
||||||
table: 'app_invite_created',
|
|
||||||
field: 'uuid',
|
|
||||||
},
|
|
||||||
onDelete: 'cascade',
|
|
||||||
onUpdate: 'cascade',
|
|
||||||
},
|
|
||||||
{ transaction: t }
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
down: (queryInterface, Sequelize) => {
|
|
||||||
return queryInterface.dropTable('Users');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -5,20 +5,28 @@ function buildDependencies() {
|
||||||
const errors = require('./errors');
|
const errors = require('./errors');
|
||||||
const constants = require('./constants');
|
const constants = require('./constants');
|
||||||
|
|
||||||
const sequelize = require('./database/database');
|
const sequelize = require('./database');
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
const ModelsProvider = require('./models');
|
const ModelsProvider = require('./models');
|
||||||
const models = new ModelsProvider({ sequelize, DataTypes }).provide();
|
const models = new ModelsProvider({ sequelize, DataTypes }).provide();
|
||||||
|
|
||||||
const AssociationsDefiner = require('./database/associations');
|
const AssociationsDefiner = require('./associations');
|
||||||
new AssociationsDefiner({ models, DataTypes }).define();
|
new AssociationsDefiner({ models, DataTypes }).define();
|
||||||
|
|
||||||
|
sequelize
|
||||||
|
.sync({ alter: true })
|
||||||
|
.then(() => {
|
||||||
|
console.log('Database synced');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error syncing the database:', err);
|
||||||
|
});
|
||||||
|
|
||||||
const ServicesProvider = require('./services');
|
const ServicesProvider = require('./services');
|
||||||
const services = new ServicesProvider({
|
const services = new ServicesProvider({
|
||||||
models,
|
models,
|
||||||
constants,
|
constants,
|
||||||
errors,
|
errors,
|
||||||
sequelize,
|
|
||||||
}).provide();
|
}).provide();
|
||||||
dependencies.services = services;
|
dependencies.services = services;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
class BuyOrSellButtonGroup {
|
|
||||||
constructor({ parentElement, id }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
this.buyButton = null;
|
|
||||||
this.sellButton = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const groupDiv = document.createElement('div');
|
|
||||||
groupDiv.className = 'button-group';
|
|
||||||
groupDiv.id = this.id;
|
|
||||||
|
|
||||||
const buyButton = document.createElement('button');
|
|
||||||
buyButton.dataset.value = 'buy-bitcoin';
|
|
||||||
buyButton.id = 'button-buy-bitcoin';
|
|
||||||
buyButton.className = 'selected';
|
|
||||||
buyButton.textContent = 'Quiero comprar Bitcoin';
|
|
||||||
this.buyButton = buyButton;
|
|
||||||
|
|
||||||
const sellButton = document.createElement('button');
|
|
||||||
sellButton.dataset.value = 'sell-bitcoin';
|
|
||||||
sellButton.id = 'button-sell-bitcoin';
|
|
||||||
sellButton.textContent = 'Quiero vender Bitcoin';
|
|
||||||
this.sellButton = sellButton;
|
|
||||||
|
|
||||||
groupDiv.appendChild(this.buyButton);
|
|
||||||
groupDiv.appendChild(this.sellButton);
|
|
||||||
|
|
||||||
for (const button of [this.buyButton, this.sellButton]) {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
[this.buyButton, this.sellButton].forEach((aButton) => {
|
|
||||||
if (aButton.classList.contains('selected')) {
|
|
||||||
aButton.classList.remove('selected');
|
|
||||||
} else {
|
|
||||||
aButton.classList.add('selected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.element = groupDiv;
|
|
||||||
this.parentElement.appendChild(this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
wants() {
|
|
||||||
if (this.buyButton.classList.contains('selected')) {
|
|
||||||
return 'BTC';
|
|
||||||
}
|
|
||||||
if (this.sellButton.classList.contains('selected')) {
|
|
||||||
return 'EUR';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = BuyOrSellButtonGroup;
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
class PopupNotification {
|
|
||||||
constructor({ parentElement, id, text }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.id = this.id;
|
|
||||||
div.className = 'top-notification-good';
|
|
||||||
|
|
||||||
div.innerHTML = `<img src="/img/circle-check-white.svg" />
|
|
||||||
<p>${this.text}</p>`;
|
|
||||||
|
|
||||||
this.element = div;
|
|
||||||
this.parentElement.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
display() {
|
|
||||||
this.element.classList.add('revealed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = PopupNotification;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
class PremiumSelector {
|
|
||||||
constructor({ parentElement, id, eventSink }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.eventSink = eventSink;
|
|
||||||
|
|
||||||
this.premiumValue = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const premiumSelectorArea = document.createElement('div');
|
|
||||||
premiumSelectorArea.id = this.id;
|
|
||||||
|
|
||||||
const premiumValue = document.createElement('div');
|
|
||||||
premiumValue.id = 'premium-value';
|
|
||||||
premiumValue.textContent = '0%';
|
|
||||||
this.premiumValue = premiumValue;
|
|
||||||
|
|
||||||
const premiumButtonsContainer = document.createElement('div');
|
|
||||||
premiumButtonsContainer.id = 'premium-buttons-container';
|
|
||||||
|
|
||||||
const increaseButton = document.createElement('button');
|
|
||||||
increaseButton.classList.add('premium-button');
|
|
||||||
increaseButton.id = 'button-increase-premium';
|
|
||||||
increaseButton.textContent = '+';
|
|
||||||
|
|
||||||
const decreaseButton = document.createElement('button');
|
|
||||||
decreaseButton.classList.add('premium-button');
|
|
||||||
decreaseButton.id = 'button-decrease-premium';
|
|
||||||
decreaseButton.textContent = '-';
|
|
||||||
|
|
||||||
premiumButtonsContainer.appendChild(increaseButton);
|
|
||||||
premiumButtonsContainer.appendChild(decreaseButton);
|
|
||||||
|
|
||||||
premiumSelectorArea.appendChild(premiumValue);
|
|
||||||
premiumSelectorArea.appendChild(premiumButtonsContainer);
|
|
||||||
|
|
||||||
increaseButton.addEventListener('click', () => {
|
|
||||||
this.modifyPremiumValue(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
decreaseButton.addEventListener('click', () => {
|
|
||||||
this.modifyPremiumValue(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.element = premiumSelectorArea;
|
|
||||||
this.parentElement.appendChild(this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyPremiumValue(delta) {
|
|
||||||
const regexExpression = /-*\d+/;
|
|
||||||
const numValue = parseInt(
|
|
||||||
this.premiumValue.innerText.match(regexExpression)[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
const newValue = `${numValue + delta}%`;
|
|
||||||
|
|
||||||
this.premiumValue.innerText = newValue;
|
|
||||||
this.eventSink.dispatchEvent(new Event('premium-changed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
getPremium() {
|
|
||||||
return parseInt(this.premiumValue.textContent.match(/-?\d+/)[0]) / 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PremiumSelector;
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
const formatNumberWithSpaces = require('../utils/formatNumbersWithSpaces');
|
|
||||||
|
|
||||||
class PriceDisplay {
|
|
||||||
constructor({
|
|
||||||
parentElement,
|
|
||||||
id,
|
|
||||||
premiumProvidingCallback,
|
|
||||||
priceProvidingCallback,
|
|
||||||
}) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.premiumProvidingCallback = premiumProvidingCallback;
|
|
||||||
this.priceProvidingCallback = priceProvidingCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const container = document.createElement('div');
|
|
||||||
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');
|
|
||||||
offerSpan.id = 'offer-price';
|
|
||||||
this.offerPriceSpan = offerSpan;
|
|
||||||
|
|
||||||
offerParagraph.appendChild(offerSpan);
|
|
||||||
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);
|
|
||||||
marketParagraph.append('€/BTC)');
|
|
||||||
|
|
||||||
container.appendChild(offerParagraph);
|
|
||||||
container.appendChild(marketParagraph);
|
|
||||||
|
|
||||||
this.updatePrices();
|
|
||||||
|
|
||||||
this.element = container;
|
|
||||||
this.parentElement.appendChild(this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePrices() {
|
|
||||||
const marketPrice = this.priceProvidingCallback();
|
|
||||||
const marketPriceString = formatNumberWithSpaces(marketPrice);
|
|
||||||
const offerPriceString = formatNumberWithSpaces(
|
|
||||||
Math.round(marketPrice * (1 + this.premiumProvidingCallback()))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.marketPriceSpan.innerText = marketPriceString;
|
|
||||||
this.offerPriceSpan.innerText = offerPriceString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PriceDisplay;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
class PublishOfferButton {
|
|
||||||
constructor({ parentElement, id, onClickCallback }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.onClickCallback = onClickCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.id = this.id;
|
|
||||||
button.className = 'button-primary button-large';
|
|
||||||
button.innerText = 'Publicar oferta';
|
|
||||||
button.addEventListener('click', this.onClickCallback);
|
|
||||||
|
|
||||||
this.element = button;
|
|
||||||
this.parentElement.appendChild(this.element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PublishOfferButton;
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
class WarningDiv {
|
|
||||||
constructor({ parentElement, id, innerHTML }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.innerHTML = innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.id = this.id;
|
|
||||||
div.className = 'card-secondary';
|
|
||||||
div.style.display = 'none';
|
|
||||||
|
|
||||||
div.innerHTML = this.innerHTML;
|
|
||||||
|
|
||||||
this.element = div;
|
|
||||||
this.parentElement.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
display() {
|
|
||||||
this.element.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = WarningDiv;
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
class NostrButton {
|
|
||||||
constructor({ parentElement, id, onClickCallback, buttonText }) {
|
|
||||||
this.element = null;
|
|
||||||
this.parentElement = parentElement;
|
|
||||||
this.id = id;
|
|
||||||
this.onClickCallback = onClickCallback;
|
|
||||||
this.buttonText = buttonText;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const thisButton = document.createElement('button');
|
|
||||||
thisButton.id = this.id;
|
|
||||||
thisButton.type = 'submit';
|
|
||||||
thisButton.className = 'button-large button-nostr';
|
|
||||||
|
|
||||||
const figure = document.createElement('figure');
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = '/img/white_ostrich.svg';
|
|
||||||
img.style.width = '40%';
|
|
||||||
img.style.margin = '-5% -5%';
|
|
||||||
|
|
||||||
figure.appendChild(img);
|
|
||||||
|
|
||||||
const paragraph = document.createElement('p');
|
|
||||||
paragraph.textContent = this.buttonText;
|
|
||||||
|
|
||||||
thisButton.appendChild(figure);
|
|
||||||
thisButton.appendChild(paragraph);
|
|
||||||
|
|
||||||
thisButton.addEventListener('click', () => {
|
|
||||||
this.onClickCallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.element = thisButton;
|
|
||||||
this.parentElement.appendChild(this.element);
|
|
||||||
}
|
|
||||||
|
|
||||||
disable() {
|
|
||||||
if (this.element) {
|
|
||||||
this.element.disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrSignupButton extends NostrButton {
|
|
||||||
constructor({ parentElement, id, onClickCallback }) {
|
|
||||||
super({ parentElement, id, onClickCallback, buttonText: 'Alta con Nostr' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrLoginButton extends NostrButton {
|
|
||||||
constructor({ parentElement, id, onClickCallback }) {
|
|
||||||
super({
|
|
||||||
parentElement,
|
|
||||||
id,
|
|
||||||
onClickCallback,
|
|
||||||
buttonText: 'Login con Nostr',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { NostrSignupButton, NostrLoginButton };
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
const createProfilesFunction = () => {
|
|
||||||
const createProfileConfirmation = document.querySelector(
|
|
||||||
'#create-profile-success'
|
|
||||||
);
|
|
||||||
|
|
||||||
function debounce(func, wait) {
|
|
||||||
let timeout;
|
|
||||||
return function (...args) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateNymInput = debounce(() => {
|
|
||||||
const nymValue = nymInput.value.trim();
|
|
||||||
const isValid = nymValue.length >= 3 && nymValue.length <= 128;
|
|
||||||
if (isValid) {
|
|
||||||
nymInputValidationWarning.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
nymInputValidationWarning.style.display = 'block';
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const checkIfSubmittable = debounce((allInputs) => {
|
|
||||||
const nymIsFilled = allInputs.nymInput.value !== '';
|
|
||||||
let atLeastOneContactIsFilled = false;
|
|
||||||
|
|
||||||
for (const contactInput of allInputs.contactInputs) {
|
|
||||||
if (contactInput.value !== '') {
|
|
||||||
atLeastOneContactIsFilled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonShouldBeDisabled = !(nymIsFilled && atLeastOneContactIsFilled);
|
|
||||||
submitProfileButton.disabled = buttonShouldBeDisabled;
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
async function createProfile(allInputs) {
|
|
||||||
const contactDetails = [];
|
|
||||||
for (const someInput of allInputs.contactInputs) {
|
|
||||||
contactDetails.push({
|
|
||||||
type: someInput.getAttribute('data-type'),
|
|
||||||
value: someInput.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const encryptedContactDetails = await window.nostr.nip04.encrypt(
|
|
||||||
await window.nostr.getPublicKey(),
|
|
||||||
JSON.stringify(contactDetails)
|
|
||||||
);
|
|
||||||
await fetch('/api/set-contact-details', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ encryptedContactDetails }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const nym = allInputs.nymInput.value;
|
|
||||||
await fetch('/api/set-nym', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ nym }),
|
|
||||||
});
|
|
||||||
|
|
||||||
createProfileConfirmation.classList.add('revealed');
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/home';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoadErrands(allInputs, submitProfileButton) {
|
|
||||||
allInputs.nymInput.addEventListener('input', validateNymInput);
|
|
||||||
|
|
||||||
for (const someInput of allInputs.allInputs) {
|
|
||||||
someInput.addEventListener('input', () => {
|
|
||||||
checkIfSubmittable(allInputs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfSubmittable(allInputs);
|
|
||||||
|
|
||||||
submitProfileButton.addEventListener('click', () => {
|
|
||||||
createProfile(allInputs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const nymInput = document.getElementById('nym-input');
|
|
||||||
const nymInputValidationWarning = document.getElementById(
|
|
||||||
'nym-input-validation-warning'
|
|
||||||
);
|
|
||||||
const phoneInput = document.getElementById('phone-input');
|
|
||||||
const whatsappInput = document.getElementById('whatsapp-input');
|
|
||||||
const telegramInput = document.getElementById('telegram-input');
|
|
||||||
const emailInput = document.getElementById('email-input');
|
|
||||||
const nostrInput = document.getElementById('nostr-input');
|
|
||||||
const signalInput = document.getElementById('signal-input');
|
|
||||||
const submitProfileButton = document.getElementById('submit-profile-button');
|
|
||||||
|
|
||||||
const allInputs = {
|
|
||||||
nymInput: nymInput,
|
|
||||||
contactInputs: [
|
|
||||||
phoneInput,
|
|
||||||
whatsappInput,
|
|
||||||
telegramInput,
|
|
||||||
emailInput,
|
|
||||||
nostrInput,
|
|
||||||
signalInput,
|
|
||||||
],
|
|
||||||
allInputs: [
|
|
||||||
nymInput,
|
|
||||||
phoneInput,
|
|
||||||
whatsappInput,
|
|
||||||
telegramInput,
|
|
||||||
emailInput,
|
|
||||||
nostrInput,
|
|
||||||
signalInput,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
onLoadErrands(allInputs, submitProfileButton);
|
|
||||||
};
|
|
||||||
|
|
||||||
createProfilesFunction();
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
const homeFunction = () => {
|
|
||||||
const navbuttonHome = document.getElementById('navbutton-home');
|
|
||||||
const navbuttonOffers = document.getElementById('navbutton-offers');
|
|
||||||
|
|
||||||
navbuttonHome.addEventListener('click', () => {
|
|
||||||
window.location.href = '/home';
|
|
||||||
});
|
|
||||||
|
|
||||||
navbuttonOffers.addEventListener('click', () => {
|
|
||||||
window.location.href = '/offers';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
homeFunction();
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
const checkNostrExtension = require('../utils/checkNostrExtension');
|
|
||||||
const inviteService = require('../services/inviteService');
|
|
||||||
const constants = require('../../constants');
|
|
||||||
const { NostrSignupButton } = require('../components/nostrButtons');
|
|
||||||
const WarningDiv = require('../components/WarningDiv');
|
|
||||||
const PopupNotification = require('../components/PopupNotification');
|
|
||||||
|
|
||||||
const invitesFunction = () => {
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
const warningsContainer = document.getElementById('warnings-container');
|
|
||||||
const nostrSignupArea = document.getElementById('nostr-signup-area');
|
|
||||||
|
|
||||||
const signupButton = new NostrSignupButton({
|
|
||||||
parentElement: nostrSignupArea,
|
|
||||||
id: 'nostr-signup-button',
|
|
||||||
onClickCallback: async () => {
|
|
||||||
const verifyResponse =
|
|
||||||
await inviteService.requestAndRespondSignUpChallenge({
|
|
||||||
onNostrErrorCallback: () => {
|
|
||||||
rejectedNostrWarning.display();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (verifyResponse.ok) {
|
|
||||||
signUpSuccessPopup.display();
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = constants.API_PATHS.createProfile;
|
|
||||||
}, constants.DEFAULT_REDIRECT_DELAY);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
signupButton.render();
|
|
||||||
|
|
||||||
const noExtensionWarning = new WarningDiv({
|
|
||||||
parentElement: warningsContainer,
|
|
||||||
id: 'no-extension-nudges',
|
|
||||||
innerHTML: `<p>
|
|
||||||
¡Atención! No se ha encontrado una extensión de Nostr en tu
|
|
||||||
navegador. Puedes usar:
|
|
||||||
</p>
|
|
||||||
<p><strong>Firefox</strong></p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Alby</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>nos2x-fox</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p><strong>Chrome</strong></p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Alby</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>nos2x</a
|
|
||||||
>
|
|
||||||
</p>`,
|
|
||||||
});
|
|
||||||
noExtensionWarning.render();
|
|
||||||
|
|
||||||
const rejectedNostrWarning = new WarningDiv({
|
|
||||||
parentElement: warningsContainer,
|
|
||||||
id: 'rejected-nostr-nudges',
|
|
||||||
innerHTML: `<p>
|
|
||||||
Ups, parece que no has aceptado que usemos tus claves. Si te has
|
|
||||||
equivocado, puedes intentarlo de nuevo.
|
|
||||||
</p>`,
|
|
||||||
});
|
|
||||||
rejectedNostrWarning.render();
|
|
||||||
|
|
||||||
const signUpSuccessPopup = new PopupNotification({
|
|
||||||
parentElement: body,
|
|
||||||
id: 'sign-up-success',
|
|
||||||
text: '¡Bien! Hemos dado de alta tu clave de Nostr. Te vamos a redirigir a la seca, espera un momento.',
|
|
||||||
});
|
|
||||||
signUpSuccessPopup.render();
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
checkNostrExtension({
|
|
||||||
window,
|
|
||||||
successCallback: () => {
|
|
||||||
console.log('Nostr extension present');
|
|
||||||
},
|
|
||||||
failureCallback: () => {
|
|
||||||
console.log('Nostr extension not present');
|
|
||||||
signupButton.disable();
|
|
||||||
noExtensionWarning.display();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
invitesFunction();
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
const checkNostrExtension = require('../utils/checkNostrExtension');
|
|
||||||
const loginService = require('../services/loginService');
|
|
||||||
const WarningDiv = require('../components/WarningDiv');
|
|
||||||
const { NostrLoginButton } = require('../components/nostrButtons');
|
|
||||||
const PopupNotification = require('../components/PopupNotification');
|
|
||||||
const constants = require('../../constants');
|
|
||||||
|
|
||||||
const loginFunction = () => {
|
|
||||||
const loginButtonArea = document.getElementById('login-button-area');
|
|
||||||
const warningsArea = document.getElementById('warnings-area');
|
|
||||||
|
|
||||||
const successPopup = new PopupNotification({
|
|
||||||
parentElement: document.querySelector('body'),
|
|
||||||
id: 'login-success-popup',
|
|
||||||
text: '¡Éxito! Te estamos llevando a la app...',
|
|
||||||
});
|
|
||||||
successPopup.render();
|
|
||||||
const nostrLoginButton = new NostrLoginButton({
|
|
||||||
parentElement: loginButtonArea,
|
|
||||||
id: 'login-button',
|
|
||||||
onClickCallback: async () => {
|
|
||||||
const verifyResponse = await loginService.requestAndRespondLoginChallenge(
|
|
||||||
{
|
|
||||||
onRejectedPubKeyCallback: () => {
|
|
||||||
nostrRejectedWarning.display();
|
|
||||||
},
|
|
||||||
onRejectedSignatureCallback: () => {
|
|
||||||
nostrRejectedWarning.display();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (verifyResponse.status === 403) {
|
|
||||||
notRegisteredPubkeyWarning.display();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verifyResponse.ok) {
|
|
||||||
nostrLoginButton.disable();
|
|
||||||
successPopup.display();
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = constants.API_PATHS.home;
|
|
||||||
}, constants.DEFAULT_REDIRECT_DELAY);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
nostrLoginButton.render();
|
|
||||||
|
|
||||||
const notRegisteredPubkeyWarning = new WarningDiv({
|
|
||||||
parentElement: warningsArea,
|
|
||||||
id: 'rejected-public-key',
|
|
||||||
innerHTML: `<p>
|
|
||||||
Ups, esa clave no está registrada en la seca. ¿Quizás estás usando un
|
|
||||||
perfil equivocado?
|
|
||||||
</p>`,
|
|
||||||
});
|
|
||||||
notRegisteredPubkeyWarning.render();
|
|
||||||
|
|
||||||
const noExtensionWarning = new WarningDiv({
|
|
||||||
parentElement: warningsArea,
|
|
||||||
id: 'no-extension-nudges',
|
|
||||||
innerHTML: `<p>
|
|
||||||
¡Atención! No se ha encontrado una extensión de Nostr en tu navegador.
|
|
||||||
Puedes usar:
|
|
||||||
</p>
|
|
||||||
<p><strong>Firefox</strong></p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Alby</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>nos2x-fox</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p><strong>Chrome</strong></p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Alby</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>nos2x</a
|
|
||||||
>
|
|
||||||
</p>`,
|
|
||||||
});
|
|
||||||
noExtensionWarning.render();
|
|
||||||
|
|
||||||
const nostrRejectedWarning = new WarningDiv({
|
|
||||||
parentElement: warningsArea,
|
|
||||||
id: 'rejected-nostr-nudges',
|
|
||||||
innerHTML: `<p>
|
|
||||||
Ups, parece que no has aceptado que usemos tus claves. Si te has
|
|
||||||
equivocado, puedes intentarlo de nuevo.
|
|
||||||
</p>`,
|
|
||||||
});
|
|
||||||
nostrRejectedWarning.render();
|
|
||||||
|
|
||||||
window.onload = () => {
|
|
||||||
checkNostrExtension({
|
|
||||||
window,
|
|
||||||
successCallback: () => {
|
|
||||||
console.log('Nostr extension present');
|
|
||||||
},
|
|
||||||
failureCallback: () => {
|
|
||||||
console.log('Nostr extension not present');
|
|
||||||
nostrLoginButton.disable();
|
|
||||||
noExtensionWarning.display();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
loginFunction();
|
|
||||||
|
|
@ -1,694 +0,0 @@
|
||||||
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 createOfferEventBus = new EventTarget();
|
|
||||||
|
|
||||||
const publishOfferButton = new PublishOfferButton({
|
|
||||||
parentElement: document.getElementById('submit-button-area'),
|
|
||||||
id: 'button-submit-offer',
|
|
||||||
onClickCallback: async () => {
|
|
||||||
await publishOffer();
|
|
||||||
await myOffers.getOffersFromApi();
|
|
||||||
await myOffers.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');
|
|
||||||
|
|
||||||
navbuttonHome.addEventListener('click', () => {
|
|
||||||
window.location.href = '/home';
|
|
||||||
});
|
|
||||||
|
|
||||||
navbuttonOffers.addEventListener('click', () => {
|
|
||||||
window.location.href = '/offers';
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttonStartCreateOffer = document.getElementById(
|
|
||||||
'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;
|
|
||||||
this.offers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOffersFromApi() {
|
|
||||||
const offersResponse = await fetch('/api/publickey-offers');
|
|
||||||
|
|
||||||
this.offers = [];
|
|
||||||
|
|
||||||
const offersData = (await offersResponse.json()).data;
|
|
||||||
if (offersResponse.ok) {
|
|
||||||
for (const record of offersData) {
|
|
||||||
this.offers.push(new Offer(record));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async render() {
|
|
||||||
if (this.offers.length === 0) {
|
|
||||||
this.ownOffersContainerElement.innerHTML =
|
|
||||||
'<p class="shadowed-round-area">Vaya, no hay nada por aquí...</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.ownOffersContainerElement.innerHTML = '';
|
|
||||||
|
|
||||||
for (const someOffer of this.offers) {
|
|
||||||
this.ownOffersContainerElement.append(someOffer.buildHTML());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteOfferByUuid(offerUuid) {
|
|
||||||
await fetch(`/api/offer/${offerUuid}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonStartCreateOffer.addEventListener('click', () => {
|
|
||||||
toggleCreateOfferModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
buttonViewMyOffers.addEventListener('click', async () => {
|
|
||||||
await myOffers.getOffersFromApi();
|
|
||||||
await myOffers.render();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
offersPage();
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
function checkNostrExtension({ window, successCallback, failureCallback }) {
|
|
||||||
if (!window.nostr) {
|
|
||||||
failureCallback();
|
|
||||||
} else {
|
|
||||||
successCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = checkNostrExtension;
|
|
||||||
|
|
@ -17,7 +17,6 @@ class NostrChallengeCompletedProvider {
|
||||||
challenge: {
|
challenge: {
|
||||||
type: this.DataTypes.STRING,
|
type: this.DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
|
||||||
},
|
},
|
||||||
signed_event: {
|
signed_event: {
|
||||||
type: this.DataTypes.JSONB,
|
type: this.DataTypes.JSONB,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ class NostrChallengeCreatedProvider {
|
||||||
challenge: {
|
challenge: {
|
||||||
type: this.DataTypes.STRING,
|
type: this.DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
|
||||||
},
|
},
|
||||||
expires_at: {
|
expires_at: {
|
||||||
type: this.DataTypes.DATE,
|
type: this.DataTypes.DATE,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class OfferDeletedProvider {
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
offer_uuid: {
|
offer_uuid: {
|
||||||
type: this.DataTypes.UUID,
|
type: this.DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class OfferDetailsSetProvider {
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
offer_uuid: {
|
offer_uuid: {
|
||||||
type: this.DataTypes.UUID,
|
type: this.DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
wants: {
|
wants: {
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-modal-background {
|
.full-screen-modal-background {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 650 B After Width: | Height: | Size: 650 B |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
10
src/public/javascript/app.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
const navbuttonHome = document.getElementById('navbutton-home');
|
||||||
|
const navbuttonOffers = document.getElementById('navbutton-offers');
|
||||||
|
|
||||||
|
navbuttonHome.addEventListener('click', () => {
|
||||||
|
window.location.href = '/home';
|
||||||
|
});
|
||||||
|
|
||||||
|
navbuttonOffers.addEventListener('click', () => {
|
||||||
|
window.location.href = '/offers';
|
||||||
|
});
|
||||||
121
src/public/javascript/createProfile.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
const createProfileConfirmation = document.querySelector(
|
||||||
|
'#create-profile-success'
|
||||||
|
);
|
||||||
|
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateNymInput = debounce(() => {
|
||||||
|
const nymValue = nymInput.value.trim();
|
||||||
|
const isValid = nymValue.length >= 3 && nymValue.length <= 128;
|
||||||
|
if (isValid) {
|
||||||
|
nymInputValidationWarning.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
nymInputValidationWarning.style.display = 'block';
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const checkIfSubmittable = debounce((allInputs) => {
|
||||||
|
const nymIsFilled = allInputs.nymInput.value !== '';
|
||||||
|
let atLeastOneContactIsFilled = false;
|
||||||
|
|
||||||
|
for (const contactInput of allInputs.contactInputs) {
|
||||||
|
if (contactInput.value !== '') {
|
||||||
|
atLeastOneContactIsFilled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonShouldBeDisabled = !(nymIsFilled && atLeastOneContactIsFilled);
|
||||||
|
submitProfileButton.disabled = buttonShouldBeDisabled;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
async function createProfile(allInputs) {
|
||||||
|
const contactDetails = [];
|
||||||
|
for (const someInput of allInputs.contactInputs) {
|
||||||
|
contactDetails.push({
|
||||||
|
type: someInput.getAttribute('data-type'),
|
||||||
|
value: someInput.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const encryptedContactDetails = await window.nostr.nip04.encrypt(
|
||||||
|
await window.nostr.getPublicKey(),
|
||||||
|
JSON.stringify(contactDetails)
|
||||||
|
);
|
||||||
|
await fetch('/api/set-contact-details', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ encryptedContactDetails }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const nym = allInputs.nymInput.value;
|
||||||
|
await fetch('/api/set-nym', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ nym }),
|
||||||
|
});
|
||||||
|
|
||||||
|
createProfileConfirmation.classList.add('revealed');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/home';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoadErrands(allInputs, submitProfileButton) {
|
||||||
|
allInputs.nymInput.addEventListener('input', validateNymInput);
|
||||||
|
|
||||||
|
for (const someInput of allInputs.allInputs) {
|
||||||
|
someInput.addEventListener('input', () => {
|
||||||
|
checkIfSubmittable(allInputs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfSubmittable(allInputs);
|
||||||
|
|
||||||
|
submitProfileButton.addEventListener('click', () => {
|
||||||
|
createProfile(allInputs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nymInput = document.getElementById('nym-input');
|
||||||
|
const nymInputValidationWarning = document.getElementById(
|
||||||
|
'nym-input-validation-warning'
|
||||||
|
);
|
||||||
|
const phoneInput = document.getElementById('phone-input');
|
||||||
|
const whatsappInput = document.getElementById('whatsapp-input');
|
||||||
|
const telegramInput = document.getElementById('telegram-input');
|
||||||
|
const emailInput = document.getElementById('email-input');
|
||||||
|
const nostrInput = document.getElementById('nostr-input');
|
||||||
|
const signalInput = document.getElementById('signal-input');
|
||||||
|
const submitProfileButton = document.getElementById('submit-profile-button');
|
||||||
|
|
||||||
|
const allInputs = {
|
||||||
|
nymInput: nymInput,
|
||||||
|
contactInputs: [
|
||||||
|
phoneInput,
|
||||||
|
whatsappInput,
|
||||||
|
telegramInput,
|
||||||
|
emailInput,
|
||||||
|
nostrInput,
|
||||||
|
signalInput,
|
||||||
|
],
|
||||||
|
allInputs: [
|
||||||
|
nymInput,
|
||||||
|
phoneInput,
|
||||||
|
whatsappInput,
|
||||||
|
telegramInput,
|
||||||
|
emailInput,
|
||||||
|
nostrInput,
|
||||||
|
signalInput,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoadErrands(allInputs, submitProfileButton);
|
||||||
0
src/public/javascript/home.js
Normal file
|
|
@ -1,4 +1,23 @@
|
||||||
const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => {
|
window.onload = function () {
|
||||||
|
if (!window.nostr) {
|
||||||
|
console.log('Nostr extension not present');
|
||||||
|
document.querySelector('#nostr-signup-button').disabled = true;
|
||||||
|
document.querySelector('#no-extension-nudges').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
console.log('Nostr extension present');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const signUpConfirmation = document.querySelector('#sign-up-success');
|
||||||
|
|
||||||
|
function showConfirmationAndRedirect() {
|
||||||
|
signUpConfirmation.classList.add('revealed');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/createProfile';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptInvite() {
|
||||||
let challengeResponse;
|
let challengeResponse;
|
||||||
try {
|
try {
|
||||||
challengeResponse = await fetch('/api/signup/nostr-challenge', {
|
challengeResponse = await fetch('/api/signup/nostr-challenge', {
|
||||||
|
|
@ -18,7 +37,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => {
|
||||||
try {
|
try {
|
||||||
pubkey = await window.nostr.getPublicKey();
|
pubkey = await window.nostr.getPublicKey();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNostrErrorCallback();
|
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const event = {
|
const event = {
|
||||||
|
|
@ -33,7 +52,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => {
|
||||||
try {
|
try {
|
||||||
signedEvent = await window.nostr.signEvent(event);
|
signedEvent = await window.nostr.signEvent(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onNostrErrorCallback();
|
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,9 +68,7 @@ const requestAndRespondSignUpChallenge = async ({ onNostrErrorCallback }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyResponse;
|
if (verifyResponse.ok) {
|
||||||
};
|
showConfirmationAndRedirect();
|
||||||
|
}
|
||||||
module.exports = {
|
}
|
||||||
requestAndRespondSignUpChallenge,
|
|
||||||
};
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
const requestAndRespondLoginChallenge = async ({
|
window.onload = function () {
|
||||||
onRejectedPubKeyCallback,
|
if (!window.nostr) {
|
||||||
onRejectedSignatureCallback,
|
console.log('Nostr extension not present');
|
||||||
}) => {
|
document.querySelector('#login-button').disabled = true;
|
||||||
onRejectedPubKeyCallback = () => {
|
document.querySelector('#no-extension-nudges').style.display = 'block';
|
||||||
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
|
} else {
|
||||||
|
console.log('Nostr extension present');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
onRejectedSignatureCallback = onRejectedPubKeyCallback;
|
|
||||||
|
|
||||||
|
async function login() {
|
||||||
let challengeResponse;
|
let challengeResponse;
|
||||||
try {
|
try {
|
||||||
challengeResponse = await fetch('/api/login/nostr-challenge', {
|
challengeResponse = await fetch('/api/login/nostr-challenge', {
|
||||||
|
|
@ -26,7 +28,7 @@ const requestAndRespondLoginChallenge = async ({
|
||||||
try {
|
try {
|
||||||
pubkey = await window.nostr.getPublicKey();
|
pubkey = await window.nostr.getPublicKey();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onRejectedPubKeyCallback();
|
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const event = {
|
const event = {
|
||||||
|
|
@ -41,7 +43,7 @@ const requestAndRespondLoginChallenge = async ({
|
||||||
try {
|
try {
|
||||||
signedEvent = await window.nostr.signEvent(event);
|
signedEvent = await window.nostr.signEvent(event);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onRejectedSignatureCallback();
|
document.querySelector('#rejected-nostr-nudges').style.display = 'block';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,9 +59,14 @@ const requestAndRespondLoginChallenge = async ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyResponse;
|
if (verifyResponse.status === 403) {
|
||||||
};
|
document.querySelector('#rejected-public-key').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
if (verifyResponse.ok) {
|
||||||
requestAndRespondLoginChallenge,
|
document.querySelector('#sign-up-success').style.display = 'block';
|
||||||
};
|
setTimeout(() => {
|
||||||
|
window.location.href = '/home';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
669
src/public/javascript/offers.js
Normal file
|
|
@ -0,0 +1,669 @@
|
||||||
|
const buttonStartCreateOffer = document.getElementById(
|
||||||
|
'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 buyOrSellButtonGroup = document.getElementById(
|
||||||
|
'button-group-buy-or-sell'
|
||||||
|
);
|
||||||
|
const buyOrSellButtons = buyOrSellButtonGroup.querySelectorAll('button');
|
||||||
|
const buyButton = document.getElementById('button-buy-bitcoin');
|
||||||
|
const sellButton = document.getElementById('button-sell-bitcoin');
|
||||||
|
|
||||||
|
const premiumValue = document.getElementById('premium-value');
|
||||||
|
const buttonIncreasePremium = document.getElementById(
|
||||||
|
'button-increase-premium'
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonDecreasePremium = document.getElementById(
|
||||||
|
'button-decrease-premium'
|
||||||
|
);
|
||||||
|
|
||||||
|
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 publishOfferButton = document.getElementById('button-submit-offer');
|
||||||
|
|
||||||
|
const offerCreatedPopup = document.getElementById('offer-created-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 modifyPremiumValue(delta) {
|
||||||
|
const regexExpression = /-*\d+/;
|
||||||
|
const numValue = parseInt(premiumValue.innerText.match(regexExpression)[0]);
|
||||||
|
|
||||||
|
const newValue = `${numValue + delta}%`;
|
||||||
|
|
||||||
|
premiumValue.innerText = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBuyOrSellButtonGroup() {
|
||||||
|
buyOrSellButtons.forEach((button) => {
|
||||||
|
if (button.classList.contains('selected')) {
|
||||||
|
button.classList.remove('selected');
|
||||||
|
} else {
|
||||||
|
button.classList.add('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
let wants;
|
||||||
|
if (buyButton.classList.contains('selected')) {
|
||||||
|
wants = 'BTC';
|
||||||
|
}
|
||||||
|
if (sellButton.classList.contains('selected')) {
|
||||||
|
wants = 'EUR';
|
||||||
|
}
|
||||||
|
|
||||||
|
const premium = parseInt(premiumValue.innerText.match(/\d+/)[0]) / 100;
|
||||||
|
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 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
offerCreatedPopup.classList.remove('max-size-zero');
|
||||||
|
offerCreatedPopup.classList.add('revealed');
|
||||||
|
setTimeout(() => {
|
||||||
|
offerCreatedPopup.classList.remove('revealed');
|
||||||
|
}, 3000);
|
||||||
|
setTimeout(() => {
|
||||||
|
offerCreatedPopup.classList.add('max-size-zero');
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
toggleCreateOfferModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
deleteOfferByUuid(this.uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
actionButtonsArea.append(editActionArea, deleteActionArea);
|
||||||
|
|
||||||
|
offerCard.append(
|
||||||
|
tradeDescDiv,
|
||||||
|
premiumDescDiv,
|
||||||
|
whereDescDiv,
|
||||||
|
whenDescDiv,
|
||||||
|
bitcoinMethodsDiv,
|
||||||
|
visibilityDiv,
|
||||||
|
otherOfferFeaturesDiv,
|
||||||
|
actionButtonsArea
|
||||||
|
);
|
||||||
|
|
||||||
|
return offerCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyOffers {
|
||||||
|
constructor(ownOffersContainerElement) {
|
||||||
|
this.ownOffersContainerElement = ownOffersContainerElement;
|
||||||
|
this.offers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOffersFromApi() {
|
||||||
|
const offersResponse = await fetch('/api/publickey-offers');
|
||||||
|
|
||||||
|
this.offers = [];
|
||||||
|
|
||||||
|
const offersData = (await offersResponse.json()).data;
|
||||||
|
if (offersResponse.ok) {
|
||||||
|
for (const record of offersData) {
|
||||||
|
this.offers.push(new Offer(record));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
if (this.offers.length === 0) {
|
||||||
|
this.ownOffersContainerElement.innerHTML =
|
||||||
|
'<p class="shadowed-round-area">Vaya, no hay nada por aquí...</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.ownOffersContainerElement.innerHTML = '';
|
||||||
|
|
||||||
|
for (const someOffer of this.offers) {
|
||||||
|
this.ownOffersContainerElement.append(someOffer.buildHTML());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOfferByUuid(offerUuid) {
|
||||||
|
await fetch(`/api/offer/${offerUuid}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
myOffers.getOffersFromApi();
|
||||||
|
myOffers.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonStartCreateOffer.addEventListener('click', () => {
|
||||||
|
toggleCreateOfferModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonViewMyOffers.addEventListener('click', async () => {
|
||||||
|
await myOffers.getOffersFromApi();
|
||||||
|
await myOffers.render();
|
||||||
|
toggleViewMyOffersPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
closeOffer.addEventListener('click', () => {
|
||||||
|
toggleCreateOfferModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
buyOrSellButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
toggleBuyOrSellButtonGroup();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonIncreasePremium.addEventListener('click', () => {
|
||||||
|
modifyPremiumValue(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonDecreasePremium.addEventListener('click', () => {
|
||||||
|
modifyPremiumValue(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
publishOfferButton.addEventListener('click', async () => {
|
||||||
|
await publishOffer();
|
||||||
|
await myOffers.getOffersFromApi();
|
||||||
|
await myOffers.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateBtcInput();
|
||||||
|
|
||||||
|
const myOffers = new MyOffers(ownOffersContainer);
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
function formatNumberWithSpaces(num) {
|
function formatNumberWithSpaces(num) {
|
||||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = formatNumberWithSpaces;
|
|
||||||
|
|
@ -274,9 +274,7 @@ class ApiRoutesProvider {
|
||||||
const publicKey = req.cookies.publicKey;
|
const publicKey = req.cookies.publicKey;
|
||||||
|
|
||||||
const offers =
|
const offers =
|
||||||
await this.services.offerService.getActiveOffersByPublicKey(
|
await this.services.offerService.getOffersByPublicKey(publicKey);
|
||||||
publicKey
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!offers) {
|
if (!offers) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
class ServicesProvider {
|
class ServicesProvider {
|
||||||
constructor({ models, constants, errors, sequelize }) {
|
constructor({ models, constants, errors }) {
|
||||||
this.models = models;
|
this.models = models;
|
||||||
this.constants = constants;
|
this.constants = constants;
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
this.sequelize = sequelize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
provide() {
|
provide() {
|
||||||
|
|
@ -44,7 +43,6 @@ class ServicesProvider {
|
||||||
const offerService = new OfferServiceProvider({
|
const offerService = new OfferServiceProvider({
|
||||||
models: this.models,
|
models: this.models,
|
||||||
errors: this.errors,
|
errors: this.errors,
|
||||||
sequelize: this.sequelize,
|
|
||||||
}).provide();
|
}).provide();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,19 @@
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
|
|
||||||
class OfferServiceProvider {
|
class OfferServiceProvider {
|
||||||
constructor({ models, errors, sequelize }) {
|
constructor({ models, errors }) {
|
||||||
this.models = models;
|
this.models = models;
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
this.sequelize = sequelize;
|
|
||||||
}
|
}
|
||||||
provide() {
|
provide() {
|
||||||
const createOffer = async (publicKey, offerDetails) => {
|
const createOffer = async (publicKey, offerDetails) => {
|
||||||
const createOfferTransaction = await this.sequelize.transaction();
|
const offerCreated = await this.models.OfferCreated.create({
|
||||||
try {
|
|
||||||
const offerCreated = await this.models.OfferCreated.create(
|
|
||||||
{
|
|
||||||
uuid: uuid.v7(),
|
uuid: uuid.v7(),
|
||||||
public_key: publicKey,
|
public_key: publicKey,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
},
|
});
|
||||||
{ transaction: createOfferTransaction }
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.models.OfferDetailsSet.create(
|
await this.models.OfferDetailsSet.create({
|
||||||
{
|
|
||||||
uuid: uuid.v7(),
|
uuid: uuid.v7(),
|
||||||
offer_uuid: offerCreated.uuid,
|
offer_uuid: offerCreated.uuid,
|
||||||
wants: offerDetails.wants,
|
wants: offerDetails.wants,
|
||||||
|
|
@ -36,13 +29,7 @@ class OfferServiceProvider {
|
||||||
is_lightning_accepted: offerDetails.is_lightning_accepted,
|
is_lightning_accepted: offerDetails.is_lightning_accepted,
|
||||||
are_big_notes_accepted: offerDetails.are_big_notes_accepted,
|
are_big_notes_accepted: offerDetails.are_big_notes_accepted,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
},
|
});
|
||||||
{ transaction: createOfferTransaction }
|
|
||||||
);
|
|
||||||
await createOfferTransaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await createOfferTransaction.rollback();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteOffer = async (offerUuid) => {
|
const deleteOffer = async (offerUuid) => {
|
||||||
|
|
@ -66,24 +53,22 @@ class OfferServiceProvider {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveOffersByPublicKey = async (publicKey) => {
|
const getOffersByPublicKey = async (publicKey) => {
|
||||||
const activeOffers = await this.models.OfferCreated.findAll({
|
const offers = await this.models.OfferCreated.findAll({
|
||||||
where: {
|
where: {
|
||||||
public_key: publicKey,
|
public_key: publicKey,
|
||||||
'$OfferDeleted.uuid$': null,
|
|
||||||
},
|
},
|
||||||
include: { model: this.models.OfferDeleted, required: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(activeOffers);
|
console.log(offers);
|
||||||
|
|
||||||
if (!activeOffers) {
|
if (!offers) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const offersToReturn = [];
|
const offersToReturn = [];
|
||||||
if (activeOffers) {
|
if (offers) {
|
||||||
for (const someOffer of activeOffers) {
|
for (const someOffer of offers) {
|
||||||
const offerDetails = await this.models.OfferDetailsSet.findOne({
|
const offerDetails = await this.models.OfferDetailsSet.findOne({
|
||||||
where: {
|
where: {
|
||||||
offer_uuid: someOffer.uuid,
|
offer_uuid: someOffer.uuid,
|
||||||
|
|
@ -115,11 +100,7 @@ class OfferServiceProvider {
|
||||||
return offersToReturn;
|
return offersToReturn;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return { createOffer, getOffersByPublicKey, deleteOffer };
|
||||||
createOffer,
|
|
||||||
getActiveOffersByPublicKey,
|
|
||||||
deleteOffer,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,5 +130,5 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="/javascript/createProfile.bundle.js"></script>
|
<script src="/javascript/createProfile.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@
|
||||||
<%- include("partials/appCommonHeader") %>
|
<%- include("partials/appCommonHeader") %>
|
||||||
|
|
||||||
<%- include("partials/appCommonScripts") %>
|
<%- include("partials/appCommonScripts") %>
|
||||||
<script src="/javascript/home.bundle.js"></script>
|
<script src="/javascript/home.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,83 @@
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
<p>Usa tu extensión de Nostr para darte de alta:</p>
|
<p>Usa tu extensión de Nostr para darte de alta:</p>
|
||||||
<div id="nostr-signup-area"></div>
|
<form onsubmit="acceptInvite();return false">
|
||||||
<div id="warnings-container"></div>
|
<button
|
||||||
|
id="nostr-signup-button"
|
||||||
|
type="submit"
|
||||||
|
class="button-large button-nostr"
|
||||||
|
>
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="/img/white_ostrich.svg"
|
||||||
|
width="40%"
|
||||||
|
margin="-5% -5%"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<p>Alta con Nostr</p>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
id="rejected-nostr-nudges"
|
||||||
|
class="card-secondary"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Ups, parece que no has aceptado que usemos tus claves. Si te has
|
||||||
|
equivocado, puedes intentarlo de nuevo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="no-extension-nudges"
|
||||||
|
class="card-secondary"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
¡Atención! No se ha encontrado una extensión de Nostr en tu
|
||||||
|
navegador. Puedes usar:
|
||||||
|
</p>
|
||||||
|
<p><strong>Firefox</strong></p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Alby</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>nos2x-fox</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p><strong>Chrome</strong></p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Alby</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>nos2x</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="sign-up-success" class="top-notification-good">
|
||||||
|
<img src="/img/circle-check-white.svg" />
|
||||||
|
<p>
|
||||||
|
¡Bien! Hemos dado de alta tu clave de Nostr. Te vamos a redirigir
|
||||||
|
a la seca, espera un momento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>¿No tienes cuenta de Nostr?</p>
|
<p>¿No tienes cuenta de Nostr?</p>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
|
|
@ -36,6 +110,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="/javascript/invite.bundle.js"></script>
|
<script src="/javascript/invite.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,91 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title></title>
|
<title>Hello World</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/css/seca.css" />
|
<link rel="stylesheet" href="/css/seca.css" />
|
||||||
<link rel="stylesheet" href="/css/login.css" />
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="login-card" class="over-background">
|
<div id="login-card" class="over-background">
|
||||||
<div id="login-card-content" class="shadowed-round-area">
|
<h1>Bienvenido a la seca</h1>
|
||||||
<figure>
|
<figure>
|
||||||
<img class="logo" src="/img/laseca_logo_white.png" width="25%" />
|
<img class="logo" src="/img/laseca_logo_white.png" width="25%" />
|
||||||
</figure>
|
</figure>
|
||||||
<div id="login-button-area"></div>
|
<form onsubmit="login();return false">
|
||||||
<div id="warnings-area"></div>
|
<button
|
||||||
|
id="login-button"
|
||||||
|
type="submit"
|
||||||
|
class="button-large button-nostr"
|
||||||
|
>
|
||||||
|
<figure>
|
||||||
|
<img src="/img/white_ostrich.svg" width="40%" margin="-5% -5%" />
|
||||||
|
</figure>
|
||||||
|
<p>Login con Nostr</p>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="sign-up-success" style="display: none">
|
||||||
|
<p>¡Bien! Tu clave es parte de la seca.</p>
|
||||||
|
<p>Redirigiendo a la app...</p>
|
||||||
|
</div>
|
||||||
|
<div id="rejected-public-key" style="display: none">
|
||||||
|
<p>
|
||||||
|
Ups, esa clave no está registrada en la seca. ¿Quizás estás usando un
|
||||||
|
perfil equivocado?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="rejected-nostr-nudges" style="display: none">
|
||||||
|
<p>
|
||||||
|
Ups, parece que no has aceptado que usemos tus claves. Si te has
|
||||||
|
equivocado, puedes intentarlo de nuevo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="no-extension-nudges"
|
||||||
|
class="card-secondary"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
¡Atención! No se ha encontrado una extensión de Nostr en tu navegador.
|
||||||
|
Puedes usar:
|
||||||
|
</p>
|
||||||
|
<p><strong>Firefox</strong></p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Alby</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>nos2x-fox</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p><strong>Chrome</strong></p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Alby</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>nos2x</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/javascript/login.bundle.js"></script>
|
<script src="/javascript/login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,41 @@
|
||||||
<div class="full-screen-modal" id="create-offer-root">
|
<div class="full-screen-modal" id="create-offer-root">
|
||||||
<div id="create-offer-controls">
|
<div id="create-offer-controls">
|
||||||
<h2>Añade los detalles de tu oferta</h2>
|
<h2>Añade los detalles de tu oferta</h2>
|
||||||
<div id="buy-or-sell-area" class="create-offer-step"></div>
|
<div id="buy-or-sell-area" class="create-offer-step">
|
||||||
|
<div id="button-group-buy-or-sell" class="button-group">
|
||||||
|
<button
|
||||||
|
data-value="buy-bitcoin"
|
||||||
|
id="button-buy-bitcoin"
|
||||||
|
class="selected"
|
||||||
|
>
|
||||||
|
Quiero comprar Bitcoin</button
|
||||||
|
><button data-value="sell-bitcoin" id="button-sell-bitcoin">
|
||||||
|
Quiero vender Bitcoin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="premium-area" class="create-offer-step">
|
<div id="premium-area" class="create-offer-step">
|
||||||
<h3>Premium</h3>
|
<h3>Premium</h3>
|
||||||
<div id="premium-content-area"></div>
|
<div id="premium-content-area">
|
||||||
|
<div id="premium-selector-area">
|
||||||
|
<div id="premium-value">0%</div>
|
||||||
|
<div id="premium-buttons-container">
|
||||||
|
<button class="premium-button" id="button-increase-premium">
|
||||||
|
+</button
|
||||||
|
><button class="premium-button" id="button-decrease-premium">
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="premium-price-display-area">
|
||||||
|
<p id="offer-price-paragraph">
|
||||||
|
Tu precio: <span id="offer-price">90 000</span>€/BTC
|
||||||
|
</p>
|
||||||
|
<p id="market-price-paragraph">
|
||||||
|
(Precio mercado: <span>83 000</span>€/BTC)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="amount-area" class="create-offer-step">
|
<div id="amount-area" class="create-offer-step">
|
||||||
<h3>¿Cuánto?</h3>
|
<h3>¿Cuánto?</h3>
|
||||||
|
|
@ -147,7 +178,14 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="submit-button-area"></div>
|
<div id="submit-button-area">
|
||||||
|
<button
|
||||||
|
id="button-submit-offer"
|
||||||
|
class="button-primary button-large"
|
||||||
|
>
|
||||||
|
Publicar oferta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div id="close-offer-controls-area">
|
<div id="close-offer-controls-area">
|
||||||
<button id="close-offer" class="button-secondary button-medium">
|
<button id="close-offer" class="button-secondary button-medium">
|
||||||
Volver
|
Volver
|
||||||
|
|
@ -163,14 +201,8 @@
|
||||||
<img src="/img/circle-check-white.svg" />
|
<img src="/img/circle-check-white.svg" />
|
||||||
<p>¡Oferta creada! Puedes verla en tus ofertas.</p>
|
<p>¡Oferta creada! Puedes verla en tus ofertas.</p>
|
||||||
</div>
|
</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>
|
<%- include("partials/appCommonScripts") %>
|
||||||
|
<script src="/javascript/offers.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
106
tests/createOffer.spec.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
27
tests/recorderHelper.spec.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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');
|
||||||
|
});
|
||||||
|
*/
|
||||||
72
tests/test-setup.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
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 };
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
invite: './src/front/pages/invite.js',
|
|
||||||
createProfile: './src/front/pages/createProfile.js',
|
|
||||||
login: './src/front/pages/login.js',
|
|
||||||
offers: './src/front/pages/offers.js',
|
|
||||||
home: './src/front/pages/home.js',
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: '[name].bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'public', 'javascript'),
|
|
||||||
},
|
|
||||||
mode: 'development',
|
|
||||||
};
|
|
||||||