Initial commit: Tapalka monorepo (bot, front, strapi)
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal.isOpen {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #1A1A1C;
|
||||
padding: 24px 16px 0 16px;
|
||||
width: 100%;
|
||||
height: 85%;
|
||||
position: fixed;
|
||||
bottom: -85%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.modalContent.isOpen {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.modalContent.isClosing {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.closeButton::before,
|
||||
.closeButton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: #FFFFFF26;
|
||||
}
|
||||
|
||||
.closeButton::before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.closeButton::after {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.exchangeRate {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
background-color: green;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
margin-top: 11px;
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
line-height: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.selectAmount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.selectAmountTitle {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding-bottom: 0.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.inputRange {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.inputRange input[type='range'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 5px;
|
||||
background: #333;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.inputRange input[type='range']::-moz-range-progress {
|
||||
background-color: #17da17;
|
||||
}
|
||||
|
||||
.inputRange input[type='range']::-moz-range-track {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.inputRange input[type='range']::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #42BB47;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inputRange input[type='range']::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #42BB47;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.amount {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #FFFFFF0D;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rate {
|
||||
background: #FFFFFF0D;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.rate span {
|
||||
font-size: 24px;
|
||||
color: #BBBBBB;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.nickname input {
|
||||
background: #FFFFFF0D;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.nickname input::placeholder {
|
||||
font-weight: 500;
|
||||
color: #BBBBBB;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.nicknameTitle {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.selectedAmount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
width: 100%;
|
||||
background: #42BB47;
|
||||
color: white;
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
|
||||
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||
import ModalExchangeContent from '@/components/ui/modal-exchange/ModalExchangeContent';
|
||||
import { useModalStore } from '@/store/modal.state';
|
||||
|
||||
interface IModalExchange {
|
||||
isOpen: boolean;
|
||||
isClosing: boolean;
|
||||
amount: number;
|
||||
balance: number | undefined;
|
||||
nickname: string;
|
||||
rangeRef: RefObject<HTMLInputElement>;
|
||||
setNickname: Dispatch<SetStateAction<string>>;
|
||||
setAmount: Dispatch<SetStateAction<number>>;
|
||||
onClose: () => void;
|
||||
handleCreateExchange: (amount: number, total: number, nickname: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const ModalExchange: FC<IModalExchange> = ({
|
||||
isOpen,
|
||||
isClosing,
|
||||
amount,
|
||||
balance,
|
||||
nickname,
|
||||
rangeRef,
|
||||
onClose,
|
||||
setNickname,
|
||||
setAmount,
|
||||
handleCreateExchange,
|
||||
}) => {
|
||||
const { priceItem, image, isDecimal } = useModalStore();
|
||||
return (
|
||||
<div
|
||||
className={`${styles.modal} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
|
||||
>
|
||||
<div
|
||||
className={`${styles.modalContent} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
|
||||
>
|
||||
<div className={styles.closeButton} onClick={onClose} />
|
||||
<div className={styles.modalTitle}>
|
||||
<span>Обмен</span>
|
||||
</div>
|
||||
<ModalExchangeContent
|
||||
isDecimal={isDecimal}
|
||||
balance={balance}
|
||||
nickname={nickname}
|
||||
amount={amount}
|
||||
image={image}
|
||||
priceItem={priceItem}
|
||||
rangeRef={rangeRef}
|
||||
setNickname={setNickname}
|
||||
setAmount={setAmount}
|
||||
/>
|
||||
<button
|
||||
className={styles.submitButton}
|
||||
onClick={() => handleCreateExchange(amount, amount / +priceItem, nickname)}
|
||||
>
|
||||
Вывод
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalExchange;
|
||||
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ModalExchange from '@/components/ui/modal-exchange/ModalExchange';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { useConfigStore } from '@/store/config.state';
|
||||
import { useNotify } from '@/hooks/useNotify';
|
||||
import { ExchangeRequestApi } from '@/api/exchange-request.api';
|
||||
import { useModalStore } from '@/store/modal.state';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
interface IModalExchangeContainer {
|
||||
isOpen: boolean;
|
||||
balance: number | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ModalExchangeContainer: FC<IModalExchangeContainer> = ({ balance, isOpen, onClose }) => {
|
||||
const { token } = useTokenStore();
|
||||
|
||||
const [amount, setAmount] = useState<number>(1);
|
||||
const [nickname, setNickname] = useState<string>('');
|
||||
const [isClosing, setIsClosing] = useState<boolean>(false);
|
||||
|
||||
const rangeRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { player, setPlayer } = usePlayerStore();
|
||||
const { config } = useConfigStore();
|
||||
const { shopItemId, isDecimal, image, setShopItemId, setIsDecimal } = useModalStore();
|
||||
|
||||
const { getSuccessNotify, getErrorNotifyByCount, getErrorNotifyByNickname } = useNotify();
|
||||
|
||||
useEffect(() => {
|
||||
if (rangeRef.current) {
|
||||
rangeRef.current.style.setProperty('--value', amount.toString());
|
||||
}
|
||||
}, [amount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsClosing(false);
|
||||
setAmount(0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleCreateExchange = useCallback(
|
||||
async (amount: number, total: number, nickname: string) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const exchangeRequestApi = ExchangeRequestApi.getInstance(token);
|
||||
if (player && config) {
|
||||
if (nickname.length === 0) {
|
||||
getErrorNotifyByNickname();
|
||||
} else if (total === 0) {
|
||||
getErrorNotifyByCount();
|
||||
} else {
|
||||
exchangeRequestApi
|
||||
.createExchange({
|
||||
amount: amount,
|
||||
total: total,
|
||||
playerId: player.id,
|
||||
nickname: nickname,
|
||||
balance: player.attributes.balance,
|
||||
telegram_id: config.attributes.admins.data.map(
|
||||
user => user.attributes.telegram_id,
|
||||
),
|
||||
shop_item: shopItemId || undefined,
|
||||
archive_request_id: player!.attributes.archive_request!.data?.id || -1,
|
||||
})
|
||||
.then(() => {
|
||||
if (player) {
|
||||
setPlayer({
|
||||
...player,
|
||||
attributes: {
|
||||
...player.attributes,
|
||||
balance: toFixed(player?.attributes.balance) - amount,
|
||||
},
|
||||
});
|
||||
setShopItemId(null);
|
||||
setIsDecimal(true);
|
||||
}
|
||||
});
|
||||
getSuccessNotify();
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
[player, token, shopItemId],
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalExchange
|
||||
isOpen={isOpen}
|
||||
isClosing={isClosing}
|
||||
balance={balance}
|
||||
amount={amount}
|
||||
nickname={nickname}
|
||||
rangeRef={rangeRef}
|
||||
onClose={handleClose}
|
||||
setNickname={setNickname}
|
||||
setAmount={setAmount}
|
||||
handleCreateExchange={handleCreateExchange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalExchangeContainer;
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
|
||||
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||
import Image from 'next/image';
|
||||
import NicknameInput from '@/components/ui/modal-exchange/inputs/NicknameInput';
|
||||
import RangeInput from '@/components/ui/modal-exchange/inputs/RangeInput';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
interface IModalExchangeContent {
|
||||
isDecimal: boolean;
|
||||
balance: number | undefined;
|
||||
nickname: string;
|
||||
amount: number;
|
||||
priceItem: number;
|
||||
image: string;
|
||||
rangeRef: RefObject<HTMLInputElement>;
|
||||
setNickname: Dispatch<SetStateAction<string>>;
|
||||
setAmount: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const ModalExchangeContent: FC<IModalExchangeContent> = ({
|
||||
isDecimal,
|
||||
balance,
|
||||
nickname,
|
||||
amount,
|
||||
priceItem,
|
||||
image,
|
||||
rangeRef,
|
||||
setNickname,
|
||||
setAmount,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.selectAmount}>
|
||||
<span className={styles.selectAmountTitle}>Выберите количество</span>
|
||||
<div className={styles.amount}>
|
||||
<div className={styles.selectedAmount}>
|
||||
<span>{`${amount.toFixed(isDecimal ? 3 : 0)} - ${toFixed(balance)}`}</span>
|
||||
<Image
|
||||
src={'/icons/small-robucks-icon.svg'}
|
||||
alt={'small robucks icon'}
|
||||
height={18}
|
||||
width={18}
|
||||
/>
|
||||
</div>
|
||||
<RangeInput
|
||||
isDecimal={isDecimal}
|
||||
balance={balance}
|
||||
amount={amount}
|
||||
priceItem={priceItem}
|
||||
rangeRef={rangeRef}
|
||||
setAmount={setAmount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src={'/icons/exchange-icon.svg'}
|
||||
alt={'exchange icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rate}>
|
||||
{priceItem && (
|
||||
<span>{isDecimal ? (amount / priceItem).toFixed(3) : amount / priceItem}</span>
|
||||
)}
|
||||
<Image src={image} alt={'small coin icon'} height={24} width={24} />
|
||||
</div>
|
||||
<NicknameInput nickname={nickname} setNickname={setNickname} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalExchangeContent;
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import React, { Dispatch, FC, SetStateAction, useState } from 'react';
|
||||
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||
|
||||
interface INicknameInput {
|
||||
nickname: string;
|
||||
setNickname: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const NicknameInput: FC<INicknameInput> = ({ nickname, setNickname }) => {
|
||||
const handleChangeNickname = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNickname(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.nickname}>
|
||||
<span className={styles.nicknameTitle}>Укажите ваш профиль в Roblox</span>
|
||||
<input
|
||||
placeholder={'Ник в Roblox'}
|
||||
type={'text'}
|
||||
value={nickname}
|
||||
onChange={handleChangeNickname}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NicknameInput;
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
import { Dispatch, FC, SetStateAction } from 'react';
|
||||
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||
|
||||
interface IRangeInput {
|
||||
isDecimal: boolean;
|
||||
balance: number | undefined;
|
||||
amount: number;
|
||||
priceItem: number;
|
||||
rangeRef: React.RefObject<HTMLInputElement>;
|
||||
setAmount: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
const RangeInput: FC<IRangeInput> = ({
|
||||
isDecimal,
|
||||
balance,
|
||||
priceItem,
|
||||
amount,
|
||||
rangeRef,
|
||||
setAmount,
|
||||
}) => {
|
||||
const handleRangeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = parseFloat(e.target.value);
|
||||
if (!isDecimal) {
|
||||
value = Math.round(value / priceItem) * priceItem;
|
||||
} else {
|
||||
value = parseFloat(value.toFixed(6));
|
||||
}
|
||||
setAmount(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.inputRange}>
|
||||
<input
|
||||
type='range'
|
||||
id='amountRange'
|
||||
min='0'
|
||||
max={balance}
|
||||
step={isDecimal ? '0.000001' : priceItem.toString()}
|
||||
value={amount}
|
||||
onChange={handleRangeChange}
|
||||
ref={rangeRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RangeInput;
|
||||
Reference in New Issue
Block a user