Initial commit
This commit is contained in:
commit
24f0a28a3c
25 changed files with 3190 additions and 0 deletions
12
.devcontainer/Dockerfile
Normal file
12
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node modules
|
||||
# RUN su node -c "npm install -g <your-package-list-here>"
|
||||
13
.devcontainer/devcontainer.json
Normal file
13
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
|
||||
{
|
||||
"name": "Node.js & PostgreSQL",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {}
|
||||
}
|
||||
}
|
||||
38
.devcontainer/docker-compose.yml
Normal file
38
.devcontainer/docker-compose.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: node
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Dockerfile.node
|
||||
docker-compose.yml
|
||||
node_modules
|
||||
.env
|
||||
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
11
Dockerfile.node
Normal file
11
Dockerfile.node
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM node:18-bullseye-slim
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN npm install -g pnpm
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
CMD ["pnpm", "start"]
|
||||
144
app.js
Normal file
144
app.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
import createError from 'http-errors';
|
||||
import express, { static as staticRoute } from 'express';
|
||||
import { join, dirname } from 'path';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import session from 'express-session';
|
||||
import bodyParser from 'body-parser';
|
||||
import logger from 'morgan';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createServer } from 'http';
|
||||
|
||||
import { DBAccess } from './db/index.js';
|
||||
await DBAccess.scaffold();
|
||||
|
||||
import indexRouter from './routes/index.js';
|
||||
import userspaceRouter from './routes/userspace.js';
|
||||
import gatewayRouter from './routes/gateway.js';
|
||||
import adminRouter from './routes/admin.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
import nssModule from 'nedb-session-store';
|
||||
const NedbStore = nssModule(session);
|
||||
|
||||
const app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', join(__dirname, 'views'));
|
||||
app.set('view engine', 'hbs');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser("secret")); // TODO: CHANGE!
|
||||
app.use(session({
|
||||
secret: "secret",
|
||||
unset: "destroy",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: new NedbStore({
|
||||
filename: join(__dirname, 'sessions.db')
|
||||
})
|
||||
}))
|
||||
app.use(staticRoute(join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/gateway', gatewayRouter);
|
||||
app.use('/admin', adminRouter);
|
||||
app.use('/userspace', userspaceRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
const port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
const port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
const addr = server.address();
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
console.log('Listening on ' + bind);
|
||||
}
|
||||
245
db/index.js
Normal file
245
db/index.js
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import pgPkg from 'pg';
|
||||
const { Pool } = pgPkg;
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
const parseVoter = (voter) => ({
|
||||
id: parseInt(voter.id),
|
||||
login: voter.login,
|
||||
password: voter.password,
|
||||
full_name: voter.full_name,
|
||||
vgroup_id: parseInt(voter.vgroup_id),
|
||||
is_admin: JSON.parse(voter.is_admin),
|
||||
registration_date: voter.registration_date,
|
||||
});
|
||||
|
||||
const parseVgroup = (vgroup) => ({
|
||||
id: parseInt(vgroup.id),
|
||||
name: vgroup.name,
|
||||
description: vgroup.description,
|
||||
});
|
||||
|
||||
const parseVote = (vote) => ({
|
||||
id: parseInt(vote.id),
|
||||
health: parseInt(vote.health),
|
||||
love: parseInt(vote.love),
|
||||
sex: parseInt(vote.sex),
|
||||
rest: parseInt(vote.rest),
|
||||
finances: parseInt(vote.finances),
|
||||
meaning_of_life: parseInt(vote.meaning_of_life),
|
||||
serenity: parseInt(vote.serenity),
|
||||
relations: parseInt(vote.relations),
|
||||
pers_growth: parseInt(vote.pers_growth),
|
||||
work: parseInt(vote.work),
|
||||
vote_date: vote.vote_date,
|
||||
voter_id: parseInt(vote.voter_id),
|
||||
});
|
||||
|
||||
// TODO: implement
|
||||
const parseVoteResult = (voteResult) => ({
|
||||
|
||||
});
|
||||
|
||||
|
||||
export class DBAccess {
|
||||
static async tableExists(table) {
|
||||
const res = await pool.query("SELECT 1 FROM pg_database WHERE datname = $1", [table]);
|
||||
return res.rows.length > 0;
|
||||
}
|
||||
|
||||
static async scaffold() {
|
||||
await pool.query(`CREATE TABLE IF NOT EXISTS vgroup (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
description VARCHAR(255) NOT NULL DEFAULT ''
|
||||
)`);
|
||||
await pool.query(`CREATE TABLE IF NOT EXISTS voter (
|
||||
id SERIAL PRIMARY KEY,
|
||||
login VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
vgroup_id INT,
|
||||
full_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
is_admin BOOLEAN NOT NULL DEFAULT false,
|
||||
registration_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_vgroup FOREIGN KEY(vgroup_id) REFERENCES vgroup(id)
|
||||
)`);
|
||||
await pool.query(`CREATE TABLE IF NOT EXISTS vote (
|
||||
id SERIAL PRIMARY KEY,
|
||||
health INT NOT NULL,
|
||||
love INT NOT NULL,
|
||||
sex INT NOT NULL,
|
||||
rest INT NOT NULL,
|
||||
finances INT NOT NULL,
|
||||
meaning_of_life INT NOT NULL,
|
||||
serenity INT NOT NULL,
|
||||
relations INT NOT NULL,
|
||||
pers_growth INT NOT NULL,
|
||||
work INT NOT NULL,
|
||||
vote_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
voter_id INT NOT NULL,
|
||||
CONSTRAINT voter FOREIGN KEY(voter_id) REFERENCES voter(id)
|
||||
)`);
|
||||
}
|
||||
|
||||
static async dropAll() {
|
||||
await pool.query("DROP TABLE IF EXISTS vote");
|
||||
await pool.query("DROP TABLE IF EXISTS voter");
|
||||
await pool.query("DROP TABLE IF EXISTS vgroup");
|
||||
}
|
||||
|
||||
static query(text, params, callback) {
|
||||
return pool.query(text, params, callback);
|
||||
}
|
||||
|
||||
static async queryAsync(text, params) {
|
||||
return pool.query(text, params);
|
||||
}
|
||||
|
||||
static async countVoters() {
|
||||
const res = await pool.query("SELECT COUNT(*) FROM voter");
|
||||
return parseInt(res.rows[0].count);
|
||||
}
|
||||
|
||||
static async findVoterByLogin(login) {
|
||||
const res = await pool.query("SELECT * FROM voter WHERE login = $1", [login]);
|
||||
if (res.rows.length != 0) {
|
||||
return parseVoter(res.rows[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async validateVoter(login, password) {
|
||||
const res = await pool.query("SELECT * FROM voter WHERE login = $1 AND password = $2", [login, password]);
|
||||
if (res.rows.length != 0) {
|
||||
return parseVoter(res.rows[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async createVoter(login, password, full_name, is_admin) {
|
||||
let res = await pool.query(
|
||||
"INSERT INTO voter(login, password, full_name, is_admin) VALUES ($1, $2, $3, $4) RETURNING *",
|
||||
[login, password, full_name, is_admin]
|
||||
);
|
||||
if (res.rows.length != 0) {
|
||||
if (is_admin) {
|
||||
let adminGroup = await pool.query("SELECT * FROM vgroup WHERE name = $1", ["admin"]);
|
||||
if (adminGroup.rows.length === 0) {
|
||||
adminGroup = await pool.query("INSERT INTO vgroup(name, description) VALUES ($1, $2) RETURNING *", ["admin", "Администраторы"]);
|
||||
}
|
||||
res = await pool.query("UPDATE voter SET vgroup_id = $1 WHERE login = $2 RETURNING *", [adminGroup.rows[0].id, login]);
|
||||
}
|
||||
if (res.rows.length != 0) {
|
||||
return parseVoter(res.rows[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async getVoters() {
|
||||
const res = await pool.query("SELECT * FROM voter ORDER BY registration_date");
|
||||
return res.rows.map(parseVoter);
|
||||
}
|
||||
|
||||
static async getVoterById(id) {
|
||||
const res = await pool.query("SELECT * FROM voter WHERE id = $1", [id]);
|
||||
if (res.rows.length != 0) {
|
||||
return parseVoter(res.rows[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async deleteVoterVotes(id) {
|
||||
await pool.query("DELETE FROM vote WHERE voter_id = $1", [id]);
|
||||
}
|
||||
|
||||
static async deleteVoter(id) {
|
||||
await this.deleteVoterVotes(id);
|
||||
await pool.query("DELETE FROM voter WHERE id = $1", [id]);
|
||||
}
|
||||
|
||||
static async promoteVoter(id, vgroup_id, setAdmin) {
|
||||
const user = await this.getVoterById(id);
|
||||
const admCount = (await this.getVoters()).reduce((acc, item) => acc + (item.is_admin ? 1 : 0), 0);
|
||||
if (user) {
|
||||
if (user.is_admin && admCount === 1) {
|
||||
throw new Error("Нельзя удалить последнего администратора");
|
||||
}
|
||||
if (setAdmin) {
|
||||
await pool.query("UPDATE voter SET vgroup_id = $1, is_admin = true WHERE id = $2", [vgroup_id, id]);
|
||||
} else {
|
||||
await pool.query("UPDATE voter SET vgroup_id = $1, is_admin = false WHERE id = $2", [vgroup_id, id]);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Такого пользователя нет");
|
||||
}
|
||||
}
|
||||
|
||||
static async getVgroups() {
|
||||
const res = await pool.query("SELECT * FROM vgroup");
|
||||
return res.rows.map(parseVgroup);
|
||||
}
|
||||
|
||||
static async createVgroup(name, description) {
|
||||
const res = await pool.query("INSERT INTO vgroup(name, description) VALUES ($1, $2) RETURNING *", [name, description]);
|
||||
if (res.rows.length != 0) {
|
||||
return parseVgroup(res.rows[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async deleteVgroup(id) {
|
||||
await pool.query("DELETE FROM vgroup WHERE id = $1", [id]);
|
||||
}
|
||||
|
||||
static async createVote(vote) {
|
||||
const res = await pool.query(`INSERT INTO vote(
|
||||
health, love, sex, rest, relations, pers_growth, work,
|
||||
finances, meaning_of_life, serenity, voter_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, [
|
||||
vote.health, vote.love, vote.sex, vote.rest, vote.relations, vote.pers_growth, vote.work,
|
||||
vote.finances, vote.meaning_of_life, vote.serenity, vote.voter_id
|
||||
]);
|
||||
if (res.rows.length != 0) {
|
||||
return parseVote(res.rows[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static async getVotes() {
|
||||
const res = await pool.query("SELECT * FROM vote ORDER BY vote.vote_date desc");
|
||||
return res.rows.map(parseVote);
|
||||
}
|
||||
|
||||
static async getVotesByVoterId(voter_id) {
|
||||
const res = await pool.query("SELECT * FROM vote WHERE voter_id = $1", [voter_id]);
|
||||
return res.rows.map(parseVote);
|
||||
}
|
||||
|
||||
static async deleteVote(id) {
|
||||
await pool.query("DELETE FROM vote WHERE id = $1", [id]);
|
||||
}
|
||||
|
||||
static async getGrouppedVotes() {
|
||||
const res = await pool.query(`
|
||||
select
|
||||
array_agg(v.health) as health, array_agg(v.love) as love, array_agg(v.sex) as sex, array_agg(v.rest) as rest, array_agg(v.finances) as finances,
|
||||
array_agg(v.meaning_of_life) as meaning_of_life, array_agg(v.serenity) as serenity, array_agg(v.relations) as relations,
|
||||
array_agg(v.pers_growth) as pers_growth, array_agg(v.work) as work,
|
||||
v1.vgroup_id, to_char(v.vote_date, 'mm.yyyy') as vote_date
|
||||
from vote as v
|
||||
inner join voter v1 on v1.id = v.voter_id
|
||||
group by v1.vgroup_id, to_char(v.vote_date, 'mm.yyyy')
|
||||
order by to_char(v.vote_date, 'mm.yyyy') asc`.trim());
|
||||
return res.rows;//.map(parseVote);
|
||||
}
|
||||
|
||||
static async didUserBotedThisMonth(voter_id) {
|
||||
const res = await pool.query("SELECT * FROM vote WHERE voter_id = $1 AND vote_date >= date_trunc('month', now())", [voter_id]);
|
||||
return res.rows.length != 0;
|
||||
}
|
||||
|
||||
static async shutdown() {
|
||||
return await pool.end();
|
||||
}
|
||||
}
|
||||
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nuark/balance_wheel:latest
|
||||
environment:
|
||||
- PGHOST='db'
|
||||
- PGUSER=postgres
|
||||
- PGDATABASE=postgres
|
||||
- PGPASSWORD=postgres
|
||||
- PGPORT=5432
|
||||
ports:
|
||||
- 8301:3000
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "balance-wheel",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"watch": "nodemon -L --watch public --watch routes --watch views --watch db -e js,css,html,hbs app.js",
|
||||
"docker-release": "docker build -f Dockerfile.node -t nuark/balance_wheel:latest . && docker push nuark/balance_wheel:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "~4.16.1",
|
||||
"express-session": "^1.17.3",
|
||||
"hbs": "~4.0.4",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"nedb-session-store": "^1.1.2",
|
||||
"pg": "^8.8.0"
|
||||
}
|
||||
}
|
||||
808
pnpm-lock.yaml
generated
Normal file
808
pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
body-parser: ^1.20.1
|
||||
cookie-parser: ~1.4.4
|
||||
debug: ~2.6.9
|
||||
dotenv: ^16.0.3
|
||||
express: ~4.16.1
|
||||
express-session: ^1.17.3
|
||||
hbs: ~4.0.4
|
||||
http-errors: ~1.6.3
|
||||
morgan: ~1.9.1
|
||||
nedb-session-store: ^1.1.2
|
||||
pg: ^8.8.0
|
||||
|
||||
dependencies:
|
||||
body-parser: 1.20.1
|
||||
cookie-parser: 1.4.6
|
||||
debug: 2.6.9
|
||||
dotenv: 16.0.3
|
||||
express: 4.16.4
|
||||
express-session: 1.17.3
|
||||
hbs: 4.0.6
|
||||
http-errors: 1.6.3
|
||||
morgan: 1.9.1
|
||||
nedb-session-store: 1.1.2
|
||||
pg: 8.8.0
|
||||
|
||||
packages:
|
||||
|
||||
/accepts/1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
negotiator: 0.6.3
|
||||
dev: false
|
||||
|
||||
/array-flatten/1.1.1:
|
||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||
dev: false
|
||||
|
||||
/async/0.2.10:
|
||||
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
|
||||
dev: false
|
||||
|
||||
/basic-auth/2.0.1:
|
||||
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
dev: false
|
||||
|
||||
/binary-search-tree/0.2.5:
|
||||
resolution: {integrity: sha512-CvNVKS6iXagL1uGwLagSXz1hzSMezxOuGnFi5FHGKqaTO3nPPWrAbyALUzK640j+xOTVm7lzD9YP8W1f/gvUdw==}
|
||||
dependencies:
|
||||
underscore: 1.4.4
|
||||
dev: false
|
||||
|
||||
/body-parser/1.18.3:
|
||||
resolution: {integrity: sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.0.0
|
||||
content-type: 1.0.4
|
||||
debug: 2.6.9
|
||||
depd: 1.1.2
|
||||
http-errors: 1.6.3
|
||||
iconv-lite: 0.4.23
|
||||
on-finished: 2.3.0
|
||||
qs: 6.5.2
|
||||
raw-body: 2.3.3
|
||||
type-is: 1.6.18
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/body-parser/1.20.1:
|
||||
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.4
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
destroy: 1.2.0
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.11.0
|
||||
raw-body: 2.5.1
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/buffer-writer/2.0.0:
|
||||
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/bytes/3.0.0:
|
||||
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/bytes/3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/call-bind/1.0.2:
|
||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
get-intrinsic: 1.1.3
|
||||
dev: false
|
||||
|
||||
/content-disposition/0.5.2:
|
||||
resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/content-type/1.0.4:
|
||||
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie-parser/1.4.6:
|
||||
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.1
|
||||
cookie-signature: 1.0.6
|
||||
dev: false
|
||||
|
||||
/cookie-signature/1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
dev: false
|
||||
|
||||
/cookie/0.3.1:
|
||||
resolution: {integrity: sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie/0.4.1:
|
||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie/0.4.2:
|
||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: false
|
||||
|
||||
/depd/1.1.2:
|
||||
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/depd/2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/destroy/1.0.4:
|
||||
resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==}
|
||||
dev: false
|
||||
|
||||
/destroy/1.2.0:
|
||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dev: false
|
||||
|
||||
/dotenv/16.0.3:
|
||||
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/ee-first/1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
dev: false
|
||||
|
||||
/encodeurl/1.0.2:
|
||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/escape-html/1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
dev: false
|
||||
|
||||
/etag/1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/express-session/1.17.3:
|
||||
resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.2
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
on-headers: 1.0.2
|
||||
parseurl: 1.3.3
|
||||
safe-buffer: 5.2.1
|
||||
uid-safe: 2.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/express/4.16.4:
|
||||
resolution: {integrity: sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.18.3
|
||||
content-disposition: 0.5.2
|
||||
content-type: 1.0.4
|
||||
cookie: 0.3.1
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 1.1.2
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 1.1.1
|
||||
fresh: 0.5.2
|
||||
merge-descriptors: 1.0.1
|
||||
methods: 1.1.2
|
||||
on-finished: 2.3.0
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.7
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.5.2
|
||||
range-parser: 1.2.1
|
||||
safe-buffer: 5.1.2
|
||||
send: 0.16.2
|
||||
serve-static: 1.13.2
|
||||
setprototypeof: 1.1.0
|
||||
statuses: 1.4.0
|
||||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/finalhandler/1.1.1:
|
||||
resolution: {integrity: sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.3.0
|
||||
parseurl: 1.3.3
|
||||
statuses: 1.4.0
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/foreachasync/3.0.0:
|
||||
resolution: {integrity: sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==}
|
||||
dev: false
|
||||
|
||||
/forwarded/0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/fresh/0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/function-bind/1.1.1:
|
||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||
dev: false
|
||||
|
||||
/get-intrinsic/1.1.3:
|
||||
resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
has: 1.0.3
|
||||
has-symbols: 1.0.3
|
||||
dev: false
|
||||
|
||||
/handlebars/4.3.5:
|
||||
resolution: {integrity: sha512-I16T/l8X9DV3sEkY9sK9lsPRgDsj82ayBY/4pAZyP2BcX5WeRM3O06bw9kIs2GLrHvFB/DNzWWJyFvof8wQGqw==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
neo-async: 2.6.2
|
||||
optimist: 0.6.1
|
||||
source-map: 0.6.1
|
||||
optionalDependencies:
|
||||
uglify-js: 3.17.4
|
||||
dev: false
|
||||
|
||||
/has-symbols/1.0.3:
|
||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: false
|
||||
|
||||
/has/1.0.3:
|
||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
dev: false
|
||||
|
||||
/hbs/4.0.6:
|
||||
resolution: {integrity: sha512-KFt3Y4zOvVQOp84TmqVaFTpBTYO1sVenBoBY712MI3vPkKxVoO6AsuEyDayIRPRAHRYZHHWnmc4spFa8fhQpLw==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dependencies:
|
||||
handlebars: 4.3.5
|
||||
walk: 2.3.14
|
||||
dev: false
|
||||
|
||||
/http-errors/1.6.3:
|
||||
resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
depd: 1.1.2
|
||||
inherits: 2.0.3
|
||||
setprototypeof: 1.1.0
|
||||
statuses: 1.5.0
|
||||
dev: false
|
||||
|
||||
/http-errors/2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
dev: false
|
||||
|
||||
/iconv-lite/0.4.23:
|
||||
resolution: {integrity: sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/iconv-lite/0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/immediate/3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
dev: false
|
||||
|
||||
/inherits/2.0.3:
|
||||
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
|
||||
dev: false
|
||||
|
||||
/inherits/2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/ipaddr.js/1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dev: false
|
||||
|
||||
/lie/3.1.1:
|
||||
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
dev: false
|
||||
|
||||
/localforage/1.10.0:
|
||||
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
|
||||
dependencies:
|
||||
lie: 3.1.1
|
||||
dev: false
|
||||
|
||||
/media-typer/0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/merge-descriptors/1.0.1:
|
||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||
dev: false
|
||||
|
||||
/methods/1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-types/2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: false
|
||||
|
||||
/mime/1.4.1:
|
||||
resolution: {integrity: sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/minimist/0.0.10:
|
||||
resolution: {integrity: sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==}
|
||||
dev: false
|
||||
|
||||
/minimist/1.2.7:
|
||||
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
|
||||
dev: false
|
||||
|
||||
/mkdirp/0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
minimist: 1.2.7
|
||||
dev: false
|
||||
|
||||
/morgan/1.9.1:
|
||||
resolution: {integrity: sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
basic-auth: 2.0.1
|
||||
debug: 2.6.9
|
||||
depd: 1.1.2
|
||||
on-finished: 2.3.0
|
||||
on-headers: 1.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: false
|
||||
|
||||
/nedb-session-store/1.1.2:
|
||||
resolution: {integrity: sha512-ahASrYzxP17k7Rn9AHIMMkFj8ibGWa8m1MJVajIP89HnCRyk93H7VtQReiiz/xiBFKxDm9MwOB71OAPv1Ybhzg==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
dependencies:
|
||||
nedb: 1.8.0
|
||||
dev: false
|
||||
|
||||
/nedb/1.8.0:
|
||||
resolution: {integrity: sha512-ip7BJdyb5m+86ZbSb4y10FCCW9g35+U8bDRrZlAfCI6m4dKwEsQ5M52grcDcVK4Vm/vnPlDLywkyo3GliEkb5A==}
|
||||
dependencies:
|
||||
async: 0.2.10
|
||||
binary-search-tree: 0.2.5
|
||||
localforage: 1.10.0
|
||||
mkdirp: 0.5.6
|
||||
underscore: 1.4.4
|
||||
dev: false
|
||||
|
||||
/negotiator/0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/neo-async/2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
dev: false
|
||||
|
||||
/object-inspect/1.12.2:
|
||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||
dev: false
|
||||
|
||||
/on-finished/2.3.0:
|
||||
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
dev: false
|
||||
|
||||
/on-finished/2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
dev: false
|
||||
|
||||
/on-headers/1.0.2:
|
||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/optimist/0.6.1:
|
||||
resolution: {integrity: sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==}
|
||||
dependencies:
|
||||
minimist: 0.0.10
|
||||
wordwrap: 0.0.3
|
||||
dev: false
|
||||
|
||||
/packet-reader/1.0.0:
|
||||
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
|
||||
dev: false
|
||||
|
||||
/parseurl/1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/path-to-regexp/0.1.7:
|
||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||
dev: false
|
||||
|
||||
/pg-connection-string/2.5.0:
|
||||
resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==}
|
||||
dev: false
|
||||
|
||||
/pg-int8/1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/pg-pool/3.5.2_pg@8.8.0:
|
||||
resolution: {integrity: sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
dependencies:
|
||||
pg: 8.8.0
|
||||
dev: false
|
||||
|
||||
/pg-protocol/1.5.0:
|
||||
resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==}
|
||||
dev: false
|
||||
|
||||
/pg-types/2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
dev: false
|
||||
|
||||
/pg/8.8.0:
|
||||
resolution: {integrity: sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
buffer-writer: 2.0.0
|
||||
packet-reader: 1.0.0
|
||||
pg-connection-string: 2.5.0
|
||||
pg-pool: 3.5.2_pg@8.8.0
|
||||
pg-protocol: 1.5.0
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
dev: false
|
||||
|
||||
/pgpass/1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
dependencies:
|
||||
split2: 4.1.0
|
||||
dev: false
|
||||
|
||||
/postgres-array/2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/postgres-bytea/1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/postgres-date/1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/postgres-interval/1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
dev: false
|
||||
|
||||
/proxy-addr/2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
dev: false
|
||||
|
||||
/qs/6.11.0:
|
||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
dependencies:
|
||||
side-channel: 1.0.4
|
||||
dev: false
|
||||
|
||||
/qs/6.5.2:
|
||||
resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/random-bytes/1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/raw-body/2.3.3:
|
||||
resolution: {integrity: sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.0.0
|
||||
http-errors: 1.6.3
|
||||
iconv-lite: 0.4.23
|
||||
unpipe: 1.0.0
|
||||
dev: false
|
||||
|
||||
/raw-body/2.5.1:
|
||||
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
dev: false
|
||||
|
||||
/safe-buffer/5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
dev: false
|
||||
|
||||
/safe-buffer/5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/safer-buffer/2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/send/0.16.2:
|
||||
resolution: {integrity: sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
depd: 1.1.2
|
||||
destroy: 1.0.4
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 1.6.3
|
||||
mime: 1.4.1
|
||||
ms: 2.0.0
|
||||
on-finished: 2.3.0
|
||||
range-parser: 1.2.1
|
||||
statuses: 1.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/serve-static/1.13.2:
|
||||
resolution: {integrity: sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
encodeurl: 1.0.2
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.16.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/setprototypeof/1.1.0:
|
||||
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
|
||||
dev: false
|
||||
|
||||
/setprototypeof/1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
dev: false
|
||||
|
||||
/side-channel/1.0.4:
|
||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.1.3
|
||||
object-inspect: 1.12.2
|
||||
dev: false
|
||||
|
||||
/source-map/0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/split2/4.1.0:
|
||||
resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==}
|
||||
engines: {node: '>= 10.x'}
|
||||
dev: false
|
||||
|
||||
/statuses/1.4.0:
|
||||
resolution: {integrity: sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/statuses/1.5.0:
|
||||
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/statuses/2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/toidentifier/1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/type-is/1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
dev: false
|
||||
|
||||
/uglify-js/3.17.4:
|
||||
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/uid-safe/2.1.5:
|
||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
random-bytes: 1.0.0
|
||||
dev: false
|
||||
|
||||
/underscore/1.4.4:
|
||||
resolution: {integrity: sha512-ZqGrAgaqqZM7LGRzNjLnw5elevWb5M8LEoDMadxIW3OWbcv72wMMgKdwOKpd5Fqxe8choLD8HN3iSj3TUh/giQ==}
|
||||
dev: false
|
||||
|
||||
/unpipe/1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/utils-merge/1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dev: false
|
||||
|
||||
/vary/1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/walk/2.3.14:
|
||||
resolution: {integrity: sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==}
|
||||
dependencies:
|
||||
foreachasync: 3.0.0
|
||||
dev: false
|
||||
|
||||
/wordwrap/0.0.3:
|
||||
resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/xtend/4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
dev: false
|
||||
13
public/javascripts/chart.min.js
vendored
Normal file
13
public/javascripts/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/javascripts/chartjs-plugin-datalabels-v2.1.0.js
Normal file
7
public/javascripts/chartjs-plugin-datalabels-v2.1.0.js
Normal file
File diff suppressed because one or more lines are too long
47
public/stylesheets/style.css
Normal file
47
public/stylesheets/style.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[x-cloak] { opacity: 0 !important; }
|
||||
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-fit-content {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.w-15 {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-2 {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.wm-vlr{
|
||||
writing-mode: vertical-lr;
|
||||
}
|
||||
|
||||
.fading-bg {
|
||||
transition: background-color .5s;
|
||||
}
|
||||
|
||||
.stats-cell {
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.status-dangerous {
|
||||
background-color: rgb(250, 217, 210) !important;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: rgb(252, 253, 177) !important;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background-color: rgb(225, 240, 181) !important;
|
||||
}
|
||||
146
routes/admin.js
Normal file
146
routes/admin.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { DBAccess } from "../db/index.js";
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
if (req.session.isAdmin) {
|
||||
next();
|
||||
} else {
|
||||
throw new Error("Вы не являетесь администраторром");
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/", async function (req, res, next) {
|
||||
res.render("admin/index", { title: "Администратор" });
|
||||
});
|
||||
|
||||
router.get("/voters", async function (req, res, next) {
|
||||
const voters = await DBAccess.getVoters();
|
||||
res.json(voters);
|
||||
});
|
||||
|
||||
router.get("/voters/:id/promote/:vgid", async function (req, res, next) {
|
||||
const isAcceptJson = req.accepts("json");
|
||||
const { id, vgid } = req.params;
|
||||
try {
|
||||
if (id == req.session.userid) {
|
||||
throw new Error("Нельзя изменить свою группу");
|
||||
}
|
||||
await DBAccess.promoteVoter(id, vgid, req.session.vgroup_id == vgid);
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "ok" });
|
||||
} else {
|
||||
res.redirect("/admin");
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "error", error: e.message });
|
||||
} else {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка изменения группы", e.message] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/voters/create", async function (req, res, next) {
|
||||
const { login, password, full_name } = req.body;
|
||||
try {
|
||||
await DBAccess.createVoter(login, password, full_name, false);
|
||||
res.redirect("/admin");
|
||||
} catch (e) {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка создания пользователя", e.message] });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/voters/:id/delete", async function (req, res, next) {
|
||||
const isAcceptJson = req.accepts("json");
|
||||
const { id } = req.params;
|
||||
try {
|
||||
if (id == req.session.userid) {
|
||||
throw new Error("Нельзя удалить самого себя");
|
||||
}
|
||||
await DBAccess.deleteVoter(id);
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "ok" });
|
||||
} else {
|
||||
res.redirect("/admin");
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "error", error: e.message });
|
||||
} else {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка удаления пользователя", e.message] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/vgroups", async function (req, res, next) {
|
||||
const vgroups = await DBAccess.getVgroups();
|
||||
res.json(vgroups);
|
||||
});
|
||||
|
||||
router.post("/vgroups/create", async function (req, res, next) {
|
||||
const { name, description } = req.body;
|
||||
if (name.length < 3) {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Минимальная длина названия группы - три символа"] });
|
||||
} else {
|
||||
const vgroup = await DBAccess.createVgroup(name, description || "");
|
||||
if (vgroup) {
|
||||
res.redirect("/admin");
|
||||
} else {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка создания группы"] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/vgroups/:id/delete", async function (req, res, next) {
|
||||
const isAcceptJson = req.accepts("json");
|
||||
const { id } = req.params;
|
||||
try {
|
||||
if (id == req.session.vgroup_id) {
|
||||
throw new Error("Нельзя удалить свою группу");
|
||||
}
|
||||
await DBAccess.deleteVgroup(id);
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "ok" });
|
||||
} else {
|
||||
res.redirect("/admin");
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "error", error: e.message });
|
||||
} else {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка удаления группы", e.message] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/votes", async function (req, res, next) {
|
||||
const votes = await DBAccess.getVotes();
|
||||
res.json(votes);
|
||||
});
|
||||
|
||||
router.get("/votes/groupped", async function (req, res, next) {
|
||||
const grouppedVotes = await DBAccess.getGrouppedVotes();
|
||||
res.json(grouppedVotes);
|
||||
});
|
||||
|
||||
router.get("/votes/:id/delete", async function (req, res, next) {
|
||||
const isAcceptJson = req.accepts("json");
|
||||
const { id } = req.params;
|
||||
try {
|
||||
await DBAccess.deleteVote(id);
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "ok" });
|
||||
} else {
|
||||
res.redirect("/admin");
|
||||
}
|
||||
} catch (e) {
|
||||
if (isAcceptJson) {
|
||||
res.json({ status: "error", error: e.message });
|
||||
} else {
|
||||
res.render("admin/index", { title: "Администратор", flashes: ["Ошибка удаления голоса", e.message] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
53
routes/gateway.js
Normal file
53
routes/gateway.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { DBAccess } from '../db/index.js';
|
||||
import { Router } from 'express';
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async function (req, res, next) {
|
||||
if (req.session.userid) {
|
||||
res.redirect("/");
|
||||
} else {
|
||||
res.render("gateway/index", { title: "Гейтвей" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/logout", async function (req, res, next) {
|
||||
req.session.destroy(e => e && console.error(e));
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
router.post("/login", async function (req, res, next) {
|
||||
const { login, password } = req.body;
|
||||
const voter = await DBAccess.validateVoter(login, password);
|
||||
console.dir(voter);
|
||||
if (voter) {
|
||||
req.session.userid = voter.id;
|
||||
req.session.login = voter.login;
|
||||
req.session.isAdmin = voter.is_admin;
|
||||
req.session.vgroup_id = voter.vgroup_id;
|
||||
req.session.full_name = voter.full_name;
|
||||
res.redirect("/");
|
||||
} else {
|
||||
res.render("gateway/index", { title: "Гейтвей", flashes: ["Неверный логин или пароль"] });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/register", async function (req, res, next) {
|
||||
const { login, password, full_name } = req.body;
|
||||
if (!login || !password || !full_name || login.length < 5 || password.length < 4 || full_name.length < 4) {
|
||||
res.render("gateway/index", { title: "Гейтвей", flashes: ["Проверьте данные"] });
|
||||
} else if (await DBAccess.findVoterByLogin(login)) {
|
||||
res.render("gateway/index", { title: "Гейтвей", flashes: ["Пользователь с таким логином уже существует"] });
|
||||
} else {
|
||||
const firstUser = await DBAccess.countVoters() === 0;
|
||||
const voter = await DBAccess.createVoter(login, password, full_name, firstUser);
|
||||
console.dir(voter);
|
||||
req.session.userid = voter.id;
|
||||
req.session.login = voter.login;
|
||||
req.session.isAdmin = voter.is_admin;
|
||||
req.session.vgroup_id = voter.vgroup_id;
|
||||
req.session.full_name = voter.full_name;
|
||||
res.redirect("/");
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
14
routes/index.js
Normal file
14
routes/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Router } from 'express';
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async function (req, res, next) {
|
||||
if (req.session.isAdmin) {
|
||||
res.redirect("/admin");
|
||||
} else if (req.session.userid) {
|
||||
res.redirect("/userspace");
|
||||
} else {
|
||||
res.redirect("/gateway");
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
71
routes/userspace.js
Normal file
71
routes/userspace.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { DBAccess } from '../db/index.js';
|
||||
import { Router } from 'express';
|
||||
const router = Router();
|
||||
|
||||
router.use(function (req, res, next) {
|
||||
console.dir(req.session);
|
||||
if (req.session.login) {
|
||||
next();
|
||||
} else {
|
||||
throw new Error("Необходима авторизация");
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
res.render("userspace/index", {
|
||||
title: "Голосование", user: {
|
||||
login: req.session.login,
|
||||
full_name: req.session.full_name,
|
||||
vgroup_id: parseInt(req.session.vgroup_id),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/vote", async function (req, res, next) {
|
||||
const bData = req.body || [];
|
||||
try {
|
||||
const userVotedTihsMonth = await DBAccess.didUserBotedThisMonth(req.session.userid);
|
||||
if (userVotedTihsMonth) {
|
||||
throw new Error("Вы уже голосовали в этом месяце");
|
||||
}
|
||||
|
||||
if (bData.length === 0) {
|
||||
throw new Error("Нет данных для голосования");
|
||||
} else {
|
||||
const data = (req.body || []).reduce((acc, item) => {
|
||||
acc[item.dbmap] = item.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
data.voter_id = parseInt(req.session.userid);
|
||||
const vote = DBAccess.createVote(data);
|
||||
if (vote) {
|
||||
res.json({ status: "ok" });
|
||||
} else {
|
||||
throw new Error("Ошибка голосования");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
res.json({ status: "error", message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/votes", async function (req, res, next) {
|
||||
const userVotes = await DBAccess.getVotesByVoterId(parseInt(req.session.userid));
|
||||
res.render("userspace/votes", {
|
||||
title: "История",
|
||||
user: {
|
||||
login: req.session.login,
|
||||
full_name: req.session.full_name,
|
||||
vgroup_id: parseInt(req.session.vgroup_id),
|
||||
},
|
||||
votes: userVotes.map(v => {
|
||||
return {
|
||||
...v,
|
||||
date: new Date(v.vote_date).toLocaleString("ru-RU"),
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
13
sessions.db
Normal file
13
sessions.db
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_id":"En5Q3EXWdMS0zlCOqrlQVT2iZemMfdQ7","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1671091152143},"createdAt":{"$$date":1669881552144},"updatedAt":{"$$date":1669896772607}}
|
||||
{"_id":"OXTzYqMqz80Xz5JeC8LDpai0hY-GVlei","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1669982116280},"createdAt":{"$$date":1668772516283},"updatedAt":{"$$date":1668777024315}}
|
||||
{"_id":"fR3mx8-8UwHOV6OhGRS-knjzMhjBEHp7","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1670218702875},"createdAt":{"$$date":1669009102879},"updatedAt":{"$$date":1669016030399}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433031459}}
|
||||
{"_id":"yd28RrwwX7ftM2YFtzApWs8XAK_f_OOf","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1670185656460},"createdAt":{"$$date":1668976056468},"updatedAt":{"$$date":1668984561459}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433050303}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433050524}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433050534}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433050543}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433051287}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433051549}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433051640}}
|
||||
{"_id":"xsiAQvQKqRkOMM-AhhtF7Gulg5qcv1BB","session":{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"},"userid":1,"login":"admin","isAdmin":true,"vgroup_id":1,"full_name":"Admin"},"expiresAt":{"$$date":1672642116725},"createdAt":{"$$date":1671432516728},"updatedAt":{"$$date":1671433051829}}
|
||||
892
views/admin/index.hbs
Normal file
892
views/admin/index.hbs
Normal file
|
|
@ -0,0 +1,892 @@
|
|||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: 25rem;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="root" x-data="{ loading: true }" x-init="fetchData()" x-ref="root">
|
||||
<div x-show="loading" class="position-absolute spinner-border start-50 top-50" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div x-show="!loading" x-data="{ tab: $persist('vgroups') }" x-ref="tabs-holder" x-cloak x-transition>
|
||||
<header class="navbar navbar-expand-md navbar-dark bd-navbar">
|
||||
<ul class="nav nav-pills container flex-wrap flex-md-nowrap gap-2">
|
||||
<li class="nav-item" @click="tab = 'vgroups'">
|
||||
<a :class="{ 'active': tab === 'vgroups' }" @click.prevent="tab = 'vgroups'" href="#vgroups" class="nav-link" aria-current="page" href="#vgroups">Группы</a>
|
||||
</li>
|
||||
<li class="nav-item" @click="tab = 'voters'">
|
||||
<a :class="{ 'active': tab === 'voters' }" @click.prevent="tab = 'voters'" href="#voters" class="nav-link" href="#voters">Пользователи</a>
|
||||
</li>
|
||||
<li class="nav-item" @click="tab = 'votes'">
|
||||
<a :class="{ 'active': tab === 'votes' }" @click.prevent="tab = 'votes'" href="#votes" class="nav-link" href="#votes">Голоса</a>
|
||||
</li>
|
||||
<li class="nav-item" @click="tab = 'analysis'">
|
||||
<a :class="{ 'active': tab === 'analysis' }" @click.prevent="tab = 'analysis'" href="#analysis" class="nav-link" href="#analysis">Анализ</a>
|
||||
</li>
|
||||
<div class="flex-grow-1"></div>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/gateway/logout">Выйти</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div class="container p-0">
|
||||
<div x-show="tab === 'vgroups'">
|
||||
<form x-data="{ newModelName: '', newModelDescription: '' }" class="mb-4 d-flex flex-row gap-2" action="/admin/vgroups/create" method="POST">
|
||||
<input type="text" x-model="newModelName" name="name" class="form-control form-control-sm flex-grow-1" placeholder="Название">
|
||||
<input type="text" x-model="newModelDescription" name="description" class="form-control form-control-sm flex-grow-1" placeholder="Описание">
|
||||
<button x-bind:disabled="newModelName.length < 3" class="btn btn-info btn-sm w-50">Создать новую группу</button>
|
||||
</form>
|
||||
<table x-data="{ titleSearch: '' }" id="vgroupsTable" class="w-100 table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle text-center">ID</th>
|
||||
<th scope="col" class="align-middle text-start">
|
||||
Название
|
||||
<input type="text" class="form-control form-control-sm" placeholder="Поиск"
|
||||
@input="titleSearch = $event.target.value;">
|
||||
</th>
|
||||
<th scope="col" class="align-middle text-center">Описание</th>
|
||||
<th scope="col" class="align-middle text-center">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<template x-if="!$store.dataset.vgroups">
|
||||
<tr><td colspan="4" scope="row"><i>Loading...</i></td></tr>
|
||||
</template>
|
||||
<template x-for="vgroup in $store.dataset.vgroups">
|
||||
<tr x-show="vgroup.name.includes(titleSearch)">
|
||||
<td x-text="vgroup.id" scope="row"></td>
|
||||
<td x-text="vgroup.name"></td>
|
||||
<td x-text="vgroup.description || '[Нет описания]'"></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
@click="deleteVgroup(vgroup.id)">
|
||||
Удалить
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'voters'">
|
||||
<form x-data="{ newVoterFullName: '', newVoterLogin: '', newVoterPassword: '' }" class="mb-4 d-flex flex-row gap-2" action="/admin/voters/create" method="POST">
|
||||
<input type="text" x-model="newVoterFullName" name="full_name" class="form-control form-control-sm flex-grow-1" placeholder="Полное имя">
|
||||
<input type="text" x-model="newVoterLogin" name="login" class="form-control form-control-sm flex-grow-1" placeholder="Логин">
|
||||
<input type="text" x-model="newVoterPassword" name="password" class="form-control form-control-sm flex-grow-1" placeholder="Пароль">
|
||||
<button x-bind:disabled="newVoterFullName.length < 4 || newVoterLogin.length < 5 || newVoterPassword.length < 4" class="btn btn-info btn-sm w-50">Создать пользователя</button>
|
||||
</form>
|
||||
<table x-data="{ groupFilter: -1 }" id="votersTable" class="w-100 table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle text-center">ID</th>
|
||||
<th scope="col" class="align-middle text-center">Имя</th>
|
||||
<th scope="col" class="align-middle text-center">Логин</th>
|
||||
<th scope="col" class="align-middle text-start">
|
||||
Группа
|
||||
<select class="form-select form-select-sm" @change="groupFilter = $event.target.value">
|
||||
<option value="-1" selected>Без фильтра</option>
|
||||
<template x-for="vgroup in $store.dataset.vgroups">
|
||||
<option x-bind:value="vgroup.id" x-text="vgroup.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</th>
|
||||
<th scope="col" class="align-middle text-center">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<template x-if="!$store.dataset.voters">
|
||||
<tr><td colspan="5" scope="row"><i>Loading...</i></td></tr>
|
||||
</template>
|
||||
<template x-for="voter in $store.dataset.voters">
|
||||
<tr x-show="groupFilter == -1 || groupFilter == voter.vgroup_id">
|
||||
<td x-text="voter.id" scope="row"></td>
|
||||
<td x-text="voter.full_name"></td>
|
||||
<td x-text="voter.login"></td>
|
||||
<td>
|
||||
<template x-if="!$store.dataset.vgroups">
|
||||
<tr><td colspan="4" scope="row"><i>Loading...</i></td></tr>
|
||||
</template>
|
||||
<select class="form-select form-select-sm"
|
||||
@change="updateUserGroup(voter.id, $event.target.value)">
|
||||
<option value="null" x-bind:selected="voter.vgroup_id === null">Не привязан</option>
|
||||
<template x-for="vgroup in $store.dataset.vgroups">
|
||||
<option x-bind:value="vgroup.id" x-text="vgroup.name" x-bind:selected="voter.vgroup_id === vgroup.id"></option>
|
||||
</template>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
@click="deleteVoter(voter.id)">
|
||||
Удалить
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'votes'" x-data="{ colorCoded: false }">
|
||||
<div class="form-check form-switch form-check-reverse">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckReverse" @change="colorCoded = $event.target.checked">
|
||||
<label class="form-check-label" for="flexSwitchCheckReverse">Колор-кодинг</label>
|
||||
</div>
|
||||
<table x-data="{ groupFilter: -1, dateFilter: '' }" id="votesTable" class="w-100 table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle text-center">
|
||||
Когда
|
||||
<select class="form-select form-select-sm"
|
||||
@change="dateFilter = $event.target.value">
|
||||
<option value="" selected>Без фильтра</option>
|
||||
<template x-for="voteDate in getDatesFromDatasets()">
|
||||
<option x-bind:value="voteDate" x-text="voteDate"></option>
|
||||
</template>
|
||||
</select>
|
||||
</th>
|
||||
<th scope="col" class="align-middle text-center">Кто</th>
|
||||
<th scope="col" class="align-middle text-start">
|
||||
Группа
|
||||
<select class="form-select form-select-sm" @change="groupFilter = $event.target.value">
|
||||
<option value="-1" selected>Без фильтра</option>
|
||||
<template x-for="vgroup in $store.dataset.vgroups">
|
||||
<option x-bind:value="vgroup.id" x-text="vgroup.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Здоровье</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Любовь</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Секс</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Работа</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Отдых</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Деньги</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Отношения</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Личн. рост</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Смысл жизни</th>
|
||||
<th scope="col" class="align-middle text-center wm-vlr">Тревожность</th>
|
||||
<th scope="col" class="align-middle text-center">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<template x-if="!$store.dataset.votes">
|
||||
<tr><td colspan="14" scope="row"><i>Loading...</i></td></tr>
|
||||
</template>
|
||||
<template x-for="vote in $store.dataset.votes">
|
||||
<tr
|
||||
x-data="{ voter: $store.dataset.voters.find((el) => el.id == vote.voter_id), vfd: formatDateMMYYYY(new Date(vote.vote_date)) }"
|
||||
x-show="(groupFilter == -1 || groupFilter == voter.vgroup_id) && (vfd.includes(dateFilter))">
|
||||
<td x-text="vfd" scope="row"></td>
|
||||
<td x-text="voter?.full_name || '[Нет имени]'"></td>
|
||||
<td x-text="getVGroupNameById(voter.vgroup_id)"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.health"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.health >= 7,
|
||||
'status-warning': colorCoded && vote.health < 7 && vote.health > 4,
|
||||
'status-dangerous': colorCoded && vote.health <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.love"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.love >= 7,
|
||||
'status-warning': colorCoded && vote.love < 7 && vote.love > 4,
|
||||
'status-dangerous': colorCoded && vote.love <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.sex"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.sex >= 7,
|
||||
'status-warning': colorCoded && vote.sex < 7 && vote.sex > 4,
|
||||
'status-dangerous': colorCoded && vote.sex <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.work"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.work >= 7,
|
||||
'status-warning': colorCoded && vote.work < 7 && vote.work > 4,
|
||||
'status-dangerous': colorCoded && vote.work <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.rest"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.rest >= 7,
|
||||
'status-warning': colorCoded && vote.rest < 7 && vote.rest > 4,
|
||||
'status-dangerous': colorCoded && vote.rest <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.finances"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.finances >= 7,
|
||||
'status-warning': colorCoded && vote.finances < 7 && vote.finances > 4,
|
||||
'status-dangerous': colorCoded && vote.finances <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.relations"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.relations >= 7,
|
||||
'status-warning': colorCoded && vote.relations < 7 && vote.relations > 4,
|
||||
'status-dangerous': colorCoded && vote.relations <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.pers_growth"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.pers_growth >= 7,
|
||||
'status-warning': colorCoded && vote.pers_growth < 7 && vote.pers_growth > 4,
|
||||
'status-dangerous': colorCoded && vote.pers_growth <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.meaning_of_life"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.meaning_of_life >= 7,
|
||||
'status-warning': colorCoded && vote.meaning_of_life < 7 && vote.meaning_of_life > 4,
|
||||
'status-dangerous': colorCoded && vote.meaning_of_life <= 4
|
||||
}" scope="row"></td>
|
||||
<td class="fading-bg stats-cell" x-text="vote.serenity"
|
||||
:class="{
|
||||
'status-ok': colorCoded && vote.serenity <= 4,
|
||||
'status-warning': colorCoded && vote.serenity > 4 && vote.serenity < 7,
|
||||
'status-dangerous': colorCoded && vote.serenity >= 7
|
||||
}" scope="row"></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
@click="deleteVote(vote.id)">
|
||||
Удалить
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'analysis'" x-data="{
|
||||
show_acloub: $persist(true),
|
||||
show_agc: $persist(true),
|
||||
shown_agac: $persist(true),
|
||||
shown_gc: $persist(true),
|
||||
shown_cat: $persist(true),
|
||||
}">
|
||||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||||
<h4>Среднее за клуб</h4>
|
||||
<button class="badge text-bg-secondary"
|
||||
@click="show_acloub = !show_acloub"
|
||||
x-text="show_acloub? 'Скрыть' : 'Показать'"></button>
|
||||
</div>
|
||||
<div x-show="show_acloub" x-transition>
|
||||
<div class="chart-container">
|
||||
<canvas chart-id="clubAverageCategories" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||||
<h4>Среднее по категориям в группах</h4>
|
||||
<button class="badge text-bg-secondary"
|
||||
@click="show_agc = !show_agc"
|
||||
x-text="show_agc? 'Скрыть' : 'Показать'"></button>
|
||||
</div>
|
||||
<div x-show="show_agc" x-transition>
|
||||
<div class="m-0 mb-1 mb-2 row">
|
||||
<select class="form-select form-select-sm col me-1"
|
||||
@change="$store.dataset.selectedGacavVgroup = parseInt($event.target.value); updateGACChart()">
|
||||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedGacavVgroup === vgroup_id"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas chart-id="groupAverageCategories" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||||
<h4>Среднее за по категориям среди всех групп</h4>
|
||||
<button class="badge text-bg-secondary"
|
||||
@click="shown_agac = !shown_agac"
|
||||
x-text="shown_agac? 'Скрыть' : 'Показать'"></button>
|
||||
</div>
|
||||
<div x-show="shown_agac" x-transition>
|
||||
<div class="m-0 mb-1 mb-2 row">
|
||||
<select class="form-select form-select-sm col ms-1"
|
||||
@change="$store.dataset.agacVoteDate = $event.target.value; updateAGACChart()">
|
||||
<template x-for="voteDate in getDatesFromDatasets()">
|
||||
<option x-bind:value="voteDate" x-text="voteDate" x-bind:selected="$store.dataset.agacVoteDate === voteDate"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas chart-id="allGroupAverageCategories" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||||
<h4>Значения в группе по категориям за дату</h4>
|
||||
<button class="badge text-bg-secondary"
|
||||
@click="shown_gc = !shown_gc"
|
||||
x-text="shown_gc? 'Скрыть' : 'Показать'"></button>
|
||||
</div>
|
||||
<div x-show="shown_gc" x-transition>
|
||||
<div class="m-0 mb-1 mb-2 row">
|
||||
<select class="form-select form-select-sm col me-1"
|
||||
@change="$store.dataset.selectedRadarVgroup = parseInt($event.target.value); updateRadarChart()">
|
||||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedRadarVgroup === vgroup_id"></option>
|
||||
</template>
|
||||
</select>
|
||||
<select class="form-select form-select-sm col ms-1"
|
||||
@change="$store.dataset.voteDate = $event.target.value; updateRadarChart()">
|
||||
<template x-for="voteDate in getDatesFromDatasets()">
|
||||
<option x-bind:value="voteDate" x-text="voteDate" x-bind:selected="$store.dataset.voteDate === voteDate"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div id="lowDatasetCountNotification" class="alert alert-info" role="alert"
|
||||
x-show="$store.dataset.grouppedVotes[$store.dataset.selectedRadarVgroup] && !$store.dataset.grouppedVotes[$store.dataset.selectedRadarVgroup].find(e => e.vote_date == $store.dataset.voteDate)"
|
||||
x-intersect:enter="$store.dynamicsChart = 'bar'">
|
||||
У группы нет данных за эту дату
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas chart-id="groupCategories" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||||
<h4>Динамика категорий группы во времени (среднее)</h4>
|
||||
<button class="badge text-bg-secondary"
|
||||
@click="shown_cat = !shown_cat"
|
||||
x-text="shown_cat? 'Скрыть' : 'Показать'"></button>
|
||||
</div>
|
||||
<div x-show="shown_cat" x-transition>
|
||||
<div class="m-0 mb-1 mb-2 row">
|
||||
<select class="form-select form-select-sm col"
|
||||
@change="$store.dataset.selectedVgroup = parseInt($event.target.value); updateLineChart()">
|
||||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedVgroup === vgroup_id"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div id="lowDatasetCountNotification" class="alert alert-info" role="alert"
|
||||
x-show="$store.dataset.grouppedVotes[$store.dataset.selectedVgroup] && $store.dataset.grouppedVotes[$store.dataset.selectedVgroup].length == 1"
|
||||
x-intersect:enter="$store.dynamicsChart = 'bar'">
|
||||
Мало данных по группе, лучше использовать столбчатую диаграмму
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas chart-id="categoryBars" width="400" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-5 pt-5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const randomNum = () => Math.floor(Math.random() * (235 - 52 + 1) + 52);
|
||||
const randomRGB = () => `rgba(${randomNum()}, ${randomNum()}, ${randomNum()}, 0.5)`;
|
||||
const randomRGBO = (opacity) => `rgba(${randomNum()}, ${randomNum()}, ${randomNum()}, ${opacity})`;
|
||||
const randomRGBV = () => [randomNum(), randomNum(), randomNum()];
|
||||
const average = (arr) => (arr.reduce((acc, c) => acc + c, 0) / arr.length);
|
||||
const fetchDatasetCategory = (dataset, group, category) => dataset[group].map(e => average(e[category]));
|
||||
const getVGroupNameById = (vgroup_id) => Alpine.store("dataset").vgroups.find((el) => el.id == vgroup_id)?.name || '[Нет группы]';
|
||||
const getDatesFromDatasets = () => [...new Set(Object.values(Alpine.store("dataset").grouppedVotes).map(e => e.map(i => i.vote_date)).reduce((acc, v) => [...acc, ...v], []))].sort();
|
||||
const formatDateMMYYYY = (date) => date.toLocaleString('ru', { month: "numeric", year: "numeric" });
|
||||
const getColorBasedOnValue = (value) => {
|
||||
if (value <= 4) return 'rgba(255, 0, 0, 0.5)';
|
||||
if (value > 4 && value < 7) return 'rgba(255, 255, 0, 0.5)';
|
||||
return 'rgba(0, 255, 0, 0.5)';
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="/javascripts/chart.min.js"></script>
|
||||
<script src="/javascripts/chartjs-plugin-datalabels-v2.1.0.js"></script>
|
||||
|
||||
<script>
|
||||
const dbToCategory = {
|
||||
health: "Здоровье",
|
||||
love: "Любовь",
|
||||
sex: "Секс",
|
||||
rest: "Отдых",
|
||||
finances: "Деньги",
|
||||
meaning_of_life: "Смысл жизни",
|
||||
serenity: "Тревожность",
|
||||
relations: "Отношения",
|
||||
pers_growth: "Личн. рост",
|
||||
work: "Работа",
|
||||
};
|
||||
|
||||
class ChartsController {
|
||||
constructor() {
|
||||
this.charts = {};
|
||||
}
|
||||
|
||||
createChart(id, type, data, options) {
|
||||
const ctx = document.querySelector(`[chart-id=${id}]`).getContext('2d');
|
||||
this.charts[id] = {
|
||||
ctx,
|
||||
chart: new Chart(ctx, {
|
||||
type: type,
|
||||
data: data,
|
||||
options: options,
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
updateChartData(id, labels, data) {
|
||||
this.charts[id].chart.data.labels.length = 0;
|
||||
this.charts[id].chart.data.datasets.length = 0;
|
||||
|
||||
this.charts[id].chart.data.labels.push(...labels);
|
||||
this.charts[id].chart.data.datasets.push(...data);
|
||||
|
||||
this.charts[id].chart.update();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
Chart.register(ChartDataLabels);
|
||||
Chart.defaults.set("plugins.datalabels", {display: false});
|
||||
const ct = new ChartsController();
|
||||
|
||||
let rootElement;
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
rootElement = document.getElementById("root");
|
||||
});
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store("dataset", {
|
||||
vgroups: [],
|
||||
voters: [],
|
||||
votes: [],
|
||||
grouppedVotes: {},
|
||||
});
|
||||
|
||||
ct.createChart("clubAverageCategories", "radar", {}, {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {},
|
||||
scales: {
|
||||
v: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
axis: "r",
|
||||
},
|
||||
}
|
||||
});
|
||||
ct.createChart("groupAverageCategories", "radar", {}, {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {},
|
||||
scales: {
|
||||
v: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
axis: "r",
|
||||
pointLabels: {
|
||||
display: true,
|
||||
centerPointLabels: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
ct.createChart("allGroupAverageCategories", "polarArea", {}, {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
v: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
axis: "r",
|
||||
pointLabels: {
|
||||
display: true,
|
||||
centerPointLabels: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
ct.createChart("groupCategories", "polarArea", {}, {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
v: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
axis: "r",
|
||||
pointLabels: {
|
||||
display: true,
|
||||
centerPointLabels: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
ct.createChart("categoryBars", "bar", {}, {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
datalabels: {
|
||||
display: true,
|
||||
color: 'black',
|
||||
font: {
|
||||
size: 14
|
||||
},
|
||||
rotation: -90,
|
||||
align: 'end',
|
||||
anchor: 'end',
|
||||
clip: true,
|
||||
display: 'auto',
|
||||
formatter: function(value, context) {
|
||||
return context.dataset.label;
|
||||
},
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
max: 12,
|
||||
},
|
||||
x: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const fetchData = async function() {
|
||||
Alpine.store("dataset").vgroups = [];
|
||||
Alpine.store("dataset").voters = [];
|
||||
Alpine.store("dataset").votes = [];
|
||||
Alpine.store("dataset").grouppedVotes = {};
|
||||
|
||||
try {
|
||||
let resp;
|
||||
|
||||
resp = await fetch("/admin/vgroups");
|
||||
Alpine.store("dataset").vgroups = await resp.json();
|
||||
|
||||
resp = await fetch("/admin/voters");
|
||||
Alpine.store("dataset").voters = await resp.json();
|
||||
|
||||
resp = await fetch("/admin/votes");
|
||||
Alpine.store("dataset").votes = await resp.json();
|
||||
|
||||
resp = await fetch("/admin/votes/groupped");
|
||||
const data = await resp.json();
|
||||
data.forEach((e) => {
|
||||
Alpine.store("dataset").grouppedVotes[e.vgroup_id] = [...(Alpine.store("dataset").grouppedVotes[e.vgroup_id] || []), e];
|
||||
});
|
||||
|
||||
try {
|
||||
const vg = Object.keys(Alpine.store("dataset").grouppedVotes)[0];
|
||||
Alpine.store("dataset").selectedVgroup = vg;
|
||||
Alpine.store("dataset").selectedRadarVgroup = vg;
|
||||
Alpine.store("dataset").selectedGacavVgroup = vg;
|
||||
|
||||
const ad = getDatesFromDatasets()[0];
|
||||
Alpine.store("dataset").voteDate = ad;
|
||||
Alpine.store("dataset").agacVoteDate = ad;
|
||||
|
||||
updateCACChart();
|
||||
updateGACChart();
|
||||
updateAGACChart();
|
||||
updateAGACChart();
|
||||
updateLineChart();
|
||||
updateRadarChart();
|
||||
} catch (e) {
|
||||
alert("Произошла ошибка выделения данных для графиков, обратитесь к разработчику");
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Ошибка загрузки данных");
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
Alpine.$data(rootElement).loading = false;
|
||||
}
|
||||
|
||||
const deleteVgroup = async function (vgroupId) {
|
||||
try {
|
||||
const resp = await fetch(`/admin/vgroups/${vgroupId}/delete`);
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
alert(json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const updateUserGroup = async function (userId, newGroup) {
|
||||
try {
|
||||
const resp = await fetch(`/admin/voters/${userId}/promote/${newGroup}`);
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
alert(json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const deleteVoter = async function (voterId) {
|
||||
try {
|
||||
const resp = await fetch(`/admin/voters/${voterId}/delete`);
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
alert(json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const deleteVote = async function (voteId) {
|
||||
try {
|
||||
const resp = await fetch(`/admin/votes/${voteId}/delete`);
|
||||
const json = await resp.json();
|
||||
if (json.error) {
|
||||
alert(json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const updateCACChart = function () {
|
||||
const baseDataset = Alpine.store("dataset").grouppedVotes;
|
||||
|
||||
const combinedDataset = {};
|
||||
Object.values(Alpine.store("dataset").grouppedVotes).forEach(e => {
|
||||
const rds = e.reduce((acc, v) => {
|
||||
acc[v.vote_date] ??= {};
|
||||
Object.keys(dbToCategory).forEach(key => {
|
||||
acc[v.vote_date][key] ??= [];
|
||||
acc[v.vote_date][key].push(...v[key])
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
Object.keys(rds).forEach(key => {
|
||||
combinedDataset[key] ??= {};
|
||||
Object.keys(rds[key]).forEach(kv => {
|
||||
combinedDataset[key][kv] ??= [];
|
||||
combinedDataset[key][kv].push(...rds[key][kv]);
|
||||
});
|
||||
});
|
||||
}, {});
|
||||
console.log(combinedDataset);
|
||||
|
||||
const labels = Object.keys(dbToCategory).map(key => dbToCategory[key]);
|
||||
const datasets = Object.keys(combinedDataset).map(voteDate => {
|
||||
const avgs = Object.keys(dbToCategory).map(key => average(combinedDataset[voteDate][key]));
|
||||
const d = {
|
||||
label: voteDate,
|
||||
backgroundColor: randomRGBO(0.2), // FIIXME: randomize
|
||||
pointBackgroundColor: avgs.map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: avgs,
|
||||
};
|
||||
|
||||
return d;
|
||||
});
|
||||
|
||||
ct.updateChartData("clubAverageCategories", labels, datasets);
|
||||
}
|
||||
|
||||
const updateGACChart = function () {
|
||||
const [baseDataset, group] = [
|
||||
Alpine.store("dataset").grouppedVotes,
|
||||
Alpine.store("dataset").selectedGacavVgroup,
|
||||
];
|
||||
|
||||
const labels = Object.keys(dbToCategory).map(key => dbToCategory[key]);
|
||||
const datasets = baseDataset[group].map(e => {
|
||||
const avgs = Object.keys(dbToCategory).map(key => e[key]).map(average);
|
||||
const d = {
|
||||
label: e.vote_date,
|
||||
backgroundColor: randomRGBO(0.2), // FIIXME: randomize
|
||||
pointBackgroundColor: avgs.map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: avgs,
|
||||
};
|
||||
|
||||
return d;
|
||||
});
|
||||
|
||||
ct.updateChartData("groupAverageCategories", labels, datasets);
|
||||
}
|
||||
|
||||
const update__Chart = function () {
|
||||
const [baseDataset, voteDate] = [
|
||||
Alpine.store("dataset").grouppedVotes,
|
||||
Alpine.store("dataset").agacVoteDate,
|
||||
];
|
||||
|
||||
const labels = [];
|
||||
const datasets = [];
|
||||
Object.keys(gvotes).forEach(cat => {
|
||||
labels.push(dbToCategory[cat]);
|
||||
// colors.push(getColorBasedOnValue(cat == "serenity" ? 10-av : av));
|
||||
});
|
||||
|
||||
ct.updateChartData("allGroupAverageCategories", labels, datasets);
|
||||
}
|
||||
|
||||
const updateAGACChart = function() {
|
||||
const [baseDataset, voteDate] = [
|
||||
Alpine.store("dataset").grouppedVotes,
|
||||
Alpine.store("dataset").agacVoteDate,
|
||||
];
|
||||
|
||||
const datasets = [];
|
||||
|
||||
const gvotes = {};
|
||||
Object.values(baseDataset).forEach(vg => {
|
||||
vg.forEach(ds => {
|
||||
if (ds.vote_date === voteDate) {
|
||||
Object.keys(dbToCategory).forEach(cat => {
|
||||
const av = average(ds[cat]);
|
||||
gvotes[cat] ??= [];
|
||||
gvotes[cat].push(av);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const labels = [];
|
||||
const colors = [];
|
||||
const vals = [];
|
||||
Object.keys(gvotes).forEach(cat => {
|
||||
const av = average(gvotes[cat]);
|
||||
labels.push(dbToCategory[cat]);
|
||||
colors.push(getColorBasedOnValue(cat == "serenity" ? 10-av : av));
|
||||
vals.push(av);
|
||||
});
|
||||
datasets.push({
|
||||
label: labels,
|
||||
backgroundColor: colors,
|
||||
borderWidth: 2,
|
||||
data: vals,
|
||||
});
|
||||
|
||||
ct.updateChartData("allGroupAverageCategories", labels, datasets);
|
||||
}
|
||||
|
||||
const updateRadarChart = function() {
|
||||
const [baseDataset, group, voteDate] = [
|
||||
Alpine.store("dataset").grouppedVotes,
|
||||
Alpine.store("dataset").selectedRadarVgroup,
|
||||
Alpine.store("dataset").voteDate,
|
||||
];
|
||||
|
||||
const groupDataset = baseDataset[group].find(e => e.vote_date === voteDate);
|
||||
const labels = [];
|
||||
const datasets = [];
|
||||
if (groupDataset) {
|
||||
const vals = [];
|
||||
Object.keys(dbToCategory).forEach(k => {
|
||||
labels.push(dbToCategory[k]);
|
||||
vals.push(average(groupDataset[k]));
|
||||
});
|
||||
const dataset = {
|
||||
label: getVGroupNameById(group),
|
||||
backgroundColor: vals.map((v, i) => getColorBasedOnValue(labels[i] == "Тревожность" ? 10-v : v)),
|
||||
borderWidth: 2,
|
||||
data: vals
|
||||
};
|
||||
datasets.push(dataset);
|
||||
}
|
||||
|
||||
ct.updateChartData("groupCategories", labels, datasets);
|
||||
}
|
||||
|
||||
const updateLineChart = function() {
|
||||
const [baseDataset, group] = [Alpine.store("dataset").grouppedVotes, Alpine.store("dataset").selectedVgroup];
|
||||
const labels = baseDataset[group].map(e => e.vote_date);
|
||||
|
||||
const datasets = [
|
||||
{
|
||||
label: "Здоровье",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "health").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "health")
|
||||
},
|
||||
{
|
||||
label: "Любовь",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "love").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "love")
|
||||
},
|
||||
{
|
||||
label: "Секс",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "sex").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "sex")
|
||||
},
|
||||
{
|
||||
label: "Отдых",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "rest").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "rest")
|
||||
},
|
||||
{
|
||||
label: "Деньги",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "finances").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "finances")
|
||||
},
|
||||
{
|
||||
label: "Смысл жизни",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "meaning_of_life").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "meaning_of_life")
|
||||
},
|
||||
{
|
||||
label: "Тревожность",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "serenity").map(e => getColorBasedOnValue(10-e)),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "serenity")
|
||||
},
|
||||
{
|
||||
label: "Отношения",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "relations").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "relations")
|
||||
},
|
||||
{
|
||||
label: "Личн. рост",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "pers_growth").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "pers_growth")
|
||||
},
|
||||
{
|
||||
label: "Работа",
|
||||
backgroundColor: fetchDatasetCategory(baseDataset, group, "work").map(getColorBasedOnValue),
|
||||
borderWidth: 2,
|
||||
data: fetchDatasetCategory(baseDataset, group, "work")
|
||||
}
|
||||
];
|
||||
|
||||
ct.updateChartData("categoryBars", labels, datasets);
|
||||
}
|
||||
</script>
|
||||
16
views/error.hbs
Normal file
16
views/error.hbs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="exclamation-triangle-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<main class="align-items-center container d-flex flex-row h-screen justify-content-center">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">{{message}}</h4>
|
||||
<p>{{error.status}}</p>
|
||||
<a href="/" class="alert-link">Вернуться на главную</a>
|
||||
<!--
|
||||
{{error.stack}}
|
||||
-->
|
||||
</div>
|
||||
</main>
|
||||
|
After Width: | Height: | Size: 783 B |
35
views/gateway/index.hbs
Normal file
35
views/gateway/index.hbs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<main class="align-items-center container d-flex flex-row h-screen justify-content-center">
|
||||
<div class="flex-grow-1 h-fit-content">
|
||||
<h2>Вход</h2>
|
||||
<form action="/gateway/login" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="login" class="form-label">Логин</label>
|
||||
<input type="text" id="login" name="login" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Пароль</label>
|
||||
<input type="password" id="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex-grow-0 p-4 h-fit-content">или</div>
|
||||
<div class="flex-grow-1 h-fit-content">
|
||||
<h2>Регистрация</h2>
|
||||
<form action="/gateway/register" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="full-name-reg" class="form-label">Как вас зовут</label>
|
||||
<input type="text" id="full-name-reg" name="full_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="login-reg" class="form-label">Логин</label>
|
||||
<input type="text" id="login-reg" name="login" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password-reg" class="form-label">Пароль</label>
|
||||
<input type="password" id="password-reg" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Зарегистрироваться</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
26
views/layout.hbs
Normal file
26
views/layout.hbs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{title}}</title>
|
||||
<script src="//unpkg.com/@alpinejs/persist" defer></script>
|
||||
<script src="//unpkg.com/@alpinejs/intersect" defer></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{#if flashes}}
|
||||
<div class="container position-absolute start-50 top-0 translate-middle-x pt-2" style="z-index: 5;">
|
||||
{{#each flashes}}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
{{this}}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{{body}}}
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</html>
|
||||
410
views/userspace/index.hbs
Normal file
410
views/userspace/index.hbs
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
<div class="h-screen d-flex flex-column">
|
||||
<header class="navbar navbar-expand-md navbar-dark bd-navbar">
|
||||
<ul class="nav nav-pills container flex-wrap flex-md-nowrap gap-2">
|
||||
<h4>Привет, {{user.full_name}}!</h4>
|
||||
<div class="flex-grow-1"></div>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/userspace/votes">История</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/gateway/logout">Выйти</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div x-data="{loading: false}" class="align-items-center container d-flex flex-grow-1 flex-row justify-content-center">
|
||||
{{#if user.vgroup_id includeZero=true}}
|
||||
<canvas id="wheel" style="height: 600"></canvas>
|
||||
|
||||
<form class="col m-4 h-fit-content">
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Здоровье</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a0" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 0)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b0" min="0" max="10" step="1" value="0" oninput="param2Change(this, 0)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Любовь</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a1" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 1)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b1" min="0" max="10" step="1" value="0" oninput="param2Change(this, 1)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Секс</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a2" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 2)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b2" min="0" max="10" step="1" value="0" oninput="param2Change(this, 2)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Работа</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a3" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 3)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b3" min="0" max="10" step="1" value="0" oninput="param2Change(this, 3)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Отдых</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a4" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 4)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b4" min="0" max="10" step="1" value="0" oninput="param2Change(this, 4)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Деньги</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a5" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 5)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b5" min="0" max="10" step="1" value="0" oninput="param2Change(this, 5)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Отношения</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a6" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 6)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b6" min="0" max="10" step="1" value="0" oninput="param2Change(this, 6)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Личн. рост</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a7" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 7)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b7" min="0" max="10" step="1" value="0" oninput="param2Change(this, 7)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Смысл жизни</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a8" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 8)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b8" min="0" max="10" step="1" value="0" oninput="param2Change(this, 8)">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<label class="w-15">Тревожность</label>
|
||||
<input x-bind:disabled="loading" type="range" id="a9" class="flex-grow-1" min="0" max="10" step="1" value="0" oninput="paramChange(this, 9)">
|
||||
<input x-bind:disabled="loading" type="number" class="dubler" id="b9" min="0" max="10" step="1" value="0" oninput="param2Change(this, 9)">
|
||||
</div>
|
||||
|
||||
<button x-bind:disabled="loading" type="button" id="sendBtn" class="btn btn-outline-primary w-100 pt-2">Отправить</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="text-center">
|
||||
<h1>Вы не состоите в группе</h1>
|
||||
<p>Повторите попытку, когда администратор привяжет вас к группе</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if user.vgroup_id includeZero=true}}
|
||||
<script>
|
||||
let myCanvas, ctx;
|
||||
let param = [{
|
||||
name: "ЗДОРОВЬЕ",
|
||||
dbmap: "health",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "ЛЮБОВЬ",
|
||||
dbmap: "love",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "СЕКС",
|
||||
dbmap: "sex",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "РАБОТА",
|
||||
dbmap: "work",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "ОТДЫХ",
|
||||
dbmap: "rest",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "ДЕНЬГИ",
|
||||
dbmap: "finances",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "ОТНОШЕНИЯ",
|
||||
dbmap: "relations",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "ЛИЧН. РОСТ",
|
||||
dbmap: "pers_growth",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "СМЫСЛ ЖИЗНИ",
|
||||
dbmap: "meaning_of_life",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "СПОКОЙСТВИЕ",
|
||||
dbmap: "serenity",
|
||||
value: 0
|
||||
},
|
||||
];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
myCanvas = document.getElementById("wheel");
|
||||
myCanvas.width = 480;
|
||||
myCanvas.height = 600;
|
||||
|
||||
ctx = myCanvas.getContext("2d");
|
||||
drawWheel();
|
||||
|
||||
const sendBtn = document.getElementById("sendBtn");
|
||||
sendBtn.addEventListener("click", async () => {
|
||||
Alpine.$data(sendBtn).loading = true;
|
||||
try {
|
||||
let res = await fetch(`${location.href}/vote`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(param),
|
||||
});
|
||||
let json = await res.json();
|
||||
console.log(json);
|
||||
if (json.status == "ok") {
|
||||
alert("Спасибо за голосование!");
|
||||
} else {
|
||||
alert(("Ошибка голосования!\n" + (json.message || "")).trim());
|
||||
console.log(json);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
alert(e.message);
|
||||
return;
|
||||
}
|
||||
resetWheel();
|
||||
Alpine.$data(sendBtn).loading = false;
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
const resetWheel = () => {
|
||||
document.querySelectorAll(".dubler").forEach((el) => {
|
||||
el.value = 0;
|
||||
});
|
||||
document.querySelector(".dubler").dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
// отрисовка линии
|
||||
function drawLine(ctx, startX, startY, endX, endY) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, startY);
|
||||
ctx.lineTo(endX, endY);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// отрисовка дуги
|
||||
function drawArc(ctx, centerX, centerY, radius, startAngle, endAngle) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// отрисовка сектора
|
||||
function drawPieSlice(ctx, centerX, centerY, radius, startAngle, endAngle, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// проставка временной отметки
|
||||
function drawTimestamp(ctx) {
|
||||
let now = new Date();
|
||||
let day, month, hour, minute, second;
|
||||
|
||||
if (now.getDate() < 10) {
|
||||
day = "0" + now.getDate();
|
||||
} else {
|
||||
day = now.getDate();
|
||||
}
|
||||
if (now.getMonth() < 10) {
|
||||
month = "0" + (now.getMonth() + 1);
|
||||
} else {
|
||||
month = (now.getMonth() + 1);
|
||||
}
|
||||
if (now.getHours() < 10) {
|
||||
hour = "0" + now.getHours();
|
||||
} else {
|
||||
hour = now.getHours();
|
||||
}
|
||||
if (now.getMinutes() < 10) {
|
||||
minute = "0" + now.getMinutes();
|
||||
} else {
|
||||
minute = now.getMinutes();
|
||||
}
|
||||
if (now.getSeconds() < 10) {
|
||||
second = "0" + now.getSeconds();
|
||||
} else {
|
||||
second = now.getSeconds();
|
||||
}
|
||||
|
||||
let timestamp = day + "-" + month + "-" + now.getFullYear() + " " + hour + ":" + minute + ":" + second;
|
||||
|
||||
|
||||
ctx.fillStyle = "darkgray";
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.font = "10pt Roboto";
|
||||
ctx.fillText(timestamp, myCanvas.width - 127, 15);
|
||||
}
|
||||
|
||||
// отрисовка текста
|
||||
function drawText(ctx) {
|
||||
ctx.fillStyle = "#222";
|
||||
|
||||
name = " " + param[6].name + " " + param[7].name + " " + param[0].name + " " + param[1].name + " ";
|
||||
drawTextAlongArc(ctx, name, 240, 240, 208, Math.PI);
|
||||
|
||||
name = " " + param[5].name + " " + param[4].name + " " + param[3].name + " " + param[2].name + " ";
|
||||
drawTextAlongArc(ctx, name, 240, 240, -220, Math.PI * -1);
|
||||
|
||||
|
||||
ctx.fillText(param[0].value, 283, 126);
|
||||
ctx.fillText(param[1].value, 354, 196);
|
||||
ctx.fillText(param[2].value, 354, 296);
|
||||
ctx.fillText(param[3].value, 283, 366);
|
||||
ctx.fillText(param[4].value, 190, 366);
|
||||
ctx.fillText(param[5].value, 115, 296);
|
||||
ctx.fillText(param[6].value, 115, 196);
|
||||
ctx.fillText(param[7].value, 190, 126);
|
||||
ctx.fillText(param[8].name + " - " + param[8].value, 45, 490);
|
||||
ctx.fillText(param[9].name + " - " + (10 - param[9].value), 45, 550);
|
||||
ctx.fillText("ТРЕВОЖНОСТЬ - " + param[9].value, 315, 550);
|
||||
}
|
||||
|
||||
// отрисовка всего колеса
|
||||
function drawWheel() {
|
||||
ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);
|
||||
ctx.strokeStyle = "#999999";
|
||||
ctx.fillStyle = "white";
|
||||
ctx.rect(0, 0, myCanvas.width, myCanvas.height);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "black";
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.font = "11pt Roboto";
|
||||
|
||||
// отрисовка опорных кругов колеса
|
||||
for (let i = 1; i <= 10; i++)
|
||||
drawArc(ctx, 240, 240, i * 20, 0, 360);
|
||||
|
||||
|
||||
// отрисовка опорных линий колеса
|
||||
drawLine(ctx, 240, 40, 240, 440);
|
||||
drawLine(ctx, 382, 98, 98, 382);
|
||||
drawLine(ctx, 98, 98, 382, 382);
|
||||
drawLine(ctx, 40, 240, 440, 240);
|
||||
|
||||
|
||||
// отрисовка секторов колеса
|
||||
for (let i = -0.5; i < 1.5; i = i + 0.25) {
|
||||
|
||||
value = param[(i + 0.5) / 0.25].value;
|
||||
name = param[(i + 0.5) / 0.25].name;
|
||||
|
||||
if (value <= 4)
|
||||
color = "#ea4335"; // красный
|
||||
else if (value >= 8)
|
||||
color = "#34a853"; // зеленый
|
||||
else
|
||||
color = "#fbbc05"; // желтый
|
||||
|
||||
drawPieSlice(ctx, 240, 240, 20 * value, Math.PI * i, Math.PI * (i + 0.25), color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
ctx.strokeStyle = "#999999";
|
||||
|
||||
for (let i = 0; i < 10; i = i + 1) {
|
||||
ctx.rect(45 + i * 40, 500, 30, 20);
|
||||
ctx.rect(45 + i * 40, 560, 30, 20);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (let i = 0; i < 10; i = i + 1) {
|
||||
|
||||
if (param[8].value < i + 1)
|
||||
color = "white";
|
||||
else
|
||||
if (param[8].value <= 4)
|
||||
color = "#ea4335" // красный
|
||||
else if (param[8].value >= 8)
|
||||
color = "#34a853"; // зеленый
|
||||
else
|
||||
color = "#fbbc05"; // желтый
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(45 + i * 40, 500, 30, 20);
|
||||
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
|
||||
if ((10 - param[9].value) < i + 1)
|
||||
color = "white";
|
||||
else
|
||||
if ((10 - param[9].value) <= 4)
|
||||
color = "#ea4335" // красный
|
||||
else if ((10 - param[9].value) >= 8)
|
||||
color = "#34a853"; // зеленый
|
||||
else
|
||||
color = "#fbbc05"; // желтый
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(45 + i * 40, 560, 30, 20);
|
||||
}
|
||||
|
||||
|
||||
drawTimestamp(ctx);
|
||||
drawText(ctx);
|
||||
}
|
||||
|
||||
// создание подписей вокруг внешнего круга
|
||||
function drawTextAlongArc(context, str, centerX, centerY, radius, angle) {
|
||||
|
||||
let len = str.length,
|
||||
s;
|
||||
context.save();
|
||||
context.translate(centerX, centerY);
|
||||
context.rotate(-1 * angle / 2);
|
||||
context.rotate(-1 * (angle / len) / 2);
|
||||
|
||||
for (let n = 0; n < len; n++) {
|
||||
context.rotate(angle / len);
|
||||
context.save();
|
||||
context.translate(0, -1 * radius);
|
||||
s = str[n];
|
||||
context.fillText(s, 0, 0);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
context.restore();
|
||||
|
||||
}
|
||||
|
||||
// действия при сдвиге ползунка
|
||||
function paramChange(block, id) {
|
||||
param[id].value = parseInt(block.value) || 0;
|
||||
document.getElementById("b" + id).value = block.value;
|
||||
drawWheel();
|
||||
}
|
||||
|
||||
|
||||
// действия при изменении значения дублирующего input
|
||||
function param2Change(block, id) {
|
||||
param[id].value = block.value;
|
||||
document.getElementById("a" + id).value = block.value;
|
||||
drawWheel();
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
59
views/userspace/votes.hbs
Normal file
59
views/userspace/votes.hbs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<div>
|
||||
<div>
|
||||
<header class="navbar navbar-expand-md navbar-dark bd-navbar">
|
||||
<ul class="nav nav-pills container flex-wrap flex-md-nowrap gap-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/userspace">Вернуться</a>
|
||||
</li>
|
||||
<div class="flex-grow-1"></div>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/gateway/logout">Выйти</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div class="container p-0">
|
||||
<div>
|
||||
<table id="votesTable" class="w-100 table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Когда</th>
|
||||
<th scope="col">Здоровье</th>
|
||||
<th scope="col">Любовь</th>
|
||||
<th scope="col">Секс</th>
|
||||
<th scope="col">Работа</th>
|
||||
<th scope="col">Отдых</th>
|
||||
<th scope="col">Деньги</th>
|
||||
<th scope="col">Отношения</th>
|
||||
<th scope="col">Личн. рост</th>
|
||||
<th scope="col">Смысл жизни</th>
|
||||
<th scope="col">Тревожность</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{{#each votes}}
|
||||
<tr>
|
||||
<td scope="row">{{date}}</td>
|
||||
<td scope="row">{{health}}</td>
|
||||
<td scope="row">{{love}}</td>
|
||||
<td scope="row">{{sex}}</td>
|
||||
<td scope="row">{{work}}</td>
|
||||
<td scope="row">{{rest}}</td>
|
||||
<td scope="row">{{finances}}</td>
|
||||
<td scope="row">{{relations}}</td>
|
||||
<td scope="row">{{pers_growth}}</td>
|
||||
<td scope="row">{{meaning_of_life}}</td>
|
||||
<td scope="row">{{serenity}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="11" class="text-center">Нет данных</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue