Initial frontend commit
BIN
frontend/.DS_Store
vendored
Normal file
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"),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
26
frontend/index.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||||
|
/>
|
||||||
|
<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/dev.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 162 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:
|
44
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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' />} />
|
||||||
|
</Routes>
|
||||||
|
<AuctionMainPopups />
|
||||||
|
</Layout>
|
||||||
|
</BrowserRouter>)}
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const App = hot(() => <AppComponent />);
|
||||||
|
|
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/btnIcon.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src/assets/coin.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
frontend/src/assets/dev.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/src/assets/imgBtn1.png
Normal file
After Width: | Height: | Size: 566 KiB |
BIN
frontend/src/assets/imgBtn2.png
Normal file
After Width: | Height: | Size: 532 KiB |
BIN
frontend/src/assets/style1.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
frontend/src/assets/style2.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
frontend/src/assets/style3.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
frontend/src/assets/style4.png
Normal file
After Width: | Height: | Size: 164 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')
|
||||||
|
);
|
182
frontend/src/main.global.css
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
|
|
73
frontend/src/shared/Auction/AuctionCard/AuctionCard.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './auctioncard.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { PointsBlock } from '../../Elements/PointsBlock';
|
||||||
|
import { PersonIcon } from '../../Elements/PersonIcon';
|
||||||
|
import { UsersIcons } from '../../Elements/UsersIcons';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface IAuctionCard {
|
||||||
|
name: string,
|
||||||
|
imgs: Array<string>,
|
||||||
|
minBet: string,
|
||||||
|
users: number,
|
||||||
|
prevBet: string,
|
||||||
|
myBetInit: string,
|
||||||
|
time: number,
|
||||||
|
isLead: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionCard({ name, imgs, users, prevBet, myBetInit, time, isLead }: 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'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${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}>{users === 0 ? 'Ты единственный участник' : 'Участники аукциона'}</p>
|
||||||
|
{users === 0 ? <PersonIcon /> : <div className={styles.usersBlock}><UsersIcons/> {users > 3 && <div className={styles.userCount} style={ETextStyles.InSb10120}>{users-3}</div>}</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>}</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 setLead={setLead} setClose={setCloseAnim} img={imgs[0]} name={name} prevBet={initPrevBet} prevUserImg={''} setBet={setMyNewBet} setCloseResultPopup={setCloseResultPopup}/>
|
||||||
|
} />}
|
||||||
|
{!closeresultPopup && <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}/>
|
||||||
|
} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
116
frontend/src/shared/Auction/AuctionCard/auctioncard.module.css
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
.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;
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionCard/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionCard';
|
|
@ -0,0 +1,38 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 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,40 @@
|
||||||
|
import React, { 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';
|
||||||
|
|
||||||
|
export function AuctionMainPopups() {
|
||||||
|
const [closeWin, setCloseWin] = useState(true);
|
||||||
|
const [closeTop, setCloseTop] = useState(true);
|
||||||
|
const [closeLose, setCloseLose] = useState(true);
|
||||||
|
const [closeAnim, setCloseAnim] = useState(false);
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
name: 'iPhone 15 Pro Max',
|
||||||
|
img: '',
|
||||||
|
bet: '788'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iPhone 13 Pro',
|
||||||
|
img: '',
|
||||||
|
bet: '200'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!closeWin && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseWin} removeBtn={true} modalBlock={
|
||||||
|
<AuctionWinPopup name='iPhone 15 Pro Max ' img='' setClose={setCloseAnim}/>
|
||||||
|
} />}
|
||||||
|
{!closeTop && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseTop} removeBtn={true} modalBlock={
|
||||||
|
<AuctionTopPopup items={items} setClose={setCloseAnim}/>
|
||||||
|
} />}
|
||||||
|
{!closeLose && <ModalWindow closeAnimOut={closeAnim} setCloseAnimOut={setCloseAnim} setClose={setCloseLose} removeBtn={true} modalBlock={
|
||||||
|
<AuctionLosePopup items={items} setClose={setCloseAnim} />
|
||||||
|
} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionMainPopups/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionMainPopups';
|
89
frontend/src/shared/Auction/AuctionPopup/AuctionPopup.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './auctionpopup.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { PersonIcon } from '../../Elements/PersonIcon';
|
||||||
|
import { PointsBlock } from '../../Elements/PointsBlock';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { EIcons } from '../../Icons';
|
||||||
|
import { declension } from '../../../utils/declension';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ProductCard } from '../ProductCard';
|
||||||
|
|
||||||
|
interface IAuctionPopup {
|
||||||
|
setClose(a: boolean): void,
|
||||||
|
setLead(a: boolean): void,
|
||||||
|
img: string,
|
||||||
|
name: string,
|
||||||
|
prevBet: string,
|
||||||
|
prevUserImg: string,
|
||||||
|
setBet(a: number): void,
|
||||||
|
setCloseResultPopup(a: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionPopup({ setClose, img, name, prevBet, prevUserImg, setBet, setLead, setCloseResultPopup }: IAuctionPopup) {
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
const [disabled, setDis] = useState(true);
|
||||||
|
const [autoBet, setAutoBet] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const percent = 5;
|
||||||
|
const userPoints = 1000;
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
setBet(Math.floor((1 + percent / 100) * Number(value)));
|
||||||
|
setClose(true);
|
||||||
|
setLead(true);
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCloseResultPopup(false);
|
||||||
|
clearTimeout(timer);
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
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(prevBet) + 5).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" />
|
||||||
|
{(Math.floor((1 + percent / 100) * Number(value)) < 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(Math.floor((1 + percent / 100) * Number(value)), 'коин', 'коина', 'коинов', 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,38 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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 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;
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionTopPopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionTopPopup';
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './auctionwinpopup.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
import { PersonIcon } from '../../Elements/PersonIcon';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ConfettiAnim } from '../Confetti';
|
||||||
|
|
||||||
|
interface IAuctionWinPopup {
|
||||||
|
name: string,
|
||||||
|
img: string,
|
||||||
|
setClose(a: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuctionWinPopup({ name, img, setClose }: IAuctionWinPopup) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [startConfetti, setStartConfetti] = useState(0);
|
||||||
|
const [removeConfetti, setRemoveConfetti] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.iconBlock}>
|
||||||
|
<div className={styles.icon} style={{backgroundImage: "url('assets/Gift.png')"}}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={styles.title} style={ETextStyles.RwSb24100}>Вы выиграли в аукционе!</h2>
|
||||||
|
<p className={styles.descr} style={ETextStyles.RwSb14120}>Поздравляем!!! Приз, за который вы так упорно боролись, теперь ваш. Ожидайте сообщение в Telegram от нашей команды.</p>
|
||||||
|
<h3 className={styles.title2} style={ETextStyles.RwSb18120}>Ваш приз</h3>
|
||||||
|
<Button className={styles.productWin} text={name} icon={<PersonIcon img={img} />} onClick={() => setStartConfetti(1)}/>
|
||||||
|
<Button text='Продолжить кликать' stroke={true} onClick={() => { navigate('/'); setClose(true); setRemoveConfetti(true)}}/>
|
||||||
|
{!removeConfetti && <ConfettiAnim opacity={startConfetti} setOpacity={setStartConfetti}/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productWin {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
1
frontend/src/shared/Auction/AuctionWinPopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuctionWinPopup';
|
37
frontend/src/shared/Auction/Confetti/ConfettiAnim.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import styles from './confetti.module.css';
|
||||||
|
import { useWindowSize } from 'usehooks-ts'
|
||||||
|
import Confetti from 'react-confetti';
|
||||||
|
|
||||||
|
interface IConfettiAnim {
|
||||||
|
opacity: number,
|
||||||
|
setOpacity(a: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfettiAnim({ opacity, setOpacity }: IConfettiAnim) {
|
||||||
|
const node = document.querySelector('#modal_root');
|
||||||
|
const { width, height } = useWindowSize();
|
||||||
|
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(opacity === 1) {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setOpacity(0);
|
||||||
|
clearInterval(timer);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}, [opacity]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
ReactDOM.createPortal((
|
||||||
|
<Confetti
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={`${styles.confetti} ${opacity === 1 && styles.animation}`}
|
||||||
|
opacity={opacity}
|
||||||
|
/>
|
||||||
|
), node)
|
||||||
|
);
|
||||||
|
}
|
21
frontend/src/shared/Auction/Confetti/confetti.module.css
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.confetti {
|
||||||
|
z-index: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation {
|
||||||
|
animation: 5s 1 confetti;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes confetti {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
1
frontend/src/shared/Auction/Confetti/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ConfettiAnim';
|
28
frontend/src/shared/Auction/ProductCard/ProductCard.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './productcard.module.css';
|
||||||
|
import { PersonIcon } from '../../Elements/PersonIcon';
|
||||||
|
import { PointsBlock } from '../../Elements/PointsBlock';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
|
||||||
|
interface IProductCard {
|
||||||
|
name: string,
|
||||||
|
img: string,
|
||||||
|
bet: string,
|
||||||
|
personImg ?: boolean | string,
|
||||||
|
className ?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductCard({ name, img, bet, personImg=false, className }: IProductCard) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.card} ${className}`} >
|
||||||
|
<div className={styles.left} style={{ width: `calc(100% - ${55 + bet.length * 9.5}px)` }}>
|
||||||
|
<PersonIcon img={img} className={styles.productImg} />
|
||||||
|
<p style={{ ...ETextStyles.InSb14120 }} className={styles.productName}>{name}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.right}>
|
||||||
|
<PointsBlock points={bet} left={true} className={styles.points} />
|
||||||
|
{personImg && typeof personImg === 'string' && <PersonIcon img={personImg} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/shared/Auction/ProductCard/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ProductCard';
|
|
@ -0,0 +1,36 @@
|
||||||
|
.card {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid var(--grey46);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productName {
|
||||||
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points div {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.productImg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './resultauctionpopup.module.css';
|
||||||
|
import { declension } from '../../../utils/declension';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { PointsBlock } from '../../Elements/PointsBlock';
|
||||||
|
import { Button } from '../../Button';
|
||||||
|
|
||||||
|
interface IResultAuctionPopup {
|
||||||
|
prevBet: string,
|
||||||
|
prevMyBet: number,
|
||||||
|
newBet: number,
|
||||||
|
setBet(a: number): void,
|
||||||
|
setClose(a: boolean): void,
|
||||||
|
setCloseBetWindow(a: boolean): void
|
||||||
|
setPrevBet(a: string): void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResultAuctionPopup({ prevBet, prevMyBet, newBet, setBet, setClose, setCloseBetWindow, setPrevBet }: IResultAuctionPopup) {
|
||||||
|
const [diff, setDiff] = useState(0);
|
||||||
|
const [prevBetOld, setPrevBetOld] = useState(prevBet);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDiff(newBet - prevMyBet);
|
||||||
|
setBet(newBet);
|
||||||
|
setPrevBet(newBet.toString())
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setClose(true);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCloseBetWindow(false);
|
||||||
|
clearInterval(timer);
|
||||||
|
}, 400);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.topIcon}>
|
||||||
|
<div className={styles.icon} style={{backgroundImage: 'url("assets/Money.png")'}}></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.text} style={ETextStyles.InSb24100}>
|
||||||
|
Вы <span>увеличили</span> ставку <span>{`на ${diff}`}</span> {declension(diff, 'коин', 'коина', 'коинов')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.cards}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<p style={ETextStyles.RwSb16120}>Сколько вы потратили</p>
|
||||||
|
<PointsBlock left={true} points={newBet.toString()} className={styles.colorPoints}/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<p style={ETextStyles.RwSb16120}>Предыдущая ставка</p>
|
||||||
|
<PointsBlock left={true} points={prevBetOld.toString()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button text='Увеличить шанс на выигрыш' onClick={() => onClick()}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/shared/Auction/ResultAuctionPopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ResultAuctionPopup';
|
|
@ -0,0 +1,50 @@
|
||||||
|
.icon {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text span {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 50%;
|
||||||
|
height: 112px;
|
||||||
|
background-color: var(--grey12);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
color: var(--greyA4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPoints div {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
53
frontend/src/shared/Auction/Timer/Timer.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './timer.module.css';
|
||||||
|
import { ETextStyles } from '../../texts';
|
||||||
|
import { declension } from '../../../utils/declension';
|
||||||
|
import { secondsToHMS } from '../../../utils/secondsToHMS';
|
||||||
|
|
||||||
|
interface ITimer {
|
||||||
|
initTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer({initTime}: ITimer) {
|
||||||
|
const timeValue: Array<number> = secondsToHMS(initTime);
|
||||||
|
const [[hour, min, sec], setTime] = useState([timeValue[0], timeValue[1], timeValue[2]]);
|
||||||
|
const [over, setOver] = useState(false);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (hour === 0 && min === 0 && sec === 0) {
|
||||||
|
setOver(true);
|
||||||
|
} else if (min === 0 && sec === 0) {
|
||||||
|
setTime([hour - 1, 59, 59]);
|
||||||
|
} else if (sec == 0) {
|
||||||
|
setTime([hour, min - 1, 59]);
|
||||||
|
} else {
|
||||||
|
setTime([hour, min, sec - 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!over) {
|
||||||
|
const timerID = setInterval(() => tick(), 1000);
|
||||||
|
return () => clearInterval(timerID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.block}>
|
||||||
|
<p className={styles.value} style={ETextStyles.InSb14120}>{hour}</p>
|
||||||
|
<p className={styles.text} style={ETextStyles.RwRg12120}>{declension(hour, 'час', 'часа', 'часов')}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.dot}></div>
|
||||||
|
<div className={styles.block}>
|
||||||
|
<p className={styles.value} style={ETextStyles.InSb14120}>{min}</p>
|
||||||
|
<p className={styles.text} style={ETextStyles.RwRg12120}>мин</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.dot}></div>
|
||||||
|
<div className={styles.block}>
|
||||||
|
<p className={styles.value} style={ETextStyles.InSb14120}>{sec.toString().length === 1 ? `0${sec}` : sec}</p>
|
||||||
|
<p className={styles.text} style={ETextStyles.RwRg12120}>сек</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
frontend/src/shared/Auction/Timer/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Timer';
|