Merge remote-tracking branch 'origin/frontend' into dev

This commit is contained in:
Michail Kostochka 2024-12-12 22:23:09 +03:00
commit a42428cb53
217 changed files with 35984 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea .idea
.env .env
.DS_Store

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

24
frontend/index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Clicker"
/>
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="/fonts/">
<title>Clicker</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal_root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

30312
frontend/package-lock.json generated Normal file

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.

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:

BIN
frontend/src/.DS_Store vendored Normal file

Binary file not shown.

46
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,46 @@
import React, { useEffect, useState } from "react";
import './main.global.css';
import { hot } from "react-hot-loader/root";
import { Layout } from "./shared/Layout";
import { applyMiddleware, createStore } from "redux";
import { rootReducer } from "./store/reducer";
import { composeWithDevTools } from '@redux-devtools/extension';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { Route, Routes, BrowserRouter } from "react-router-dom";
import { AuctionMainPopups } from "./shared/Auction/AuctionMainPopups";
import { RoutePage } from "./shared/Pages/RoutePage";
const store = createStore(rootReducer, composeWithDevTools(
applyMiddleware(thunk)
));
function AppComponent() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<Provider store={store}>
{mounted && (<BrowserRouter>
<Layout>
<Routes>
<Route path='/' element={<RoutePage page='main' />} />
<Route path='/rating' element={<RoutePage page='rating' />} />
<Route path='/referral' element={<RoutePage page='referral' />} />
<Route path='/auction' element={<RoutePage page='auction' />} />
<Route path='/styles' element={<RoutePage page='styles' />} />
<Route path='/dev' element={<RoutePage page='dev' />} />
<Route path='*' element={<RoutePage page='main' />} />
</Routes>
<AuctionMainPopups />
</Layout>
</BrowserRouter>)}
</Provider>
)
};
export const App = hot(() => <AppComponent />);

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

