Initial commit: Tapalka monorepo (bot, front, strapi)

This commit is contained in:
2026-05-21 11:54:44 +07:00
commit 9ec9485940
208 changed files with 58314 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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