Initial commit: Tapalka monorepo (bot, front, strapi)
This commit is contained in:
34
CMyTapper/robucks-front/components/friends/Friend.module.css
Normal file
34
CMyTapper/robucks-front/components/friends/Friend.module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.friend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #FFFFFF0D;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
.friendContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.friendData {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.friendReferrals {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.friendBalance span {
|
||||
font-family: var(--font-manrope);
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
color: white;
|
||||
}
|
||||
43
CMyTapper/robucks-front/components/friends/Friend.tsx
Normal file
43
CMyTapper/robucks-front/components/friends/Friend.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
import styles from '@/components/friends/Friend.module.css';
|
||||
import Image from 'next/image';
|
||||
import { IPlayer } from '@/interfaces/player.type';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
type Props = {
|
||||
player: IPlayer;
|
||||
};
|
||||
|
||||
const Friend = ({ player }: Props) => {
|
||||
return (
|
||||
<div className={styles.friend}>
|
||||
<div className={styles.friendContent}>
|
||||
<div className={styles.friendAvatar}>
|
||||
<Image
|
||||
src={'/icons/friend-icon.svg'}
|
||||
alt={'friend avatar'}
|
||||
height={42}
|
||||
width={42}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.friendData}>
|
||||
<span>{player?.attributes.telegram_nick}</span>
|
||||
<div className={styles.friendReferrals}>
|
||||
<Image
|
||||
src={'/icons/referrals-icon.svg'}
|
||||
alt={'referrals icon'}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
<span>{player?.attributes.my_referrals?.data?.length || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.friendBalance}>
|
||||
<span>{toFixed(player?.attributes.balance)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Friend;
|
||||
113
CMyTapper/robucks-front/components/friends/Friends.module.css
Normal file
113
CMyTapper/robucks-front/components/friends/Friends.module.css
Normal file
@@ -0,0 +1,113 @@
|
||||
.friends {
|
||||
background: #101010;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.friendsTitle {
|
||||
align-items: flex-start;
|
||||
padding-top: 45px;
|
||||
padding-left: 1.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
color: white;
|
||||
line-height: 21px;
|
||||
font-family: var(--font-manrope);
|
||||
}
|
||||
|
||||
.inviteBlock {
|
||||
width: min(343px, 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 1rem;
|
||||
padding: 20px 16px;
|
||||
border-radius: 1.5rem;
|
||||
background: linear-gradient(81.19deg, rgba(66, 187, 71, 0.06) 0%, rgba(66, 187, 71, 0.2) 100%), #0E0E10;
|
||||
border: 1px solid #18181A;
|
||||
}
|
||||
|
||||
.rewardInvite {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
gap: 3px;
|
||||
font-family: var(--font-clash);
|
||||
}
|
||||
|
||||
.rewardInvite span {
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
line-height: 51px;
|
||||
background: linear-gradient(180deg, #42BB47 4.41%, #33A137 61.59%, #42BB47 101.32%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.inviteButtonBlock {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inviteButtonBlock button {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
background: #42BB47;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
font-family: var(--font-manrope);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infoInvite {
|
||||
text-align: center;
|
||||
font-family: var(--font-manrope);
|
||||
}
|
||||
|
||||
.infoInvite span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
color: #BBBBBB;
|
||||
}
|
||||
|
||||
.friendsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 1rem;
|
||||
margin-top: 7px;
|
||||
gap: 5px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.friendsListBlock {
|
||||
width: min(375px, 100%);
|
||||
margin-top: 2rem;
|
||||
background: #0E0E10;
|
||||
padding: 20px 16px;
|
||||
border: 1px solid #18181A;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.friendsListHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 19px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-manrope);
|
||||
line-height: 19px;
|
||||
}
|
||||
67
CMyTapper/robucks-front/components/friends/Friends.tsx
Normal file
67
CMyTapper/robucks-front/components/friends/Friends.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
import styles from '@/components/friends/Friends.module.css';
|
||||
import Image from 'next/image';
|
||||
import { FC } from 'react';
|
||||
import Friend from '@/components/friends/Friend';
|
||||
import CopyLinkToast from '@/components/ui/copy-link-toast/CopyLinkToast';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
|
||||
interface IFriends {
|
||||
isVisible: boolean;
|
||||
referral_fee: number | undefined;
|
||||
friendsAndReferralsCount: { friendsWord: string; referralWord: string };
|
||||
handleCopyInviteLink: () => Promise<void>;
|
||||
handleCloseToastLink: () => void;
|
||||
}
|
||||
|
||||
const Friends: FC<IFriends> = ({
|
||||
isVisible,
|
||||
referral_fee,
|
||||
friendsAndReferralsCount,
|
||||
handleCopyInviteLink,
|
||||
handleCloseToastLink,
|
||||
}) => {
|
||||
const { player } = usePlayerStore();
|
||||
const referrals = player?.attributes.my_referrals.data;
|
||||
|
||||
return (
|
||||
<div className={styles.friends}>
|
||||
<div className={styles.friendsTitle}>
|
||||
<span>Друзья</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.inviteBlock}>
|
||||
<div className={styles.rewardInvite}>
|
||||
<span>{referral_fee || 0}</span>
|
||||
<Image
|
||||
src={'/icons/robucks-icon.svg'}
|
||||
alt={'robucks icon'}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inviteButtonBlock}>
|
||||
<button onClick={handleCopyInviteLink}>Пригласи друга</button>
|
||||
</div>
|
||||
<div className={styles.infoInvite}>
|
||||
<span>
|
||||
Получай 10% от твоих друзей +5% от друзей реферала +2.5% от их рефералов
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.friendsListBlock}>
|
||||
<div className={styles.friendsListHeader}>
|
||||
<span>{friendsAndReferralsCount.friendsWord}</span>
|
||||
<span>{friendsAndReferralsCount.referralWord}</span>
|
||||
</div>
|
||||
<div className={styles.friendsList}>
|
||||
{referrals?.map(referral => <Friend key={referral.id} player={referral} />)}
|
||||
</div>
|
||||
</div>
|
||||
<CopyLinkToast isVisible={isVisible} handleCloseToastLink={handleCloseToastLink} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Friends;
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import Friends from '@/components/friends/Friends';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CalculateReferralsCount } from '@/utils/CalculateReferralsCount';
|
||||
import { updateFriendAndReferralCounts } from '@/utils/UpdateFriendAndReferralCount';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
|
||||
const FriendsContainer = ({ botLink }: { botLink: string }) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
const [userData, setUserData] = useState<{ id: number; username: string } | null>(null);
|
||||
|
||||
const { player } = usePlayerStore();
|
||||
|
||||
useEffect(() => {
|
||||
const initDataUnsafe = window.Telegram?.WebApp?.initDataUnsafe;
|
||||
if (!initDataUnsafe?.user) {
|
||||
return;
|
||||
}
|
||||
setUserData(initDataUnsafe.user);
|
||||
}, []);
|
||||
|
||||
const handleCopyInviteLink = useCallback(async () => {
|
||||
setIsVisible(true);
|
||||
setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 3000);
|
||||
return await navigator.clipboard.writeText(`${botLink}ref_${userData?.id}`);
|
||||
}, [userData, botLink]);
|
||||
|
||||
const handleCloseToastLink = useCallback(() => {
|
||||
setIsVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Friends
|
||||
isVisible={isVisible}
|
||||
referral_fee={player?.attributes.referral_fee}
|
||||
friendsAndReferralsCount={updateFriendAndReferralCounts(
|
||||
player?.attributes.my_referrals?.data?.length || 0,
|
||||
CalculateReferralsCount(player),
|
||||
)}
|
||||
handleCopyInviteLink={handleCopyInviteLink}
|
||||
handleCloseToastLink={handleCloseToastLink}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FriendsContainer;
|
||||
137
CMyTapper/robucks-front/components/home/Home.module.css
Normal file
137
CMyTapper/robucks-front/components/home/Home.module.css
Normal file
@@ -0,0 +1,137 @@
|
||||
.home {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-image: url("../../public/images/background-img.jpg");
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 3rem;
|
||||
height: 100%;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 34.4px;
|
||||
}
|
||||
|
||||
.greeting span {
|
||||
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.balance {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.balanceAmount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.totalBalance {
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
line-height: 51px;
|
||||
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.miningButton {
|
||||
margin-bottom: 75px;
|
||||
width: 187px;
|
||||
height: 60px;
|
||||
padding: 20px 30px;
|
||||
background: #42BB47;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
line-height: 19.6px;
|
||||
box-shadow: 0 0 42px 0 #42BB47B2;
|
||||
border-radius: 30px;
|
||||
border: 0.5px solid;
|
||||
cursor: pointer;
|
||||
border-image-source: linear-gradient(180deg, #42BB47 4.41%, #308A34 61.59%, #42BB47 101.32%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.farmingButton {
|
||||
margin-bottom: 75px;
|
||||
width: 300px;
|
||||
height: 60px;
|
||||
padding: 20px 30px;
|
||||
background: black;
|
||||
box-shadow: 0px 0px 42px 0px #42BB4780;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
border-radius: 30px;
|
||||
border: 1px solid #42BB4780;
|
||||
color: #42BB4780;
|
||||
font-weight: 600;
|
||||
line-height: 19.6px;
|
||||
}
|
||||
|
||||
.claimReward {
|
||||
box-shadow: 0px 0px 42px 0px #42BB4780;
|
||||
width: 257px;
|
||||
height: 60px;
|
||||
padding: 20px 30px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 75px;
|
||||
border-radius: 30px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
line-height: 19.6px;
|
||||
background: #10101080;
|
||||
border: 1px solid #42BB47;
|
||||
color: #42BB4780;
|
||||
}
|
||||
|
||||
.claimAmount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.claimAmount span {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #42BB47;
|
||||
}
|
||||
|
||||
.reward {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
gap: 10px;
|
||||
color: #42BB47;
|
||||
}
|
||||
|
||||
.reward span {
|
||||
color: #42BB47;
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
background: #10101080;
|
||||
color: #42BB47;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.miningButton,
|
||||
.farmingButton,
|
||||
.claimReward {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
62
CMyTapper/robucks-front/components/home/Home.tsx
Normal file
62
CMyTapper/robucks-front/components/home/Home.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
import React, { FC } from 'react';
|
||||
import styles from './Home.module.css';
|
||||
import Image from 'next/image';
|
||||
import { IConfig } from '@/api/config.api';
|
||||
import ButtonMining from '@/components/ui/button-mining/ButtonMining';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
interface IHome {
|
||||
remainingTime: string | null;
|
||||
config: IConfig | null;
|
||||
isLoading: boolean;
|
||||
typeButton: 'pending' | 'farming' | 'claim_reward';
|
||||
handleStartFarming: () => void;
|
||||
handleClaimRewards: () => void;
|
||||
}
|
||||
|
||||
const Home: FC<IHome> = ({
|
||||
remainingTime,
|
||||
config,
|
||||
isLoading,
|
||||
typeButton,
|
||||
handleStartFarming,
|
||||
handleClaimRewards,
|
||||
}) => {
|
||||
const { player } = usePlayerStore();
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.greeting}>
|
||||
<p>Привет, </p>
|
||||
<span>{player ? player.attributes.telegram_nick : '-'}!</span>
|
||||
</div>
|
||||
<div className={styles.balance}>
|
||||
<span>Ваш баланс</span>
|
||||
<div className={styles.balanceAmount}>
|
||||
<Image
|
||||
src={'/icons/robucks-icon.svg'}
|
||||
alt={'robucks icon'}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span className={styles.totalBalance}>
|
||||
{player ? toFixed(player.attributes.balance) : '0'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonMining
|
||||
isLoading={isLoading}
|
||||
typeButton={typeButton}
|
||||
remainingTime={remainingTime}
|
||||
farm_amount={config?.attributes.farm_amount}
|
||||
handleStartFarming={handleStartFarming}
|
||||
handleClaimRewards={handleClaimRewards}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
93
CMyTapper/robucks-front/components/home/HomeContainer.tsx
Normal file
93
CMyTapper/robucks-front/components/home/HomeContainer.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Home from '@/components/home/Home';
|
||||
import { FormatTime } from '@/utils/FormatTime';
|
||||
import { useButtonStore } from '@/store/button.state';
|
||||
import { PlayerApi } from '@/api/player.api';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { useConfigStore } from '@/store/config.state';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
const HomeContainer = () => {
|
||||
const { typeButton, setTypeButton } = useButtonStore();
|
||||
|
||||
const { player: playerStore, setPlayer } = usePlayerStore();
|
||||
const { config: configStore } = useConfigStore();
|
||||
const { token } = useTokenStore();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [remainingTime, setRemainingTime] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerStore && playerStore.attributes.start_farm && configStore) {
|
||||
const farmingTime = configStore.attributes.farming_time * 1000;
|
||||
const endTime = new Date(playerStore.attributes.start_farm).getTime() + farmingTime;
|
||||
|
||||
const updateRemainingTime = () => {
|
||||
const now = new Date().getTime();
|
||||
const timeLeft = endTime - now;
|
||||
setRemainingTime(timeLeft > 0 ? timeLeft : 0);
|
||||
};
|
||||
|
||||
updateRemainingTime();
|
||||
const interval = setInterval(updateRemainingTime, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
setRemainingTime(null);
|
||||
}
|
||||
}, [playerStore, configStore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!remainingTime && !playerStore?.attributes.start_farm) {
|
||||
setTypeButton('pending');
|
||||
} else if (remainingTime && playerStore?.attributes.start_farm) {
|
||||
setTypeButton('farming');
|
||||
} else if (!remainingTime && playerStore?.attributes.start_farm) {
|
||||
setTypeButton('claim_reward');
|
||||
}
|
||||
}, [remainingTime, playerStore]);
|
||||
|
||||
const handleStartFarming = useCallback(async () => {
|
||||
if (!token || !playerStore) {
|
||||
return;
|
||||
}
|
||||
const api = PlayerApi.getInstance(token);
|
||||
const updatedPlayer = await api.startFarm(new Date(), playerStore.id);
|
||||
setPlayer(updatedPlayer);
|
||||
}, [playerStore]);
|
||||
|
||||
const handleClaimRewards = useCallback(async () => {
|
||||
if (!token || !playerStore || !configStore) {
|
||||
return;
|
||||
}
|
||||
const api = PlayerApi.getInstance(token);
|
||||
// TODO: улучшить безопасность в методе claimRewards
|
||||
const updatedPlayer = await api.claimRewards(
|
||||
playerStore.id,
|
||||
toFixed(playerStore?.attributes.balance) + configStore?.attributes.farm_amount,
|
||||
);
|
||||
setPlayer(updatedPlayer);
|
||||
}, [playerStore, configStore]);
|
||||
|
||||
return (
|
||||
<Home
|
||||
config={configStore}
|
||||
isLoading={isLoading}
|
||||
typeButton={typeButton}
|
||||
remainingTime={FormatTime(remainingTime)}
|
||||
handleStartFarming={handleStartFarming}
|
||||
handleClaimRewards={handleClaimRewards}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeContainer;
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { ConfigApi } from '@/api/config.api';
|
||||
import { useConfigStore } from '@/store/config.state';
|
||||
|
||||
export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||
const { setConfig } = useConfigStore();
|
||||
const { token } = useTokenStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const api = ConfigApi.getInstance(token);
|
||||
api.getConfig().then(config => setConfig(config.data));
|
||||
}, [token]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { ProvidersProps } from '@/components/providers/Providers';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { PlayerApi } from '@/api/player.api';
|
||||
import styles from './Providers.module.css';
|
||||
|
||||
export const PlayerProvider = ({
|
||||
children,
|
||||
id,
|
||||
}: PropsWithChildren & Pick<ProvidersProps, 'id'>) => {
|
||||
const { player, setPlayer, isLoading, stopLoading } = usePlayerStore();
|
||||
const { token } = useTokenStore();
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
console.log('[PlayerProvider] no token yet, waiting...');
|
||||
return;
|
||||
}
|
||||
console.log('[PlayerProvider] token received, loading player...');
|
||||
try {
|
||||
const tgUser = window.Telegram?.WebApp?.initDataUnsafe?.user;
|
||||
const username = String(tgUser?.username || 'unknown');
|
||||
console.log('[PlayerProvider] telegram username:', username, 'tgUser:', tgUser);
|
||||
setUsername(username);
|
||||
} catch (e) {
|
||||
console.error('[PlayerProvider] error getting telegram user:', e);
|
||||
}
|
||||
const playerApi = PlayerApi.getInstance(token);
|
||||
playerApi.findPlayerByAuthId(id).then(player => {
|
||||
console.log('[PlayerProvider] player loaded:', player?.id);
|
||||
setPlayer(player);
|
||||
stopLoading();
|
||||
}).catch(err => {
|
||||
console.error('[PlayerProvider] error loading player:', err);
|
||||
stopLoading();
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<span>Загрузка...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!player) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<span>
|
||||
Что-то пошло не так... Попробуйте написать /start, чтобы получить актуальную
|
||||
ссылку
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (player.attributes.telegram_nick !== username) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<span>
|
||||
Похоже, что Вы изменили свой ID в Telegram, попробуйте написать /start, чтобы
|
||||
получить актуальную ссылку
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.loading {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: var(--font-manrope);
|
||||
text-align: center;
|
||||
}
|
||||
28
CMyTapper/robucks-front/components/providers/Providers.tsx
Normal file
28
CMyTapper/robucks-front/components/providers/Providers.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { PlayerProvider } from '@/components/providers/PlayerProvider';
|
||||
import { TokenProvider } from '@/components/providers/TokenProvider';
|
||||
import { TasksProvider } from '@/components/providers/TasksProvider';
|
||||
import { ShopItemsProviver } from '@/components/providers/ShopItemsProvider';
|
||||
import { ConfigProvider } from '@/components/providers/ConfigProvider';
|
||||
import Navigation from '@/components/ui/navigation/Navigation';
|
||||
|
||||
export type ProvidersProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const Providers = ({ children, id }: PropsWithChildren & ProvidersProps) => {
|
||||
return (
|
||||
<TokenProvider>
|
||||
<TasksProvider>
|
||||
<ShopItemsProviver>
|
||||
<ConfigProvider>
|
||||
<PlayerProvider id={id}>
|
||||
{children}
|
||||
<Navigation id={id} />
|
||||
</PlayerProvider>
|
||||
</ConfigProvider>
|
||||
</ShopItemsProviver>
|
||||
</TasksProvider>
|
||||
</TokenProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { ShopItemApi } from '@/api/shop-item.api';
|
||||
import { useShopItemsStore } from '@/store/shopItems.state';
|
||||
|
||||
export const ShopItemsProviver = ({ children }: PropsWithChildren) => {
|
||||
const { setShopItems } = useShopItemsStore();
|
||||
const { token } = useTokenStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const api = ShopItemApi.getInstance(token);
|
||||
api.getAllShopItems().then(shopItems => setShopItems(shopItems));
|
||||
}, [token]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { useTasksStore } from '@/store/tasks.state';
|
||||
import { TasksApi } from '@/api/tasks.api';
|
||||
|
||||
export const TasksProvider = ({ children }: PropsWithChildren) => {
|
||||
const { setTasks } = useTasksStore();
|
||||
const { token } = useTokenStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const api = TasksApi.getInstance(token);
|
||||
api.getAllTasks().then(tasks => setTasks(tasks));
|
||||
}, [token]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
import { ConfigBotApi } from '@/api/config-bot.api';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
|
||||
const configBotApi = ConfigBotApi.getInstance();
|
||||
|
||||
export const TokenProvider = ({ children }: PropsWithChildren) => {
|
||||
const { setToken } = useTokenStore();
|
||||
|
||||
useEffect(() => {
|
||||
configBotApi.getConfigBot().then(cfg => {
|
||||
console.log('[TokenProvider] got config:', cfg);
|
||||
setToken(cfg.token);
|
||||
}).catch(err => {
|
||||
console.error('[TokenProvider] error getting config:', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
155
CMyTapper/robucks-front/components/shop/Shop.module.css
Normal file
155
CMyTapper/robucks-front/components/shop/Shop.module.css
Normal file
@@ -0,0 +1,155 @@
|
||||
.shop {
|
||||
background: #101010;
|
||||
height: calc(100vh - 82px);
|
||||
font-family: var(--font-manrope);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 82px;
|
||||
}
|
||||
|
||||
.item_img {
|
||||
--size: 32px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
max-width: var(--size);
|
||||
max-height: var(--size);
|
||||
}
|
||||
|
||||
.item_img_sm {
|
||||
--size: 18px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
max-width: var(--size);
|
||||
max-height: var(--size);
|
||||
}
|
||||
|
||||
.item_img > img, .item_img_sm > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 45px;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.shop.overflow {
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.itemsAll {
|
||||
color: #42BB47;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.header span {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.exchangeRobucks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.items {
|
||||
min-height: 155px;
|
||||
border-radius: 1.5rem;
|
||||
padding: 20px 16px;
|
||||
background: #0E0E10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.itemsHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
background: #FFFFFF0D;
|
||||
height: 76px;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shopItem {
|
||||
cursor: pointer;
|
||||
background: #FFFFFF0D;
|
||||
height: 76px;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shopItemPrice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.itemInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.itemInfoTitle span {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.itemInfoContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.upgrades {
|
||||
margin-top: 1rem;
|
||||
border-radius: 1.5rem;
|
||||
background: #0E0E10;
|
||||
border: 1px solid #18181A;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.upgradesTitle span {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
line-height: 20.9px;
|
||||
}
|
||||
|
||||
.upgradesContent {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.upgradesContent span {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.shopItems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
84
CMyTapper/robucks-front/components/shop/Shop.tsx
Normal file
84
CMyTapper/robucks-front/components/shop/Shop.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import styles from '@/components/shop/Shop.module.css';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import ModalExchangeContainer from '@/components/ui/modal-exchange/ModalExchangeContainer';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import ShopItem from '@/components/shop/ShopItem';
|
||||
import { IHandleOpen } from '@/components/shop/ShopContainer';
|
||||
import { useShopItemsStore } from '@/store/shopItems.state';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { toFixed } from '@/utils/FormatNumber';
|
||||
|
||||
interface IShop {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
handleOpen: (data: IHandleOpen) => void;
|
||||
}
|
||||
|
||||
const Shop: FC<IShop> = ({ isOpen, handleOpen, handleClose }) => {
|
||||
const shopBlockRef = useRef<HTMLDivElement>(null);
|
||||
const [isOverflow, setIsOverflow] = useState(false);
|
||||
|
||||
const { shopItems } = useShopItemsStore();
|
||||
const { player } = usePlayerStore();
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (shopBlockRef.current) {
|
||||
setIsOverflow(
|
||||
shopBlockRef.current.scrollHeight > shopBlockRef.current.clientHeight,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
|
||||
return () => window.removeEventListener('resize', checkOverflow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${styles.shop} ${isOverflow ? styles.overflow : ''}`} ref={shopBlockRef}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<span>Магазин</span>
|
||||
</div>
|
||||
<div className={styles.items}>
|
||||
<div className={styles.itemsHeader}>
|
||||
<span>Товары</span>
|
||||
<span className={styles.itemsAll}>Все</span>
|
||||
</div>
|
||||
<div className={styles.shopItems}>
|
||||
{shopItems &&
|
||||
shopItems.map(shopItem => (
|
||||
<ShopItem
|
||||
key={shopItem.id}
|
||||
shopItem={shopItem}
|
||||
handleOpen={handleOpen}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.upgrades}>
|
||||
<div className={styles.upgradesTitle}>
|
||||
<span>Улучшения</span>
|
||||
</div>
|
||||
<div className={styles.upgradesContent}>
|
||||
<span>Уже скоро...</span>
|
||||
</div>
|
||||
</div>
|
||||
<ModalExchangeContainer
|
||||
isOpen={isOpen}
|
||||
balance={toFixed(player?.attributes.balance)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shop;
|
||||
42
CMyTapper/robucks-front/components/shop/ShopContainer.tsx
Normal file
42
CMyTapper/robucks-front/components/shop/ShopContainer.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import Shop from '@/components/shop/Shop';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useModalStore } from '@/store/modal.state';
|
||||
|
||||
export interface IHandleOpen {
|
||||
image?: string;
|
||||
isDecimal?: boolean;
|
||||
priceItem?: number;
|
||||
shopItem?: number;
|
||||
}
|
||||
|
||||
const ShopContainer = () => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const { setShopItemId, setPriceItem, setImage, setIsDecimal } = useModalStore();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleOpen = useCallback((data: IHandleOpen) => {
|
||||
if (data.image && data.priceItem && data.shopItem && data.isDecimal !== undefined) {
|
||||
setIsDecimal(data.isDecimal);
|
||||
setImage(data.image);
|
||||
setPriceItem(data.priceItem);
|
||||
setShopItemId(data.shopItem);
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
setIsDecimal(true);
|
||||
setImage('/icons/small-coin-icon.svg');
|
||||
setPriceItem(0);
|
||||
setShopItemId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <Shop isOpen={isOpen} handleClose={handleClose} handleOpen={handleOpen} />;
|
||||
};
|
||||
|
||||
export default ShopContainer;
|
||||
63
CMyTapper/robucks-front/components/shop/ShopItem.tsx
Normal file
63
CMyTapper/robucks-front/components/shop/ShopItem.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { IShopItem } from '@/api/shop-item.api';
|
||||
import styles from '@/components/shop/Shop.module.css';
|
||||
import Image from 'next/image';
|
||||
import { IHandleOpen } from '@/components/shop/ShopContainer';
|
||||
|
||||
interface IShopItemProps {
|
||||
shopItem: IShopItem;
|
||||
handleOpen: (data: IHandleOpen) => void;
|
||||
}
|
||||
|
||||
const ShopItem: FC<IShopItemProps> = ({ shopItem, handleOpen }) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.item}
|
||||
onClick={() =>
|
||||
handleOpen({
|
||||
image: `${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`,
|
||||
isDecimal: shopItem.attributes.isDecimal,
|
||||
priceItem: shopItem.attributes.price,
|
||||
shopItem: shopItem.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
{shopItem?.attributes?.icon?.data && (
|
||||
<div className={styles.item_img}>
|
||||
<img
|
||||
loading={'lazy'}
|
||||
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
|
||||
alt={'shop item icon'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.itemInfo}>
|
||||
<div className={styles.itemInfoTitle}>
|
||||
<span>{shopItem.attributes.name}</span>
|
||||
</div>
|
||||
<div className={styles.itemInfoContent}>
|
||||
<span>{shopItem.attributes.price}</span>
|
||||
<Image
|
||||
src={'/icons/small-robucks-icon.svg'}
|
||||
alt={'small robucks'}
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
<span>=</span>
|
||||
<span>1</span>
|
||||
<div className={styles.item_img_sm}>
|
||||
<img
|
||||
loading={'lazy'}
|
||||
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
|
||||
alt={'small coin icon'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShopItem;
|
||||
65
CMyTapper/robucks-front/components/tasks/Task.tsx
Normal file
65
CMyTapper/robucks-front/components/tasks/Task.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { ITask } from '@/api/tasks.api';
|
||||
import { FC, useCallback } from 'react';
|
||||
import styles from '@/components/tasks/Tasks.module.css';
|
||||
import Image from 'next/image';
|
||||
import { IFinishedTasks } from '@/interfaces/player.type';
|
||||
import TaskContent from '@/components/tasks/TaskContent';
|
||||
|
||||
interface ITaskProps {
|
||||
task: ITask;
|
||||
finishedTask: IFinishedTasks | undefined;
|
||||
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||
handleOpenLink: (link: string, taskId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const Task: FC<ITaskProps> = ({ task, finishedTask, handleClaimPrize, handleOpenLink }) => {
|
||||
const handleTaskFunction = useCallback(() => {
|
||||
if (!finishedTask) {
|
||||
return handleOpenLink(task.attributes.link, task.id);
|
||||
}
|
||||
}, [handleOpenLink, finishedTask]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
finishedTask && finishedTask.attributes.status === 'claimed'
|
||||
? `${styles.taskClaimed} ${styles.task}`
|
||||
: `${styles.task}`
|
||||
}
|
||||
onClick={handleTaskFunction}
|
||||
>
|
||||
{task?.attributes?.icon?.data && (
|
||||
<div className={styles.taskIcon}>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${task.attributes.icon.data.attributes.url}`}
|
||||
alt={'task icon'}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.taskContent}>
|
||||
<div
|
||||
className={
|
||||
finishedTask && finishedTask.attributes.status === 'claimed'
|
||||
? `${styles.taskClaimedTitle} ${styles.taskTitle}`
|
||||
: `${styles.taskTitle}`
|
||||
}
|
||||
>
|
||||
<span>{task.attributes.title}</span>
|
||||
</div>
|
||||
<TaskContent
|
||||
taskId={task.id}
|
||||
link={task.attributes.link}
|
||||
status={finishedTask?.attributes.status}
|
||||
prize={task.attributes.prize}
|
||||
handleClaimPrize={handleClaimPrize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Task;
|
||||
61
CMyTapper/robucks-front/components/tasks/TaskContent.tsx
Normal file
61
CMyTapper/robucks-front/components/tasks/TaskContent.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
import { FC, useState } from 'react';
|
||||
import styles from '@/components/tasks/Tasks.module.css';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface ITaskContent {
|
||||
taskId: number;
|
||||
link: string;
|
||||
status: string | undefined;
|
||||
prize: number;
|
||||
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const TaskContent: FC<ITaskContent> = ({ taskId, status, prize, handleClaimPrize }) => {
|
||||
const [isClaiming, setIsClaiming] = useState(false);
|
||||
|
||||
const onClaim = () => {
|
||||
setIsClaiming(true);
|
||||
handleClaimPrize(taskId, prize).catch(err => {
|
||||
console.error(err);
|
||||
setIsClaiming(false);
|
||||
});
|
||||
};
|
||||
if (status) {
|
||||
if (status === 'pending_prize') {
|
||||
return (
|
||||
<>
|
||||
<button className={styles.claimPrize} onClick={onClaim} disabled={isClaiming}>
|
||||
{`Получить +${prize}`}
|
||||
<Image
|
||||
src={'/icons/small-robucks-icon.svg'}
|
||||
alt={'small coin icon'}
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
} else if (status === 'claimed') {
|
||||
return (
|
||||
<div className={styles.claimedPrize}>
|
||||
<span>Выполнено</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.taskPrize}>
|
||||
<span>+ {prize}</span>
|
||||
<Image
|
||||
src={'/icons/small-robucks-icon.svg'}
|
||||
alt={'small coin icon'}
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TaskContent;
|
||||
109
CMyTapper/robucks-front/components/tasks/Tasks.module.css
Normal file
109
CMyTapper/robucks-front/components/tasks/Tasks.module.css
Normal file
@@ -0,0 +1,109 @@
|
||||
.tasks {
|
||||
background: #101010;
|
||||
padding-top: 45px;
|
||||
font-family: var(--font-manrope);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tasksHeader {
|
||||
padding-left: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tasksHeader span {
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
color: white;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.tasksBlock {
|
||||
height: auto;
|
||||
margin-top: 1rem;
|
||||
padding: 30px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: #0E0E10;
|
||||
border: 1px solid #18181A;
|
||||
border-radius: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tasksBlock.overflow {
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
|
||||
.task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFFFF0D;
|
||||
min-height: 75px;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
gap: 7.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.taskContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.taskTitle span {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.taskPrize {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.taskPrize span {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #BBBBBB;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.claimPrize {
|
||||
font-family: var(--font-manrope);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #101010;
|
||||
border: 1px solid #42BB47;
|
||||
width: 267px;
|
||||
height: 42px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 40px;
|
||||
gap: 5px;
|
||||
color: #42BB47;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.claimedPrize {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.claimedPrize span {
|
||||
color: #42BB47;
|
||||
}
|
||||
|
||||
.taskClaimed {
|
||||
background: #42BB4740;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.taskClaimedTitle span {
|
||||
color: #42BB47;
|
||||
}
|
||||
64
CMyTapper/robucks-front/components/tasks/Tasks.tsx
Normal file
64
CMyTapper/robucks-front/components/tasks/Tasks.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import styles from '@/components/tasks/Tasks.module.css';
|
||||
import Task from '@/components/tasks/Task';
|
||||
import { useTasksStore } from '@/store/tasks.state';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
|
||||
interface ITasksProps {
|
||||
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||
handleOpenLink: (link: string, taskId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const Tasks: FC<ITasksProps> = ({ handleOpenLink, handleClaimPrize }) => {
|
||||
const tasksBlockRef = useRef<HTMLDivElement>(null);
|
||||
const [isOverflow, setIsOverflow] = useState(false);
|
||||
const { tasks } = useTasksStore();
|
||||
const { player } = usePlayerStore();
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (tasksBlockRef.current) {
|
||||
setIsOverflow(
|
||||
tasksBlockRef.current.scrollHeight > tasksBlockRef.current.clientHeight,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
window.addEventListener('resize', checkOverflow);
|
||||
|
||||
return () => window.removeEventListener('resize', checkOverflow);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.tasks}>
|
||||
<div className={styles.tasksHeader}>
|
||||
<span>Задания</span>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.tasksBlock} ${isOverflow ? styles.overflow : ''}`}
|
||||
ref={tasksBlockRef}
|
||||
>
|
||||
{tasks.map(task => {
|
||||
const finishedTask = player?.attributes.finished_tasks.data?.find(
|
||||
el => el?.attributes.task?.data?.id === task?.id,
|
||||
);
|
||||
return (
|
||||
<Task
|
||||
key={task.id}
|
||||
task={task}
|
||||
finishedTask={finishedTask}
|
||||
handleClaimPrize={handleClaimPrize}
|
||||
handleOpenLink={handleOpenLink}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tasks;
|
||||
54
CMyTapper/robucks-front/components/tasks/TasksContainer.tsx
Normal file
54
CMyTapper/robucks-front/components/tasks/TasksContainer.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import Tasks from '@/components/tasks/Tasks';
|
||||
import { useCallback } from 'react';
|
||||
import { StatusTask } from '@/interfaces/task.type';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { useTokenStore } from '@/store/token.state';
|
||||
import { PlayerApi } from '@/api/player.api';
|
||||
|
||||
const TasksContainer = () => {
|
||||
const { player, setPlayer } = usePlayerStore();
|
||||
const { token } = useTokenStore();
|
||||
|
||||
const handleClaimPrize = useCallback(
|
||||
async (taskId: number) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const playerApi = PlayerApi.getInstance(token);
|
||||
const updatedPlayer = await playerApi.updateTasks(player!.id, {
|
||||
taskId: taskId,
|
||||
status: 'claimed' as StatusTask,
|
||||
});
|
||||
const newPlayer = await playerApi.findPlayerByAuthId(
|
||||
encodeURIComponent(updatedPlayer.attributes.auth_id),
|
||||
);
|
||||
setPlayer(newPlayer);
|
||||
},
|
||||
[player, token],
|
||||
);
|
||||
|
||||
const handleOpenLink = useCallback(
|
||||
async (link: string, taskId: number) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
window.Telegram.WebApp.openLink(link);
|
||||
const playerApi = PlayerApi.getInstance(token);
|
||||
const updatedPlayer = await playerApi.updateTasks(player!.id, {
|
||||
taskId: taskId,
|
||||
status: 'pending_prize' as StatusTask,
|
||||
});
|
||||
const newPlayer = await playerApi.findPlayerByAuthId(
|
||||
encodeURIComponent(updatedPlayer.attributes.auth_id),
|
||||
);
|
||||
setPlayer(newPlayer);
|
||||
},
|
||||
[player, token],
|
||||
);
|
||||
|
||||
return <Tasks handleClaimPrize={handleClaimPrize} handleOpenLink={handleOpenLink} />;
|
||||
};
|
||||
|
||||
export default TasksContainer;
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import styles from '@/components/home/Home.module.css';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface IButtonMining {
|
||||
isLoading: boolean;
|
||||
typeButton: 'pending' | 'farming' | 'claim_reward';
|
||||
remainingTime: string | null;
|
||||
farm_amount: number | undefined;
|
||||
handleStartFarming: () => void;
|
||||
handleClaimRewards: () => void;
|
||||
}
|
||||
|
||||
const ButtonMining: FC<IButtonMining> = ({
|
||||
isLoading,
|
||||
typeButton,
|
||||
remainingTime,
|
||||
farm_amount,
|
||||
handleStartFarming,
|
||||
handleClaimRewards,
|
||||
}) => {
|
||||
if (typeButton === 'pending') {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={
|
||||
isLoading
|
||||
? `${styles.disabledButton} ${styles.miningButton}`
|
||||
: `${styles.miningButton}`
|
||||
}
|
||||
disabled={isLoading}
|
||||
onClick={handleStartFarming}
|
||||
>
|
||||
Начать добычу
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (typeButton === 'farming' && remainingTime) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={
|
||||
isLoading
|
||||
? `${styles.disabledButton} ${styles.farmingButton}`
|
||||
: `${styles.farmingButton}`
|
||||
}
|
||||
>{`Собрать добычу через ${remainingTime}`}</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (typeButton === 'claim_reward') {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={
|
||||
isLoading
|
||||
? `${styles.disabledButton} ${styles.claimReward}`
|
||||
: `${styles.claimReward}`
|
||||
}
|
||||
onClick={handleClaimRewards}
|
||||
>
|
||||
<div className={styles.reward}>
|
||||
<span>Собрать добычу </span>
|
||||
<div className={styles.claimAmount}>
|
||||
<span>+</span>
|
||||
<Image
|
||||
src={'/icons/small-robucks-icon.svg'}
|
||||
alt={'small robucks'}
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
<span>{farm_amount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ButtonMining;
|
||||
@@ -0,0 +1,56 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
right: -500px;
|
||||
bottom: 90px;
|
||||
height: 89px;
|
||||
border-radius: 20px;
|
||||
padding: 12px 16px;
|
||||
background: #000000;
|
||||
border: 1px solid #143916;
|
||||
transition: right 0.3s ease;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 21px;
|
||||
font-family: var(--font-manrope);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.visible {
|
||||
right: 20px;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
import { FC } from 'react';
|
||||
import styles from '@/components/ui/copy-link-toast/CopyLinkToast.module.css';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface ICopyLinkToast {
|
||||
isVisible: boolean;
|
||||
handleCloseToastLink: () => void;
|
||||
}
|
||||
|
||||
const CopyLinkToast: FC<ICopyLinkToast> = ({ isVisible, handleCloseToastLink }) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={`${styles.toast} ${isVisible ? styles.visible : ''}`}>
|
||||
<div className={styles.closeIcon} onClick={handleCloseToastLink}>
|
||||
<Image
|
||||
src={'/icons/close-icon.svg'}
|
||||
alt={'close icon'}
|
||||
width={12}
|
||||
height={12}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.img}>
|
||||
<Image
|
||||
src={'/icons/info-icon.svg'}
|
||||
alt={'info icon'}
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<p>Приглашение скопировано 🔗</p>
|
||||
<span>Отправьте эту ссылку своим друзьям 📨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyLinkToast;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,40 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
position: fixed;
|
||||
height: 82px;
|
||||
width: 100%;
|
||||
background: #101010;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav_menu, .active_nav_menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16.3px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.countTasks {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
right: 10%;
|
||||
top: -10%;
|
||||
background: #D92000;
|
||||
}
|
||||
|
||||
.active_nav_menu span, .active_nav_menu img {
|
||||
color: #42BB47;
|
||||
}
|
||||
126
CMyTapper/robucks-front/components/ui/navigation/Navigation.tsx
Normal file
126
CMyTapper/robucks-front/components/ui/navigation/Navigation.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import styles from './Navigation.module.css';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { usePlayerStore } from '@/store/player.state';
|
||||
import { useTasksStore } from '@/store/tasks.state';
|
||||
import { ProvidersProps } from '@/components/providers/Providers';
|
||||
|
||||
const Navigation = ({ id }: Pick<ProvidersProps, 'id'>) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { player } = usePlayerStore();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const { tasks } = useTasksStore();
|
||||
const countTasks = tasks.length - (player?.attributes.finished_tasks.data?.length || 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
const getNavLink = (endpoint: string) => {
|
||||
return `/${id}/${endpoint}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Link href={getNavLink('home')} style={{ textDecoration: 'none' }}>
|
||||
<div className={pathname === '/' ? styles.active_nav_menu : styles.nav_menu}>
|
||||
{pathname === '/' ? (
|
||||
<Image
|
||||
src={'/icons/pickaxe-icon-selected.svg'}
|
||||
alt={'pickaxe-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={'/icons/pickaxe-icon.svg'}
|
||||
alt={'pickaxe-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
<span>Главная</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={getNavLink('shop')} style={{ textDecoration: 'none' }}>
|
||||
<div className={pathname === '/shop' ? styles.active_nav_menu : styles.nav_menu}>
|
||||
{pathname === '/shop' ? (
|
||||
<Image
|
||||
src={'/icons/shop-icon-selected.svg'}
|
||||
alt={'shop-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={'/icons/shop-icon.svg'}
|
||||
alt={'shop-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
<span>Магазин</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={getNavLink('tasks')}
|
||||
style={{ textDecoration: 'none', position: 'relative', height: '82px' }}
|
||||
>
|
||||
{countTasks !== 0 && <div className={styles.countTasks}>{countTasks}</div>}
|
||||
<div
|
||||
className={
|
||||
pathname === '/tasks'
|
||||
? `${styles.active_nav_menu} ${styles.task}`
|
||||
: `${styles.nav_menu} ${styles.task}`
|
||||
}
|
||||
>
|
||||
{pathname === '/tasks' ? (
|
||||
<Image
|
||||
src={'/icons/task-list-icon-selected.svg'}
|
||||
alt={'task-list-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={'/icons/task-list-icon.svg'}
|
||||
alt={'task-list-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
<span>Задания</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={getNavLink('friends')} style={{ textDecoration: 'none' }}>
|
||||
<div className={pathname === '/friends' ? styles.active_nav_menu : styles.nav_menu}>
|
||||
{pathname === '/friends' ? (
|
||||
<Image
|
||||
src={'/icons/friends-icon-selected.svg'}
|
||||
alt={'friends-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={'/icons/friends-icon.svg'}
|
||||
alt={'friends-icon'}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
<span>Друзья</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
@@ -0,0 +1,6 @@
|
||||
.wrapper {
|
||||
max-width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
12
CMyTapper/robucks-front/components/ui/wrapper/Wrapper.tsx
Normal file
12
CMyTapper/robucks-front/components/ui/wrapper/Wrapper.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import {PropsWithChildren} from "react";
|
||||
import styles from './Wrapper.module.css'
|
||||
|
||||
const Wrapper = ({ children } : PropsWithChildren) => {
|
||||
return(
|
||||
<div className={styles.wrapper}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
Reference in New Issue
Block a user