Binary file not shown.

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,185 @@
@font-face {
font-family: 'Inter';
src: url('./fonts/Inter-Regular.woff2') format("woff2"),
url('./fonts/Inter-Regular.woff') format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('./fonts/Inter-SemiBold.woff2') format("woff2"),
url('./fonts/Inter-SemiBold.woff') format("woff");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('./fonts/Inter-Bold.woff2') format("woff2"),
url('./fonts/Inter-Bold.woff') format("woff");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('./fonts/Inter-ExtraBold.woff2') format("woff2"),
url('./fonts/Inter-ExtraBold.woff') format("woff");
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Raleway';
src: url('./fonts/Raleway-Regular.woff2') format("woff2"),
url('./fonts/Raleway-Regular.woff') format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Raleway';
src: url('./fonts/Raleway-SemiBold.woff2') format("woff2"),
url('./fonts/Raleway-SemiBold.woff') format("woff");
font-weight: 600;
font-style: normal;
font-display: swap;
}
:root {
--primary: #7EB4DB;
--gradientPrimary: linear-gradient(90deg, #90D7ED 13.05%, #6887C4 91.06%, #8085C0 172.24%);
--black: #000000;
--white: #FFFFFF;
--elBackround: #383838;
--textColor: #FFFFFF;
--textColor2: #8F8F8F;
--pink: #FC4848;
--red: #FF0000;
--blue: #7EB4DB;
--purple: #9747FF;
--gradientBlue: linear-gradient(90deg, #90D7ED 13.05%, #6887C4 91.06%, #8085C0 172.24%);
--gradientOrange: linear-gradient(302deg, #FF5421 -59.57%, #FF7248 43.7%, #FF9576 163.26%);
--gradientYellow: linear-gradient(302deg, #6ACB54 -59.57%, #DCBB5A 43.7%, #E2883D 163.26%);
--gradientOrangeYellow: linear-gradient(302deg, #FF805A -1.15%, #DEAE53 83.89%);
--grey6F: #6F6F6F;
--grey6C: #6C6C6C;
--grey35: #353535;
--grey34: #343434;
--grey1B: #1B1B1B;
--greyA4: #A4A4A4;
--grey46: #464646;
--grey12: #121212;
--grey19: #191919;
--grey77: #777777;
--grey24: #242424;
--grey22: #222222;
--grey9A: #9A9A9A;
--grey93: #939393;
--grey1F: #1F1F1F;
--grey27: #272727;
}
body {
padding: 0;
margin: 0;
font-size: 12px;
line-height: 120%;
font-family: 'Inter', sans-serif;
font-weight: 400;
box-sizing: border-box;
color: var(--textColor);
}
html {
scroll-behavior: smooth;
overflow-x: hidden;
}
p, h1, h2, h3 {
margin: 0;
padding: 0;
}
h1,
h2,
h3 {
font-weight: 700;
line-height: 120%;
}
* {
color: var(--textColor);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
a {
text-decoration: none;
}
input::placeholder {
white-space: pre-wrap;
}
button {
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.main-header {
font-size: 65px;
line-height: 120%;
font-weight: 500;
}
#root {
height: 100%;
}
.background {
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 0;
}
/* slider */
.swiper-pagination-bullet {
margin: 0 2px !important;
height: 5px !important;
width: 5px !important;
background-color: var(--white) !important;
opacity: 1 !important;
}
.swiper-pagination-bullet-active {
width: 20px !important;
border-radius: 5px !important;
background: var(--gradientPrimary) !important;
}
.swiper-slide {
display: flex !important;
align-items: center;
justify-content: center;
}

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';

View File

@ -0,0 +1,30 @@
.container {
width: 110px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.block {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 33px;
flex-shrink: 0;
}
.text {
color: var(--greyA4);
}
.dot {
flex-shrink: 0;
padding-left: 1px;
padding-right: 1px;
width: 3px;
height: 3px;
border-radius: 50%;
background-color: var(--white);
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import styles from './button.module.css';
import { ETextStyles } from '../texts';
interface IButton {
text: string | boolean | React.ReactNode,
className ?: string,
onClick?:() => void,
icon?: React.ReactNode,
disabled?: boolean,
stroke?: boolean,
}
export function Button({ className, onClick, text, icon, disabled = false, stroke=false }: IButton) {
return (
<button disabled={disabled} onClick={onClick} className={`${styles.btn} ${className} ${disabled && styles.dis} ${stroke && styles.stroke}`} style={ETextStyles.InSb16120}>
<div className={styles.content}>
{icon && icon}
{text}
</div>
</button>
);
}

View File

@ -0,0 +1,29 @@
.btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 13px;
border-radius: 30px;
background: var(--gradientPrimary);
}
.content {
display: flex;
align-items: center;
gap: 4px;
color: var(--grey34);
}
.dis {
background: var(--greyA4);
}
.stroke {
background: none;
border: 2px solid var(--primary);
}
.stroke .content {
color: var(--white);
}

View File

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

View File

@ -0,0 +1,170 @@
import React, { useEffect, useState } from 'react';
import styles from './clickerbtn.module.css';
import { ModalWindow } from '../../ModalWindow';
import { ClickerPopup } from '../ClickerPopup';
import { getGradient } from '../../../utils/getGradient';
import { useAppSelector } from '../../hooks/useAppSelector';
import { useDispatch } from 'react-redux';
import { IUserData, updateEnergyRequestAsync } from '../../../store/me/actions';
import axios from 'axios';
import { DevPopup } from '../../Elements/DevPopup';
import { saveMult } from '../../../store/mult';
import { Spinner } from '../../Elements/Spinner';
interface IClickerBtn {
coins: number,
setCoins(a: number): void,
energy: number,
setMult(a: number): void,
setEnergy(a: number): void
}
export function ClickerBtn({ setCoins, energy, setMult, coins, setEnergy }: IClickerBtn) {
const urlClick = useAppSelector<string>(state => state.urlClick);
const token = useAppSelector<string>(state => state.token);
const [fill, setFill] = useState(0);
const [size, setSize] = useState(240);
const circumference = 2 * Math.PI * 125;
const dashArray = fill * circumference / 100;
const img = fill < 100 ? '/assets/imgBtn1.png' : '/assets/imgBtn2.png';
const [close, setClose] = useState(true);
const [gradient, setGradient] = useState(getGradient());
let styleIndex = useAppSelector<number>(state => state.styleIndex);
const [maxEnergy, setMaxEnergy] = useState(500);
const dispatch = useDispatch();
useEffect(() => {
setFill((maxEnergy - energy) / maxEnergy * 100);
}, [energy]);
useEffect(() => {
const savedEnergy = sessionStorage.getItem('eg');
if(savedEnergy) {
const encodeEnergy = atob(savedEnergy);
setMaxEnergy(Number(encodeEnergy));
}
setFill((maxEnergy - energy) / maxEnergy * 100);
}, []);
useEffect(() => {
setGradient(getGradient())
}, [styleIndex]);
const btnClick = () => {
if (energy != 0) {
const newEnergy = energy - 1;
const newFill = (maxEnergy - newEnergy) / maxEnergy * 100;
const newCoins = coins + 1;
dispatch<any>(updateEnergyRequestAsync(newEnergy));
setCoins(newCoins);
setEnergy(newEnergy)
setFill(newFill);
if (newFill < 100) {
setSize(220);
const timer = setTimeout(() => {
setSize(240);
clearTimeout(timer);
}, 100);
} else {
setClose(false);
}
//sendClick();
} else {
setClose(false);
}
};
/*const sendClick = () => {
if (token && !loading) {
setLoading(true);
axios.post(`${urlClick}/api/v1/click/`,
{},
{
headers: {
"Authorization": `TelegramToken ${token}`
}
}
).then((resp) => {
//console.log(resp);
if(resp.data) {
setLoading(false);
const click = Number(resp.data.click.value);
//
const encodeMult = btoa(click.toString());
sessionStorage.setItem('mt', encodeMult);
//
const newEnergy = Number(resp.data.energy);
setMult(Number(click.toFixed(2)))
dispatch<any>(saveMult(Number(click.toFixed(2))));
const newFill = (maxEnergy - newEnergy) / maxEnergy * 100;
if (newFill <= 100) {
const newCoins = Number(coins + click);
dispatch<any>(updateEnergyRequestAsync(newEnergy))
setCoins(newCoins);
setEnergy(newEnergy)
setFill(newFill);
} else {
setFill(100);
}
if (newFill < 100) {
setSize(220);
const timer = setTimeout(() => {
setSize(240);
clearTimeout(timer);
}, 100);
} else {
setClose(false);
}
//
}
if(error) {
setError(false)
}
}).catch((err) => {
setLoading(false);
setCloseError(false);
setError(true);
console.log(err);
})
}
};*/
const hotCards = [
{
title: 'Ты большой молодец',
text: <span>Твоя концентрация на&nbsp;высоте, ты&nbsp;перегрел кнопку на&nbsp;15% сильнее остальных игроков, на&nbsp;столько&nbsp;же уголь Max Flow дает жар сильнее, чем остальные.</span>,
img: 'assets/Biceps.png'
},
{
title: 'Как продолжить кликать',
text: <span>Чтобы охладиться, нужно закрыть приложение, нажав по&nbsp;кнопке ниже.</span>,
img: 'assets/Monocle.png'
},
{
title: 'Что дальше',
text: <span>Затем ты&nbsp;сможешь открыть приложение заново и&nbsp;продолжить поддерживать свою большую концентрацию.</span>,
img: 'assets/Rocket.png'
},
];
return (
<div className={styles.ringContainer}>
<div className={`${styles.ringBig} ${fill === 100 && styles.borderNone}`}></div>
<div className={`${styles.ringSmall} ${fill === 100 && styles.borderNone}`} style={{ backgroundImage: `url(${img})`, backgroundSize: `${size}px` }} onClick={btnClick}></div>
<svg className={styles.svg} width="270" height="270" viewBox="0 0 270 270" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="135" cy="135" r="125" stroke={fill < 100 ? "url(#paint0_linear_619_10702)" : '#FF0000'} strokeWidth="20" strokeLinecap="round" style={{ strokeDasharray: `${dashArray} ${circumference}` }} />
<defs>
{gradient}
</defs>
</svg>
{!close && <ModalWindow setCloseAnimOut={setClose} removeBtn={true} setClose={setClose} modalBlock={
<ClickerPopup title='Кнопка перегрелась' cards={hotCards} setClose={setClose} isBtn={true}/>
} />}
</div>
);
}

View File

@ -0,0 +1,86 @@
.ringContainer {
position: relative;
width: 270px;
height: 270px;
user-select: none;
}
.ringContainer::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-radius: 50%;
backdrop-filter: blur(25px);
}
.ringBig {
user-select: none;
position: absolute;
top: 0;
left: 0;
width: 270px;
height: 270px;
border: 2px solid var(--white);
border-radius: 50%;
}
.ringSmall {
user-select: none;
z-index: 1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 230px;
height: 230px;
border: 2px solid var(--white);
border-radius: 50%;
background-position: center;
background-repeat: no-repeat;
}
.fill {
z-index: -1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 267px;
height: 267px;
border-radius: 50%;
background: conic-gradient(red 30%, transparent 0);
border-right-color: transparent;
}
.svg {
z-index: -1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.borderNone {
border: none;
}
.loadingContainer {
user-select: none;
z-index: 1;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--grey27);
width: 230px;
height: 230px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 2px solid var(--white);
}

View File

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

View File

@ -0,0 +1,21 @@
import React from 'react';
import styles from './clickerbtnfooter.module.css';
import { ETextStyles } from '../../texts';
import { EIcons, Icon } from '../../Icons';
interface IClickerBtnFooter {
text: string,
className ?: string,
onClick(): void
}
export function ClickerBtnFooter({ text, className, onClick }: IClickerBtnFooter) {
return (
<div className={`${styles.container} ${className}`} onClick={onClick}>
<div className={styles.content}>
<div className={styles.icon}>{text === 'Аукцион' ? <Icon icon={EIcons.BitCoinIcon} /> :<Icon icon={EIcons.StyleIcon}/>}</div>
<p style={ETextStyles.RwSb14120} className={styles.text}>{text}</p>
</div>
</div>
);
}

View File

@ -0,0 +1,31 @@
.container {
display: flex;
padding: 12px;
align-items: center;
justify-content: center;
/*background-color: var(--grey1B);
background-color: rgba(0, 0, 0, 0.6);*/
background: rgba(0, 0, 0, 0.6);
border-radius: 12px;
cursor: pointer;
}
.content {
display: flex;
align-items: center;
gap: 4px;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--grey34);
width: 14px;
height: 14px;
border-radius: 3px;
}
.text {
color: var(--greyA4);
}

View File

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

View File

@ -0,0 +1,20 @@
import React, { useState } from 'react';
import styles from './clickerfooter.module.css';
import { ClickerBtnFooter } from '../ClickerBtnFooter';
import { EIcons, Icon } from '../../Icons';
import { useNavigate } from 'react-router-dom';
export function ClickerFooter() {
const navigate = useNavigate();
const isDev = true;
return (
<div className={styles.container}>
<ClickerBtnFooter text='Стили' className={styles.btn} onClick={() => navigate('/styles')}/>
<ClickerBtnFooter text='Аукцион' className={styles.btn} onClick={() => { !isDev ? navigate('/auction') : navigate('/dev?type=auction') }}/>
{ !isDev && <div className={styles.fire}>
<Icon icon={EIcons.FireIcon}/>
</div>}
</div>
);
}

View File

@ -0,0 +1,33 @@
.container {
z-index: 10;
position: fixed;
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
bottom: 10px;
left: 10px;
width: calc(100% - 20px);
border-radius: 24px;
/*background-color: var(--grey35);*/
backdrop-filter: blur(25px);
background-color: rgba(255, 255, 255, 0.1);
}
.btn {
width: 50%;
}
.fire {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
background-color: var(--grey46);
transform: rotate(10deg);
}

View File

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

View File

@ -0,0 +1,42 @@
import React from 'react';
import styles from './clickerpopup.module.css';
import { ETextStyles } from '../../texts';
import { PopupCard } from '../../Elements/PopupCard';
import { generateRandomString } from '../../../utils/generateRandom';
import { Button } from '../../Button';
interface ICardInterface {
title: string,
text: string | React.ReactNode,
img: string
}
interface IClickerPopup {
title: string,
cards: Array<ICardInterface>,
setClose(a: boolean): void,
text ?: string,
isBtn?: boolean
}
export function ClickerPopup({ title, cards, text, isBtn=false }: IClickerPopup) {
const blockCards = cards.map((item) => {
return <PopupCard key={generateRandomString()} title={item.title} text={item.text} img={item.img}/>
});
return (
<div>
<h2 style={ETextStyles.RwSb24100} className={styles.title}>{title}</h2>
<div className={styles.cards}>{blockCards}</div>
{text && <p className={styles.text}>{text}</p> }
{isBtn && <Button text='Закрыть' onClick={() => {
//@ts-ignore
if (window.Telegram) {
//@ts-ignore
window.Telegram.WebApp.close()
}
}}/>}
</div>
);
}

View File

@ -0,0 +1,16 @@
.title {
margin-bottom: 24px;
width: calc(100% - 44px);
}
.cards {
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 8px;
}
.text {
margin-top: -24px;
margin-bottom: 30px;
}

View File

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

View File

@ -0,0 +1,105 @@
import React, { useEffect, useState } from 'react';
import styles from './pointszoom.module.css';
import { formatNumber } from '../../../utils/formatNumber';
import { ETextStyles } from '../../texts';
import ReactDOM from 'react-dom';
import { useDispatch } from 'react-redux';
import { meRequest, meRequestError, updateEnergyRequestAsync, updatePointsRequestAsync } from '../../../store/me/actions';
import { checkIOS } from '../../../utils/checkMobile';
import axios from 'axios';
import { useAppSelector } from '../../hooks/useAppSelector';
import { saveMult } from '../../../store/mult';
interface IPointsZoom {
points: number,
setClose(a:boolean): void,
className ?: string,
closePointsAnim: boolean,
setClosePointsAnim(a: boolean): void,
setCoins(a:number):void,
setCloseError(a: boolean): void,
setEnergy(a: number): void,
setMult(a: number): void,
}
export function PointsZoom({ points, setMult, setClose, setCoins, className, closePointsAnim, setClosePointsAnim, setCloseError, setEnergy }: IPointsZoom) {
const [open, setOpen] = useState(true);
const node = document.querySelector('#modal_root');
const urlClick = useAppSelector<string>(state => state.urlClick);
const token = useAppSelector<string>(state => state.token);
const [sizeHand, setSizeHand] = useState(30);
const energy = Number(useAppSelector<string | undefined>(state=>state.me.data.energy));
if (!node) return null;
const dispatch = useDispatch();
const sendClicks = () => {
const initPoints = points;
dispatch(meRequest());
axios.post(`${urlClick}/api/v1/batch-click/`,
{
count: initPoints
},
{
headers: {
"Authorization": `TelegramToken ${token}`
}
}
).then(resp => {
const data = resp.data;
const click = Number(data.click.value);
const encodeMult = btoa(click.toString());
sessionStorage.setItem('mt', encodeMult);
setMult(Number(click.toFixed(2)));
const energy = Number(data.energy);
dispatch<any>(saveMult(Number(click.toFixed(2))));
dispatch<any>(updateEnergyRequestAsync(energy));
dispatch<any>(updatePointsRequestAsync());
}).catch(err => {
console.log(err);
setCloseError(false);
const returnEnergy = energy + initPoints;
setEnergy(returnEnergy);
dispatch<any>(updateEnergyRequestAsync(returnEnergy));
dispatch(meRequestError(String(err)));
})
};
useEffect(() => {
const timer = setInterval(() => {
setOpen(false);
clearInterval(timer);
}, 400);
}, []);
useEffect(() => {
if (closePointsAnim) {
sendClicks();
const timer = setTimeout(() => {
setClosePointsAnim(false);
setClose(true);
clearTimeout(timer);
setCoins(0);
}, 400);
}
}, [closePointsAnim]);
useEffect(() => {
setSizeHand(25);
const timer = setTimeout(() => {
setSizeHand(30);
}, 100);
return () => clearTimeout(timer);
}, [points]);
return (
<div className={`${styles.container} ${className} ${open ? styles.animBack : ''} ${closePointsAnim ? styles.animBackClose : ''}`}>
{ReactDOM.createPortal((
<div className={`${styles.innerContainer} ${open ? styles.animBlock : ''} ${closePointsAnim ? styles.animBlockClose : ''} ${checkIOS() && styles.ios}`}>
<div className={styles.icon} style={{ backgroundImage: `url('assets/point.png')`, width: `${sizeHand}px`, height: `${sizeHand}px` }}></div>
<p className={styles.point} style={ETextStyles.InSb18100}>{`${formatNumber(Number(points.toFixed(2)))}`}</p>
</div>
), node)}
</div>
);
}

View File

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

View File

@ -0,0 +1,105 @@
.container {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
background: rgba(14, 14, 14, 0.01);
backdrop-filter: blur(2px);
}
.innerContainer {
position: fixed;
top: 20px;
left: 10px;
z-index: 50;
width: calc(100% - 20px);
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 12px;
border-radius: 20px;
background-color: var(--grey1F);
box-shadow: 0px 0px 30px 15px rgba(128, 135, 192, 0.25);
}
.ios {
top: 40px !important;
}
.icon {
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.animBack {
animation-name: animBack;
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-timing-function: ease-in;
}
.animBlock {
animation-name: animBlock;
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-timing-function: ease-in;
}
@keyframes animBack {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes animBlock {
0% {
width: 64px;
}
100% {
width: 100%;
}
}
.animBackClose {
animation-name: animBackClose;
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-timing-function: ease-out;
}
.animBlockClose {
animation-name: animBlockClose;
animation-duration: 0.4s;
animation-iteration-count: 1;
animation-timing-function: ease-out;
}
@keyframes animBackClose {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes animBlockClose {
0% {
width: 100%;
}
100% {
width: 64px;
}
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import styles from './profile.module.css';
import { ETextStyles } from '../../texts';
import { formatNumber } from '../../../utils/formatNumber';
import { PersonIcon } from '../../Elements/PersonIcon';
import { EIcons, Icon } from '../../Icons';
import { useAppSelector } from '../../hooks/useAppSelector';
import { Spinner } from '../../Elements/Spinner';
interface IProfileClicker {
name: string,
img: string,
className ?: string
}
export function Profile({ name, img, className }: IProfileClicker) {
const points = useAppSelector<string | undefined>(state => state.me.data.points);
const loading = useAppSelector<boolean>(state => state.me.loading);
return (
<div className={`${styles.container} ${className}`}>
{img ? <PersonIcon img={`${img}`} size={30} /> : <Icon icon={EIcons.ProfileIcon} /> }
<div className={styles.content}>
<p style={ETextStyles.RwSb12120} className={styles.name}>{name}</p>
<div className={styles.pointsContainer}>
{points && <p className={styles.points} style={ETextStyles.InSb10120}>
{formatNumber(Number(Number(points).toFixed(2)))}
</p>}
{!loading && <div className={styles.icon} style={{ backgroundImage: "url('assets/btnIcon.png')"}}></div>}
{loading && <Spinner size='14px' color='#FFFFFF' thickness='2px' />}
</div>
</div>
</div>
);
}

View File

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

View File

@ -0,0 +1,50 @@
.container {
display: flex;
align-items: center;
}
.img {
margin-right: 4px;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: var(--white);
}
.name {
margin-bottom: 2px;
}
.pointsContainer {
display: flex;
align-items: center;
}
.points {
margin-right: 3px;
-webkit-text-fill-color: transparent;
background-clip: text !important;
background: var(--gradientPrimary);
}
.icon {
width: 14px;
height: 14px;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
.content {
margin-left: 4px;
}
.emptyIcon {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
background-color: var(--grey12);
border-radius: 50%;
}

View File

@ -0,0 +1,89 @@
import React, { useEffect, useState } from 'react';
import styles from './sectionsblock.module.css';
import { CardSection } from '../../Elements/CardSection';
import { ETextStyles } from '../../texts';
import { PointsBlock } from '../../Elements/PointsBlock';
import { ModalWindow } from '../../ModalWindow';
import { ClickerPopup } from '../ClickerPopup';
import { useNavigate } from 'react-router-dom';
import { UsersIcons } from '../../Elements/UsersIcons';
import { formatNumber } from '../../../utils/formatNumber';
import { useAppSelector } from '../../hooks/useAppSelector';
interface ISectionsBlock {
mult:number;
}
export function SectionsBlock({ mult }: ISectionsBlock) {
const [close, setClose] = useState(true);
const navigate = useNavigate();
const referralStorage = Number(useAppSelector<string | undefined>(state => state.me.data.referralStorage));
//const referralStorage = 500;
const maxReferralStorage = useAppSelector<number>(state => state.me.data.maxStorage);
const [referralPercent, serReferralPercent] = useState(0);
useEffect(() => {
if(referralStorage >= maxReferralStorage) {
serReferralPercent(100);
} else {
serReferralPercent(referralStorage / maxReferralStorage * 100);
}
}, [referralStorage, maxReferralStorage]);
const isDev = true;
const multipCards = [
{
title: 'Что он делает',
text: <span>Увеличивает получение баллов с&nbsp;одного клика в&nbsp;столько раз, сколько указано в&nbsp;рамке.</span>,
img: 'assets/Rocket.png'
},
{
title: 'Как его увеличить',
text: <span>Чем выше концентрация&nbsp;&mdash; клики в&nbsp;час, тем выше множитель, он&nbsp;рассчитывается по&nbsp;формуле.</span>,
img: 'assets/Monocle.png'
},
{
title: 'Может ли он уменьшиться',
text: <span>Да, если снизится концентрация или долго не&nbsp;заходить в&nbsp;приложение и&nbsp;не&nbsp;совершать клики.</span>,
img: 'assets/Chain.png'
},
];
return (
<div className={styles.sectionContainer}>
<div className={styles.leftContainer}>
<CardSection title='Место в топе' onClick={() => {!isDev ? navigate('/rating') : navigate('/dev?type=rating')}}>
{<div className={`${styles.bottomRank} ${isDev ? styles.dev : ''}`}>
<div style={ETextStyles.InSb12120}>
<span className={styles.rank1}>#</span>
<span>{formatNumber(1)}</span>
</div>
<UsersIcons size={16}/>
</div>}
</CardSection>
<CardSection title='Множитель' onClick={() => { setClose(false) }}>
<p style={ETextStyles.InSb12120}>
<span style={{color: 'var(--primary)'}}>{'X '}</span>
{mult}
</p>
</CardSection>
</div>
<CardSection title='Реферальное хранилище' className={styles.rigthEl} onClick={() => { navigate('/referral') }}>
{<div>
<PointsBlock points={formatNumber(referralStorage.toFixed(2))} className={styles.scalePoints} />
<div className={styles.scaleContainer}>
<div className={`${styles.scale} ${referralPercent === 100 ? styles.scaleFull : ''}`} style={{ width: `${referralPercent}%` }}></div>
</div>
<p className={`${styles.scaleText} ${referralPercent === 100 ? styles.textFull : ''}`}>
{referralPercent === 100 ? 'Хранилище заполнено, заберите коины' : 'Когда хранилище заполнится, вы сможете забрать баллы'}
</p>
</div>}
</CardSection>
{!close && <ModalWindow setCloseAnimOut={setClose} setClose={setClose} modalBlock={
<ClickerPopup title='Что такое множитель' cards={multipCards} setClose={setClose}/>
} />}
</div>
);
}

View File

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

View File

@ -0,0 +1,81 @@
.sectionContainer {
display: flex;
gap: 8px;
align-items: stretch;
}
.leftContainer {
display: flex;
gap: 8px;
flex-direction: column;
width: 50% !important;
}
.rigthEl {
width: 50% !important;
}
.bottomRank {
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
}
.rank1 {
margin-right: 3px;
-webkit-text-fill-color: transparent;
background-clip: text !important;
background: var(--gradientPrimary);
}
.scalePoints {
margin-bottom: 4px;
}
.scaleContainer {
position: relative;
margin-bottom: 8px;
width: 100%;
height: 10px;
background-color: var(--grey6F);
border-radius: 20px;
overflow: hidden;
}
.scaleText {
font-size: 10px;
color: var(--grey6C);
}
.textFull {
color: var(--white);
}
.scale {
position: absolute;
top: 0;
left: 0;
height: 10px;
background: var(--primary);
}
.scaleFull {
background: var(--purple);
}
.dev {
position: relative;
}
.dev::after {
content: '';
position: absolute;
top: -12px;
left: -12px;
width: calc(100% + 24px);
height: calc(100% + 24px);
border-radius: 14px;
background: rgba(20, 20, 20, 0.80);
backdrop-filter: blur(5px);
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import styles from './styleelements.module.css';
interface IStyleElements {
styleIndex: number
}
export function StyleElements({ styleIndex }: IStyleElements) {
return (
<div>
<div className={`${styles.img} ${styles.img1}`} style={{ backgroundImage: `url("assets/style${styleIndex + 1}.png")`, width: `${styleIndex === 1 ? 73 : (styleIndex === 2 ? 48 : 82)}px`, height: `${styleIndex === 1 ? 68 : (styleIndex === 2 ? 86 : 127)}px`, top: `${styleIndex === 1 ? 2 : (styleIndex === 2 ? -5 : -20)}px`, right: `${styleIndex === 1 ? 10 : (styleIndex === 2 ? 20 : 10)}px`, transform: `rotate(${styleIndex === 1 ? 28 : (styleIndex === 2 ? 43 : 48)}deg)` }}></div>
<div className={`${styles.img} ${styles.img2}`} style={{ backgroundImage: `url("assets/style${styleIndex + 1}.png")`, width: `${styleIndex === 1 ? 73 : (styleIndex === 2 ? 78 : 98)}px`, height: `${styleIndex === 1 ? 68 : (styleIndex === 2 ? 139 : 152)}px`, top: `${styleIndex === 1 ? 255 : (styleIndex === 2 ? 180 : 174)}px`, right: `${styleIndex === 1 ? 0 : (styleIndex === 2 ? 3 : -10)}px`, transform: `rotate(${styleIndex === 1 ? 32 : (styleIndex === 2 ? 19 : -114)}deg)`, zIndex: `${styleIndex > 1 ? 2 : 1}` }}></div>
<div className={`${styles.img} ${styles.img3}`} style={{ backgroundImage: `url("assets/style${styleIndex + 1}.png")`, width: `${styleIndex === 1 ? 73 : (styleIndex === 2 ? 77 : 98)}px`, height: `${styleIndex === 1 ? 68 : (styleIndex === 2 ? 139 : 152)}px`, top: `${styleIndex === 1 ? 275 : (styleIndex === 2 ? 322 : 325)}px`, left: `${styleIndex === 1 ? -2 : (styleIndex === 2 ? -35 : -40)}px`, transform: `rotate(${styleIndex === 1 ? -36 : (styleIndex === 2 ? -24 : 65)}deg)` }}></div>
<div className={`${styles.img} ${styles.img4}`} style={{ backgroundImage: `url("assets/style${styleIndex + 1}.png")`, width: `${styleIndex === 1 ? 73 : (styleIndex === 2 ? 77 : 98)}px`, height: `${styleIndex === 1 ? 68 : (styleIndex === 2 ? 139 : 152)}px`, top: `${styleIndex === 1 ? 530 : (styleIndex === 2 ? 433 : 425)}px`, right: `${styleIndex === 1 ? 55 : (styleIndex === 2 ? -5 : 0)}px`, transform: `rotate(${styleIndex === 1 ? 82 : (styleIndex === 2 ? 19 : 0)}deg)` }}></div>
</div>
);
}

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