Initial frontend commit

This commit is contained in:
Arseniy Sitnikov 2024-11-18 16:33:44 +03:00
parent c1869e90c2
commit 4352fcd884
233 changed files with 35100 additions and 0 deletions

BIN
frontend/.DS_Store vendored Normal file

Binary file not shown.

22
frontend/.eslintrc.js Normal file
View 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
View File

@ -0,0 +1 @@
/node_modules

12
frontend/Dockerfile Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

77
frontend/package.json Normal file
View 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

Binary file not shown.

BIN
frontend/public/assets/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

44
frontend/src/App.tsx Normal file
View 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 />);

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
frontend/src/assets/dev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View 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"));
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

10
frontend/src/index.tsx Normal file
View 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')
);

View 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;
}

View 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>
`;

View 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}`);
});

View 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>
);
}

View 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;
}

View File

@ -0,0 +1 @@
export * from './AuctionCard';

View File

@ -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}>Чтобы сохранить лидерство, повысьте свою ставку в&nbsp;аукционе.</p>
<h3 className={styles.title2} style={ETextStyles.RwSb18120}>Аукционы, в&nbsp;которых нужно увеличить ставку:</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>
);
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './AuctionLosePopup';

View File

@ -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>
);
}

View File

@ -0,0 +1 @@
export * from './AuctionMainPopups';

View 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>
);
}

View File

@ -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);
}

View File

@ -0,0 +1 @@
export * from './AuctionPopup';

View File

@ -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}>Кликайте, чтобы заработать больше очков и&nbsp;потратить их&nbsp;на&nbsp;ставку в&nbsp;аукционе.</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>
);
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './AuctionTopPopup';

View File

@ -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}>Поздравляем!!! Приз, за&nbsp;который вы&nbsp;так упорно боролись, теперь ваш. Ожидайте сообщение в&nbsp;Telegram от&nbsp;нашей команды.</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>
);
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './AuctionWinPopup';

View 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)
);
}

View 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;
}
}

View File

@ -0,0 +1 @@
export * from './ConfettiAnim';

View 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>
);
}

View File

@ -0,0 +1 @@
export * from './ProductCard';

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -0,0 +1 @@
export * from './ResultAuctionPopup';

View File

@ -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);
}

View 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>
);
}

View File

@ -0,0 +1 @@
export * from './Timer';

Some files were not shown because too many files have changed in this diff Show More