Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8908d99245 | ||
![]() |
9253069a07 | ||
![]() |
a75e958944 | ||
![]() |
41bcc33175 | ||
![]() |
17d46952c2 | ||
![]() |
7be3835d57 | ||
![]() |
22ca4b177c | ||
![]() |
c8a3049349 | ||
![]() |
0cb8a5c7a9 | ||
![]() |
6ae11e83d6 | ||
![]() |
07915ca426 | ||
![]() |
51f4599031 | ||
![]() |
16932959d5 | ||
![]() |
9e9fcc5f94 | ||
![]() |
86d3e9bbf4 | ||
![]() |
26633b4b55 | ||
![]() |
e9e0729a77 | ||
![]() |
4352fcd884 |
22
.eslintrc.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: "plugin:react/recommended",
|
||||||
|
overrides: [],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["react", "@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/exhaustive-deps": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
additionalHooks: "(useRecoilCallback|useRecoilTransaction_UNSTABLE)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
168
.gitignore
vendored
|
@ -1,169 +1 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
BIN
architecture.png
Before Width: | Height: | Size: 35 KiB |
22
frontend/.eslintrc.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: "plugin:react/recommended",
|
||||||
|
overrides: [],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["react", "@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"react-hooks/exhaustive-deps": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
additionalHooks: "(useRecoilCallback|useRecoilTransaction_UNSTABLE)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
1
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/node_modules
|
12
frontend/Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM node:16.20.0
|
||||||
|
|
||||||
|
WORKDIR /opt/project
|
||||||
|
|
||||||
|
COPY package.json /opt/project
|
||||||
|
COPY package-lock.json /opt/project
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [ "npm", "run", "dev" ]
|
75
frontend/bin/dev.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const [webpackClientConfig, webpackServerConfig] = require("../webpack.config");
|
||||||
|
const nodemon = require("nodemon");
|
||||||
|
const path = require("path");
|
||||||
|
const webpackDevMiddleware = require("webpack-dev-middleware");
|
||||||
|
const webpackHotMiddleware = require("webpack-hot-middleware");
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
|
||||||
|
const hmrServer = express();
|
||||||
|
const clientCompiler = webpack(webpackClientConfig);
|
||||||
|
|
||||||
|
const allowedOrigins = ["http://localhost:3000", "http://localhost:3001"];
|
||||||
|
|
||||||
|
hmrServer.use(
|
||||||
|
cors({
|
||||||
|
origin: function (origin, callback) {
|
||||||
|
// allow requests with no origin
|
||||||
|
// (like mobile apps or curl requests)
|
||||||
|
if (!origin) return callback(null, true);
|
||||||
|
if (allowedOrigins.indexOf(origin) === -1) {
|
||||||
|
var msg =
|
||||||
|
"The CORS policy for this site does not " +
|
||||||
|
"allow access from the specified Origin.";
|
||||||
|
return callback(new Error(msg), false);
|
||||||
|
}
|
||||||
|
return callback(null, true);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
hmrServer.use(
|
||||||
|
webpackDevMiddleware(clientCompiler, {
|
||||||
|
publicPath: webpackClientConfig.output.publicPath,
|
||||||
|
serverSideRender: true,
|
||||||
|
noInfo: true,
|
||||||
|
watchOptions: {
|
||||||
|
ignore: /dist/,
|
||||||
|
},
|
||||||
|
writeToDisk: true,
|
||||||
|
stats: "errors-only",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
hmrServer.use(
|
||||||
|
webpackHotMiddleware(clientCompiler, {
|
||||||
|
path: "/static/__webpack_hmr",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
hmrServer.listen(3001, () => {
|
||||||
|
console.log("Hmr Server successfully started");
|
||||||
|
});
|
||||||
|
|
||||||
|
const compiler = webpack(webpackServerConfig);
|
||||||
|
|
||||||
|
compiler.run((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(`compilation failed:`, err);
|
||||||
|
}
|
||||||
|
compiler.watch({}, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(`compilation failed:`, err);
|
||||||
|
}
|
||||||
|
console.log("Compilation was successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
nodemon({
|
||||||
|
script: path.resolve(__dirname, "../dist/server/server.js"),
|
||||||
|
watch: [
|
||||||
|
path.resolve(__dirname, "../dist/server"),
|
||||||
|
path.resolve(__dirname, "../dist/client"),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
24
frontend/index.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Clicker"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="preconnect" href="/fonts/">
|
||||||
|
<title>Clicker</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<div id="modal_root"></div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
30312
frontend/package-lock.json
generated
Normal file
77
frontend/package.json
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
"name": "gpnevent",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "vite.config.js",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "16.x"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"predeploy": "npm run build:dev",
|
||||||
|
"deploy": "gh-pages -d build:dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"dev": "vite serve",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Anna Efremova",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@hot-loader/react-dom": "^17.0.1",
|
||||||
|
"@redux-devtools/extension": "^3.2.5",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/intl-tel-input": "^18.1.1",
|
||||||
|
"@types/jest": "^28.1.6",
|
||||||
|
"@types/react": "^17.0.50",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/react-linkify": "^1.0.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||||
|
"@typescript-eslint/parser": "^5.59.5",
|
||||||
|
"@vitejs/plugin-react": "^4.0.4",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"css-loader": "^3.4.2",
|
||||||
|
"eslint": "^8.40.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"gh-pages": "^4.0.0",
|
||||||
|
"html-webpack-plugin": "^4.5.2",
|
||||||
|
"intl-tel-input": "^18.2.1",
|
||||||
|
"js-sha256": "^0.9.0",
|
||||||
|
"less": "^3.13.1",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
|
"nodemon": "^2.0.12",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-confetti": "^6.1.0",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-hot-loader": "^4.13.0",
|
||||||
|
"react-linkify": "^1.0.0-alpha",
|
||||||
|
"react-player": "^2.12.0",
|
||||||
|
"react-redux": "^8.0.5",
|
||||||
|
"react-router-dom": "^6.11.1",
|
||||||
|
"react-select": "^5.7.3",
|
||||||
|
"redux": "^4.2.1",
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
|
"redux-thunk": "^2.4.2",
|
||||||
|
"style-loader": "^1.1.3",
|
||||||
|
"swiper": "^11.1.1",
|
||||||
|
"ts-jest": "^28.0.7",
|
||||||
|
"ts-loader": "^6.2.1",
|
||||||
|
"typescript": "4.6.4",
|
||||||
|
"usehooks-ts": "^3.1.0",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vite-plugin-html": "^3.2.0",
|
||||||
|
"webpack": "^4.42.0",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-dev-middleware": "^3.7.3",
|
||||||
|
"webpack-dev-server": "^3.10.3",
|
||||||
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
|
"webpack-node-externals": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/public/.DS_Store
vendored
Normal file
BIN
frontend/public/assets/.DS_Store
vendored
Normal file
BIN
frontend/public/assets/Angry.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/public/assets/Biceps.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/public/assets/Chain.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontend/public/assets/Fire.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/public/assets/Gift.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
frontend/public/assets/Money.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/public/assets/Monocle.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/public/assets/Rocket.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/public/assets/Volt.png
Normal file
After Width: | Height: | Size: 641 B |
BIN
frontend/public/assets/btnIcon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/assets/coin.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/public/assets/compass.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/assets/dev.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/public/assets/friends.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/assets/imgBtn1.png
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
frontend/public/assets/imgBtn2.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
frontend/public/assets/point.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/public/assets/police.gif
Normal file
After Width: | Height: | Size: 4.2 MiB |
BIN
frontend/public/assets/programming.gif
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
frontend/public/assets/shopping.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/assets/style1.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
frontend/public/assets/style2.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
frontend/public/assets/style3.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
frontend/public/assets/style4.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
frontend/public/fonts/Inter-Bold.woff
Normal file
BIN
frontend/public/fonts/Inter-Bold.woff2
Normal file
BIN
frontend/public/fonts/Inter-ExtraBold.woff
Normal file
BIN
frontend/public/fonts/Inter-ExtraBold.woff2
Normal file
BIN
frontend/public/fonts/Inter-Regular.woff
Normal file
BIN
frontend/public/fonts/Inter-Regular.woff2
Normal file
BIN
frontend/public/fonts/Inter-SemiBold.woff
Normal file
BIN
frontend/public/fonts/Inter-SemiBold.woff2
Normal file
BIN
frontend/public/fonts/Raleway-Regular.woff
Normal file
BIN
frontend/public/fonts/Raleway-Regular.woff2
Normal file
BIN
frontend/public/fonts/Raleway-SemiBold.woff
Normal file
BIN
frontend/public/fonts/Raleway-SemiBold.woff2
Normal file
15
frontend/public/manifest.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"short_name": "Clicker",
|
||||||
|
"name": "Clicker",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#222222",
|
||||||
|
"background_color": "#222222"
|
||||||
|
}
|
3
frontend/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
46
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import './main.global.css';
|
||||||
|
import { hot } from "react-hot-loader/root";
|
||||||
|
import { Layout } from "./shared/Layout";
|
||||||
|
import { applyMiddleware, createStore } from "redux";
|
||||||
|
import { rootReducer } from "./store/reducer";
|
||||||
|
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Route, Routes, BrowserRouter } from "react-router-dom";
|
||||||
|
import { AuctionMainPopups } from "./shared/Auction/AuctionMainPopups";
|
||||||
|
import { RoutePage } from "./shared/Pages/RoutePage";
|
||||||
|
|
||||||
|
const store = createStore(rootReducer, composeWithDevTools(
|
||||||
|
applyMiddleware(thunk)
|
||||||
|
));
|
||||||
|
|
||||||
|
function AppComponent() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
{mounted && (<BrowserRouter>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path='/' element={<RoutePage page='main' />} />
|
||||||
|
<Route path='/rating' element={<RoutePage page='rating' />} />
|
||||||
|
<Route path='/referral' element={<RoutePage page='referral' />} />
|
||||||
|
<Route path='/auction' element={<RoutePage page='auction' />} />
|
||||||
|
<Route path='/styles' element={<RoutePage page='styles' />} />
|
||||||
|
<Route path='/dev' element={<RoutePage page='dev' />} />
|
||||||
|
<Route path='*' element={<RoutePage page='main' />} />
|
||||||
|
</Routes>
|
||||||
|
<AuctionMainPopups />
|
||||||
|
</Layout>
|
||||||
|
</BrowserRouter>)}
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const App = hot(() => <AppComponent />);
|
||||||
|
|
BIN
frontend/src/assets/.DS_Store
vendored
Normal file
BIN
frontend/src/assets/Angry.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
frontend/src/assets/Biceps.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/src/assets/Chain.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontend/src/assets/Fire.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/src/assets/Gift.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
frontend/src/assets/Money.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/src/assets/Monocle.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/src/assets/Rocket.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/src/assets/Volt.png
Normal file
After Width: | Height: | Size: 641 B |
BIN
frontend/src/assets/dev.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/src/assets/friends.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/src/assets/imgBtn1.png
Normal file
After Width: | Height: | Size: 2.8 MiB |
BIN
frontend/src/assets/imgBtn2.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
frontend/src/assets/point.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
frontend/src/assets/police.gif
Normal file
After Width: | Height: | Size: 4.2 MiB |
BIN
frontend/src/assets/programming.gif
Normal file
After Width: | Height: | Size: 3.6 MiB |
BIN
frontend/src/assets/shopping.png
Normal file
After Width: | Height: | Size: 36 KiB |
7
frontend/src/client/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDom from "react-dom";
|
||||||
|
import { App } from "../App";
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
ReactDom.hydrate(<App />, document.getElementById("main_root"));
|
||||||
|
});
|
BIN
frontend/src/fonts/Inter-Bold.woff
Normal file
BIN
frontend/src/fonts/Inter-Bold.woff2
Normal file
BIN
frontend/src/fonts/Inter-ExtraBold.woff
Normal file
BIN
frontend/src/fonts/Inter-ExtraBold.woff2
Normal file
BIN
frontend/src/fonts/Inter-Regular.woff
Normal file
BIN
frontend/src/fonts/Inter-Regular.woff2
Normal file
BIN
frontend/src/fonts/Inter-SemiBold.woff
Normal file
BIN
frontend/src/fonts/Inter-SemiBold.woff2
Normal file
BIN
frontend/src/fonts/Raleway-Regular.woff
Normal file
BIN
frontend/src/fonts/Raleway-Regular.woff2
Normal file
BIN
frontend/src/fonts/Raleway-SemiBold.woff
Normal file
BIN
frontend/src/fonts/Raleway-SemiBold.woff2
Normal file
10
frontend/src/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
185
frontend/src/main.global.css
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./fonts/Inter-Regular.woff2') format("woff2"),
|
||||||
|
url('./fonts/Inter-Regular.woff') format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./fonts/Inter-SemiBold.woff2') format("woff2"),
|
||||||
|
url('./fonts/Inter-SemiBold.woff') format("woff");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./fonts/Inter-Bold.woff2') format("woff2"),
|
||||||
|
url('./fonts/Inter-Bold.woff') format("woff");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./fonts/Inter-ExtraBold.woff2') format("woff2"),
|
||||||
|
url('./fonts/Inter-ExtraBold.woff') format("woff");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
src: url('./fonts/Raleway-Regular.woff2') format("woff2"),
|
||||||
|
url('./fonts/Raleway-Regular.woff') format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
src: url('./fonts/Raleway-SemiBold.woff2') format("woff2"),
|
||||||
|
url('./fonts/Raleway-SemiBold.woff') format("woff");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #7EB4DB;
|
||||||
|
--gradientPrimary: linear-gradient(90deg, #90D7ED 13.05%, #6887C4 91.06%, #8085C0 172.24%);
|
||||||
|
--black: #000000;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--elBackround: #383838;
|
||||||
|
--textColor: #FFFFFF;
|
||||||
|
--textColor2: #8F8F8F;
|
||||||
|
--pink: #FC4848;
|
||||||
|
--red: #FF0000;
|
||||||
|
--blue: #7EB4DB;
|
||||||
|
--purple: #9747FF;
|
||||||
|
--gradientBlue: linear-gradient(90deg, #90D7ED 13.05%, #6887C4 91.06%, #8085C0 172.24%);
|
||||||
|
--gradientOrange: linear-gradient(302deg, #FF5421 -59.57%, #FF7248 43.7%, #FF9576 163.26%);
|
||||||
|
--gradientYellow: linear-gradient(302deg, #6ACB54 -59.57%, #DCBB5A 43.7%, #E2883D 163.26%);
|
||||||
|
--gradientOrangeYellow: linear-gradient(302deg, #FF805A -1.15%, #DEAE53 83.89%);
|
||||||
|
--grey6F: #6F6F6F;
|
||||||
|
--grey6C: #6C6C6C;
|
||||||
|
--grey35: #353535;
|
||||||
|
--grey34: #343434;
|
||||||
|
--grey1B: #1B1B1B;
|
||||||
|
--greyA4: #A4A4A4;
|
||||||
|
--grey46: #464646;
|
||||||
|
--grey12: #121212;
|
||||||
|
--grey19: #191919;
|
||||||
|
--grey77: #777777;
|
||||||
|
--grey24: #242424;
|
||||||
|
--grey22: #222222;
|
||||||
|
--grey9A: #9A9A9A;
|
||||||
|
--grey93: #939393;
|
||||||
|
--grey1F: #1F1F1F;
|
||||||
|
--grey27: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 120%;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, h1, h2, h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: var(--textColor);
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
font-size: 65px;
|
||||||
|
line-height: 120%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* slider */
|
||||||
|
|
||||||
|
.swiper-pagination-bullet {
|
||||||
|
margin: 0 2px !important;
|
||||||
|
height: 5px !important;
|
||||||
|
width: 5px !important;
|
||||||
|
background-color: var(--white) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
width: 20px !important;
|
||||||
|
border-radius: 5px !important;
|
||||||
|
background: var(--gradientPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-slide {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
19
frontend/src/server/indexTemplate.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export const indexTemplate = (content) => `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Clicker</title>
|
||||||
|
<script src="/static/client.js" type="application/javascript"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="main_root">${content}</div>
|
||||||
|
<div id="modal_root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
`;
|
20
frontend/src/server/server.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import express from "express";
|
||||||
|
import ReactDOM from "react-dom/server";
|
||||||
|
import { App } from "../App";
|
||||||
|
import { indexTemplate } from "./indexTemplate";
|
||||||
|
import compression from 'compression';
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(compression());
|
||||||
|
app.use("/static", express.static("./dist/client"));
|
||||||
|
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
res.send(indexTemplate(ReactDOM.renderToString(App())));
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`server started on port ${port}`);
|
||||||
|
});
|
||||||
|
|
79
frontend/src/shared/Auction/AuctionCard/AuctionCard.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './auctioncard.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { PointsBlock } from '../../Elements/PointsBlock';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { EIcons } from '../../Icons';
|
||||||
|
import { Timer } from '../Timer';
|
||||||
|
import { formatNumber } from '../../../utils/formatNumber';
|
||||||
|
import { Slider } from '../../Elements/Slider';
|
||||||
|
import { ModalWindow } from '../../ModalWindow';
|
||||||
|
import { AuctionPopup } from '../AuctionPopup';
|
||||||
|
import { ResultAuctionPopup } from '../ResultAuctionPopup';
|
||||||
|
import { DevPopup } from '../../Elements/DevPopup';
|
||||||
|
import { useAppSelector } from '../../hooks/useAppSelector';
|
||||||
|
|
||||||
|
interface IAuctionCard {
|
||||||
|
auctionId: string,
|
||||||
|
name: string,
|
||||||
|
imgs: Array<string>,
|
||||||
|
users: number,
|
||||||
|
prevBet: string,
|
||||||
|
myBetInit: string,
|
||||||
|
time: number,
|
||||||
|
isLead: boolean,
|
||||||
|
commission: number,
|
||||||
|
className ?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionCard({ name, imgs, users, prevBet, myBetInit, time, isLead, commission, auctionId, className }: IAuctionCard) {
|
||||||
|
const [myBet, setBet] = useState(Number(myBetInit));
|
||||||
|
const [myNewBet, setMyNewBet] = useState(0);
|
||||||
|
const [initPrevBet, setPrevBet] = useState(prevBet);
|
||||||
|
const [initLead, setLead] = useState(isLead);
|
||||||
|
const [closeWindow, setClose] = useState(true);
|
||||||
|
const [closeAnim, setCloseAnim] = useState(false);
|
||||||
|
const [closeresultPopup, setCloseResultPopup] = useState(true);
|
||||||
|
const styleIndex = Number(localStorage.getItem('selectedStyle'));
|
||||||
|
const [closeErrorBet, setCloseErrorBet] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${className} ${styleIndex===0 ? styles.darkContainer : styles.opacityContainer}`}>
|
||||||
|
<Slider className={styles.slider} imgs={imgs}/>
|
||||||
|
<h2 style={ETextStyles.InBd18120} className={styles.title}>{name}</h2>
|
||||||
|
<h3 style={ETextStyles.RwSb16120} className={styles.title2}>Подробности аукциона</h3>
|
||||||
|
<div className={`${styles.card} ${styles.cardFlex} ${styles.card1}`}>
|
||||||
|
<p style={ETextStyles.RwRg14100}>Минимальная ставка</p>
|
||||||
|
<PointsBlock points={initPrevBet} sizeIcon={20}/>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.card} ${styles.cardFlex} ${styles.card2}`}>
|
||||||
|
<p style={ETextStyles.RwRg14100}>Количество победителей</p>
|
||||||
|
<div className={styles.winnersNumber}>{users}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.card} ${initLead && styles.leadCard}`}>
|
||||||
|
<div className={styles.cardTop}>
|
||||||
|
<div className={styles.cardLeft} style={ETextStyles.RwSb14120}>{initLead ? <p><span>Ты в числе победителей! </span>Но все может поменяться</p>
|
||||||
|
: <p>{myBet > 0 ? 'Вашу ставку перебили, повысьте ее, чтобы сохранить лидерство' : 'Успей сделать ставку! До конца осталось:' }</p> }</div>
|
||||||
|
<Timer initTime={time}/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setClose(false)} text={myBet === 0 ? 'Сделать первую ставку' : <div className={styles.newBtn}>
|
||||||
|
<p>Увеличить ставку</p>
|
||||||
|
<div className={styles.prevText}>
|
||||||
|
<p style={ETextStyles.InRg12140}>{`Предыдущая ставка — ${formatNumber(myBet)}`}</p>
|
||||||
|
<div className={styles.icon} style={{ backgroundImage: "url('assets/btnIcon.png')"}}></div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
icon={EIcons.UpPriceIcon}/>
|
||||||
|
</div>
|
||||||
|
{!closeWindow && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setClose} removeBtn={true} modalBlock={
|
||||||
|
<AuctionPopup myBet={myBet} setCloseErrorBet={setCloseErrorBet} auctionId={auctionId} commission={commission} setLead={setLead} setClose={setCloseAnim} img={imgs[0]} name={name} prevBet={initPrevBet} prevUserImg={''} setBet={setMyNewBet} setCloseResultPopup={setCloseResultPopup}/>
|
||||||
|
} />}
|
||||||
|
{!closeresultPopup && closeErrorBet && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseResultPopup} removeBtn={true} modalBlock={
|
||||||
|
<ResultAuctionPopup prevBet={initPrevBet} prevMyBet={myBet} newBet={myNewBet} setBet={setBet} setClose={setCloseAnim} setCloseBetWindow={setClose} setPrevBet={setPrevBet}/>
|
||||||
|
} />}
|
||||||
|
{!closeErrorBet && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseErrorBet} removeBtn={true} modalBlock={
|
||||||
|
<DevPopup setClose={setCloseAnim} title='Возникла ошибка' text='Не получилось сделать ставку. Но мы скоро всё починим.' />
|
||||||
|
} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
126
frontend/src/shared/Auction/AuctionCard/auctioncard.module.css
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
.container {
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0px 0px 130px 0px rgba(124, 173, 216, 0.20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkContainer {
|
||||||
|
background-color: var(--grey22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkContainer .card {
|
||||||
|
background-color: var(--grey12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacityContainer {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacityContainer .card {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 15px 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--grey35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardFlex {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card1 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card2 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTop {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardLeft {
|
||||||
|
max-width: 187px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardLeft p span {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usersBlock {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userCount {
|
||||||
|
margin-left: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--grey35);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBtn {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBtn p {
|
||||||
|
color: var(--grey35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevText {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leadCard {
|
||||||
|
box-shadow: 0px 0px 15px 0px rgba(122, 170, 214, 0.48);
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 340/237;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnersNumber {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--grey34);
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionCard/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionCard';
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './auctionlosepopup.module.css';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { ProductCard } from '../ProductCard';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { generateRandomString } from '../../../utils/generateRandom';
|
||||||
|
|
||||||
|
interface IProduct {
|
||||||
|
name: string,
|
||||||
|
img: string,
|
||||||
|
bet: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAuctionLosePopup {
|
||||||
|
items: Array<IProduct>,
|
||||||
|
setClose(a: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionLosePopup({ items, setClose }: IAuctionLosePopup) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.iconBlock}>
|
||||||
|
<div className={styles.icon} style={{ backgroundImage: "url('assets/Angry.png')" }}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={styles.title} style={ETextStyles.RwSb24100}>Вы больше не в топе...</h2>
|
||||||
|
<p className={styles.descr} style={ETextStyles.RwSb14120}>Чтобы сохранить лидерство, повысьте свою ставку в аукционе.</p>
|
||||||
|
<h3 className={styles.title2} style={ETextStyles.RwSb18120}>Аукционы, в которых нужно увеличить ставку:</h3>
|
||||||
|
<div className={styles.cards}>
|
||||||
|
{items.map(item => {
|
||||||
|
return <ProductCard key={ generateRandomString() } name={item.name} img={item.img} bet={item.bet} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button text='Увеличить ставку' onClick={() => { navigate('/auction'); setClose(true) }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
.iconBlock {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.descr {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionLosePopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionLosePopup';
|
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './auctionmainpopups.module.css';
|
||||||
|
import { ModalWindow } from '../../ModalWindow';
|
||||||
|
import { AuctionWinPopup } from '../AuctionWinPopup';
|
||||||
|
import { AuctionTopPopup } from '../AuctionTopPopup';
|
||||||
|
import { AuctionLosePopup } from '../AuctionLosePopup';
|
||||||
|
import { useAppSelector } from '../../hooks/useAppSelector';
|
||||||
|
import { IAuctionItem } from '../../../store/me/actions';
|
||||||
|
|
||||||
|
export function AuctionMainPopups() {
|
||||||
|
const [closeWin, setCloseWin] = useState(true);
|
||||||
|
const [closeTop, setCloseTop] = useState(true);
|
||||||
|
const [closeLose, setCloseLose] = useState(true);
|
||||||
|
const [closeAnim, setCloseAnim] = useState(false);
|
||||||
|
const topAuctions = useAppSelector<Array<IAuctionItem> | undefined>(state=>state.me.data.topAuctions);
|
||||||
|
const loseAuctions = useAppSelector<Array<IAuctionItem> | undefined>(state => state.me.data.loseAuctions);
|
||||||
|
const winAuctions = useAppSelector<Array<IAuctionItem> | undefined>(state => state.me.data.winAuctions);
|
||||||
|
const [winInfo, setWinInfo] = useState<IAuctionItem>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let showWindow = false;
|
||||||
|
if (winAuctions && winAuctions.length != 0) {
|
||||||
|
for (let i = 0; i < winAuctions.length; i++) {
|
||||||
|
const winShow = localStorage.getItem('wS');
|
||||||
|
if (winShow) {
|
||||||
|
const winArray = JSON.parse(winShow);
|
||||||
|
if (winArray && winArray.length != 0) {
|
||||||
|
let isExist = false;
|
||||||
|
for (let k = 0; k < winArray.length; k++) {
|
||||||
|
if (Number(winArray[k]) === Number(winAuctions[i].id)) {
|
||||||
|
isExist = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!isExist) {
|
||||||
|
winArray.push(winAuctions[i].id);
|
||||||
|
localStorage.setItem('wS', JSON.stringify(winArray));
|
||||||
|
showWindow = true;
|
||||||
|
setWinInfo(winAuctions[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newArray = [];
|
||||||
|
newArray.push(winAuctions[i].id);
|
||||||
|
localStorage.setItem('wS', JSON.stringify(newArray));
|
||||||
|
showWindow = true;
|
||||||
|
setWinInfo(winAuctions[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newArray = [];
|
||||||
|
newArray.push(winAuctions[i].id);
|
||||||
|
localStorage.setItem('wS', JSON.stringify(newArray));
|
||||||
|
showWindow = true;
|
||||||
|
setWinInfo(winAuctions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showWindow) {
|
||||||
|
setCloseWin(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [winAuctions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const show = sessionStorage.getItem('shT');
|
||||||
|
if (show === 't' && closeTop) {
|
||||||
|
if (topAuctions && topAuctions.length != 0) {
|
||||||
|
sessionStorage.setItem('shT', 'f');
|
||||||
|
setCloseTop(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [topAuctions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const show = sessionStorage.getItem('shL');
|
||||||
|
if (show === 't' && closeLose) {
|
||||||
|
if (loseAuctions && loseAuctions.length != 0) {
|
||||||
|
sessionStorage.setItem('shL', 'f');
|
||||||
|
setCloseLose(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loseAuctions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!closeWin && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseWin} removeBtn={true} modalBlock={
|
||||||
|
<AuctionWinPopup name={winInfo?.name ? winInfo?.name : ''} img={winInfo?.img ? winInfo?.img : ''} setClose={setCloseAnim}/>
|
||||||
|
} />}
|
||||||
|
{!closeTop && topAuctions != undefined && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseTop} removeBtn={true} modalBlock={
|
||||||
|
<AuctionTopPopup items={topAuctions} setClose={setCloseAnim}/>
|
||||||
|
} />}
|
||||||
|
{!closeLose && loseAuctions != undefined && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseLose} removeBtn={true} modalBlock={
|
||||||
|
<AuctionLosePopup items={loseAuctions} setClose={setCloseAnim} />
|
||||||
|
} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionMainPopups/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionMainPopups';
|
115
frontend/src/shared/Auction/AuctionPopup/AuctionPopup.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './auctionpopup.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { EIcons } from '../../Icons';
|
||||||
|
import { declension } from '../../../utils/declension';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ProductCard } from '../ProductCard';
|
||||||
|
import { useAppSelector } from '../../hooks/useAppSelector';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { updatePointsRequestAsync } from '../../../store/me/actions';
|
||||||
|
import { updateAuction } from '../../../store/auction/actions';
|
||||||
|
|
||||||
|
interface IAuctionPopup {
|
||||||
|
auctionId: string,
|
||||||
|
setClose(a: boolean): void,
|
||||||
|
setLead(a: boolean): void,
|
||||||
|
img: string,
|
||||||
|
name: string,
|
||||||
|
prevBet: string,
|
||||||
|
prevUserImg: string,
|
||||||
|
setBet(a: number): void,
|
||||||
|
setCloseResultPopup(a: boolean): void,
|
||||||
|
commission: number,
|
||||||
|
setCloseErrorBet(a: boolean): void,
|
||||||
|
myBet: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionPopup({ setClose, setCloseErrorBet, auctionId, img, name, prevBet, prevUserImg, setBet, setLead, setCloseResultPopup, commission, myBet }: IAuctionPopup) {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [disabled, setDis] = useState(true);
|
||||||
|
const [autoBet, setAutoBet] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [percent, setPercent] = useState(commission);
|
||||||
|
const userPoints = Number(useAppSelector<string | undefined>(state=>state.me.data.points));
|
||||||
|
const URL = useAppSelector<string>(state=>state.url);
|
||||||
|
const token = useAppSelector<string>(state => state.token);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let newValue = event.target.value;
|
||||||
|
newValue = newValue.replace(/[^0-9]/g, '');
|
||||||
|
setValue(newValue);
|
||||||
|
|
||||||
|
if (newValue.length != 0) {
|
||||||
|
setDis(false);
|
||||||
|
} else {
|
||||||
|
setDis(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBet = () => {
|
||||||
|
const bet = Number(value);
|
||||||
|
setClose(true);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
axios.post(`${URL}/api/v1/auction/auction/${auctionId}/place-bet/?value=${bet}`, {},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Authorization": `TelegramToken ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then(resp => {
|
||||||
|
const data = resp.data;
|
||||||
|
dispatch<any>(updatePointsRequestAsync());
|
||||||
|
dispatch<any>(updateAuction(auctionId));
|
||||||
|
setBet(bet);
|
||||||
|
//setLead(true);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCloseResultPopup(false);
|
||||||
|
clearTimeout(timer);
|
||||||
|
}, 400);
|
||||||
|
}).catch(err => {
|
||||||
|
setCloseErrorBet(false);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.title} style={ETextStyles.RwSb24100}>Сделать ставку</h2>
|
||||||
|
<ProductCard name={name} img={img} bet={prevBet} personImg={prevUserImg} className={styles.card} />
|
||||||
|
{!autoBet ? <Button onClick={() => { setAutoBet(true), setValue(Number(Number((1 + percent / 100) * Number(prevBet)).toFixed(2)).toString()), setDis(false) }} text='Сразу перебить ставку' className={styles.btnFirst} icon={<div className={styles.icon} style={{ backgroundImage: "url('assets/Rocket.png')" }}></div>} /> :
|
||||||
|
<button style={ETextStyles.InBd14120} className={styles.btnCancel} onClick={() => setClose(true)}>Не перебивать</button>
|
||||||
|
}
|
||||||
|
<p className={styles.descr} style={ETextStyles.RwRg10140}>Наши алгоритмы автоматически рассчитают стоимость, чтобы ваша ставка стала самой высокой</p>
|
||||||
|
<h3 className={styles.title2} style={ETextStyles.InSb14120}>{!autoBet ? 'Ввести свою цену' : 'Цена, чтобы перебить ставку'}</h3>
|
||||||
|
<input style={ETextStyles.InSb14120} className={styles.input} value={value} type="text" onChange={handleChange} inputMode="numeric" />
|
||||||
|
{(Number(Number((1 + percent / 100) * Number(value))) - myBet < userPoints) ? ((Number(value) < Number(prevBet) && value.length > 0) ?
|
||||||
|
<button className={styles.btnForbidden}>
|
||||||
|
<p style={ETextStyles.InBd14120}>Ставка должна быть больше</p>
|
||||||
|
<p style={ETextStyles.InRg12140} className={styles.textForbidden}>Нельзя сделать ставку меньше предыдущей</p>
|
||||||
|
</button>
|
||||||
|
: <Button onClick={() => newBet()} disabled={disabled} text={disabled ? 'Перебить ставку' : <div className={styles.newBtn}>
|
||||||
|
<p>Перебить ставку</p>
|
||||||
|
<div className={styles.btnText}>
|
||||||
|
<p style={ETextStyles.InRg12140}>{`${declension(value, 'коин', 'коина', 'коинов', true)} + ${percent}% = ${declension(Number(Number((1 + percent / 100) * Number(value)).toFixed(2)), 'коин', 'коина', 'коинов', true)}`}</p>
|
||||||
|
<div className={styles.icon} style={{ backgroundImage: "url('assets/btnIcon.png')" }}></div>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
icon={EIcons.UpPriceIcon} />
|
||||||
|
) :
|
||||||
|
<button className={styles.btnForbidden} onClick={() => navigate('/')}>
|
||||||
|
<p style={ETextStyles.InBd14120}>Тебе не хватает очков</p>
|
||||||
|
<div className={styles.btnText}>
|
||||||
|
<p style={ETextStyles.InRg12140} className={styles.textForbidden}>Нажми сюда, чтобы накопить еще</p>
|
||||||
|
<div className={styles.icon} style={{ backgroundImage: "url('assets/btnIcon.png')" }}></div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
.title {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnFirst {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descr {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: var(--grey9A)
|
||||||
|
}
|
||||||
|
|
||||||
|
.title2 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom:16px;
|
||||||
|
padding: 6px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--grey22);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--grey93);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-bottom: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnText {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBtn {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newBtn p {
|
||||||
|
color: var(--grey35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnText {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnForbidden {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 2px solid var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textForbidden {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnCancel {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionPopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionPopup';
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './auctiontoppopup.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { ProductCard } from '../ProductCard';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { generateRandomString } from '../../../utils/generateRandom';
|
||||||
|
|
||||||
|
interface IProduct {
|
||||||
|
name: string,
|
||||||
|
img: string,
|
||||||
|
bet: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAuctionTopPopup {
|
||||||
|
items: Array<IProduct>,
|
||||||
|
setClose(a: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionTopPopup({ items, setClose }: IAuctionTopPopup) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='top'>
|
||||||
|
<div className={styles.iconBlock}>
|
||||||
|
<div className={styles.icon} style={{ backgroundImage: "url('assets/Fire.png')" }}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={styles.title} style={ETextStyles.RwSb24100}>Вы в топе</h2>
|
||||||
|
<p className={styles.descr} style={ETextStyles.RwSb14120}>Кликайте, чтобы заработать больше очков и потратить их на ставку в аукционе.</p>
|
||||||
|
<h3 className={styles.title2} style={ETextStyles.RwSb18120}>Аукционы, в которых вы лидируете:</h3>
|
||||||
|
<div className={styles.cards}>
|
||||||
|
{items.map(item => {
|
||||||
|
return <ProductCard key={ generateRandomString() } name={item.name} img={item.img} bet={item.bet} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button text='Продолжить кликать' onClick={() => { navigate('/'); setClose(true)}}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
.iconBlock {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.descr {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title2 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|