diff --git a/public/locales/en/airdrop.json b/public/locales/en/airdrop.json index b567870d5..637707fc7 100644 --- a/public/locales/en/airdrop.json +++ b/public/locales/en/airdrop.json @@ -1,8 +1,14 @@ { + "airdropGame": "Airdrop Game", "loginButton": "Log in to claim gems", "logout": "Logout", "referral": "Invite a friend", - "claimGems": "Claim gift Gems", + "inviteFirends": "Invite Friends", + "invitedAmount": "You’ve invited {{count}} friends", + "nextTierBonus": "Invite {{count}} & earn {{bonusGems}} bonus gems", + "claimGems": "Claim Gems", + "doLater": "I'll do this later", + "linkCopied": "Link copied!", "giftReferralCodeHeader": "Claim your referral code {{gems}} gems.", "claimReferralCode": "Claim your referral code {{gems}} gems.", "claimReferralGifts": "Claim your referral code and earn {{gems}} gems. Once you connect your account, you’ll be awarded your gems.", diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 7db585ddb..6cea709bf 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -39,6 +39,8 @@ pub struct AppConfigFromFile { should_always_use_system_language: bool, #[serde(default = "default_application_language")] application_language: String, + #[serde(default = "default_false")] + airdrop_ui_enabled: bool, } impl Default for AppConfigFromFile { @@ -57,6 +59,7 @@ impl Default for AppConfigFromFile { has_system_language_been_proposed: false, should_always_use_system_language: false, application_language: default_application_language(), + airdrop_ui_enabled: false, } } } @@ -101,6 +104,7 @@ pub(crate) struct AppConfig { has_system_language_been_proposed: bool, should_always_use_system_language: bool, application_language: String, + airdrop_ui_enabled: bool, } impl AppConfig { @@ -120,6 +124,7 @@ impl AppConfig { has_system_language_been_proposed: false, should_always_use_system_language: false, application_language: default_application_language(), + airdrop_ui_enabled: false, } } @@ -155,6 +160,7 @@ impl AppConfig { self.has_system_language_been_proposed = config.has_system_language_been_proposed; self.should_always_use_system_language = config.should_always_use_system_language; self.application_language = config.application_language; + self.airdrop_ui_enabled = config.airdrop_ui_enabled; } Err(e) => { warn!(target: LOG_TARGET, "Failed to parse app config: {}", e.to_string()); @@ -222,6 +228,12 @@ impl AppConfig { self.auto_mining } + // pub async fn set_airdrop_ui_enabled(&mut self, airdrop_ui_enabled: bool) -> Result<(), anyhow::Error> { + // self.airdrop_ui_enabled = airdrop_ui_enabled; + // self.update_config_file().await?; + // Ok(()) + // } + pub async fn set_allow_telemetry( &mut self, allow_telemetry: bool, diff --git a/src/App.tsx b/src/App.tsx index 62fb87fd2..4a3149ca3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,11 +5,9 @@ import { Dashboard } from './containers/Dashboard'; import { useUIStore } from './store/useUIStore.ts'; import { useSetUp } from './hooks/useSetUp.ts'; -import { useEnvironment } from './hooks/useEnvironment.ts'; import { SplashScreen } from './containers/SplashScreen'; import ThemeProvider from './theme/ThemeProvider.tsx'; import { GlobalReset, GlobalStyle } from '@app/theme/GlobalStyle.ts'; -import AirdropLogin from './containers/Airdrop/AirdropLogin/AirdropLogin.tsx'; import ErrorSnackbar from '@app/containers/Error/ErrorSnackbar.tsx'; import { useShuttingDown } from './hooks/useShuttingDown.ts'; import ShuttingDownScreen from './containers/ShuttingDownScreen/ShuttingDownScreen.tsx'; @@ -60,7 +58,6 @@ export default function App() { - {shutDownMarkup} {!visualMode || view != 'mining' ? ( diff --git a/src/containers/Airdrop/AirdropGiftTracker/AirdropGiftTracker.tsx b/src/containers/Airdrop/AirdropGiftTracker/AirdropGiftTracker.tsx new file mode 100644 index 000000000..2d79a1ea4 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/AirdropGiftTracker.tsx @@ -0,0 +1,28 @@ +import { useAirdropStore } from '@app/store/useAirdropStore'; +import { Title, Wrapper, TitleWrapper } from './styles'; +import LoggedOut from './sections/LoggedOut/LoggedOut'; +import LoggedIn from './sections/LoggedIn/LoggedIn'; +import { useAirdropSyncState } from '@app/hooks/airdrop/useAirdropSyncState'; +import { useAppConfigStore } from '@app/store/useAppConfigStore'; +import { useTranslation } from 'react-i18next'; + +export default function AirdropGiftTracker() { + useAirdropSyncState(); + const { t } = useTranslation(['airdrop'], { useSuspense: false }); + const airdrop_ui_enabled = useAppConfigStore((s) => s.airdrop_ui_enabled); + const { airdropTokens } = useAirdropStore(); + + if (!airdrop_ui_enabled) return null; + + const isLoggedIn = !!airdropTokens; + + return ( + + + {t('airdropGame')} + + + {isLoggedIn ? : } + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/ClaimModal.tsx b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/ClaimModal.tsx new file mode 100644 index 000000000..023e11d58 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/ClaimModal.tsx @@ -0,0 +1,86 @@ +import { + ActionWrapper, + BoxWrapper, + ClaimButton, + Cover, + Gem1, + Gem2, + Gem3, + GemsWrapper, + GemTextImage, + ShareWrapper, + StyledInput, + Text, + TextButton, + TextWrapper, + Title, + Wrapper, +} from './styles'; +import gemImage from './images/gems.png'; +import gemLargeImage from './images/gem-large.png'; +import { useCallback, useState } from 'react'; +import { GIFT_GEMS, useAirdropStore } from '@app/store/useAirdropStore'; +import { Trans, useTranslation } from 'react-i18next'; + +interface ClaimModalProps { + onSubmit: (code?: string) => void; + onClose: () => void; +} + +export default function ClaimModal({ onSubmit, onClose }: ClaimModalProps) { + const referralQuestPoints = useAirdropStore((state) => state.referralQuestPoints); + const { t } = useTranslation(['airdrop'], { useSuspense: false }); + + const [claimCode, setClaimCode] = useState(''); + + const handleSubmit = useCallback(async () => { + return onSubmit(claimCode); + }, [claimCode, onSubmit]); + + return ( + + + + + + + + + + + <Trans + ns="airdrop" + i18nKey="claimReferralCode" + components={{ span: <span />, image: <GemTextImage src={gemImage} alt="" /> }} + values={{ gems: referralQuestPoints?.pointsForClaimingReferral || GIFT_GEMS }} + /> + + + {t('claimReferralGifts', { gems: referralQuestPoints?.pointsForClaimingReferral || GIFT_GEMS })} + + + + + setClaimCode(e.target.value)} + value={claimCode} + /> + { + // + } + + {t('claimGems')} + + + {t('doLater')} + + + + + + ); +} diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/images/gem-large.png b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/images/gem-large.png similarity index 100% rename from src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/images/gem-large.png rename to src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/images/gem-large.png diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/images/gems.png b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/images/gems.png similarity index 100% rename from src/containers/Airdrop/AirdropLogin/UserInfo/images/gems.png rename to src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/images/gems.png diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/styles.ts similarity index 76% rename from src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/styles.ts rename to src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/styles.ts index 189d56300..b70d70697 100644 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/styles.ts +++ b/src/containers/Airdrop/AirdropGiftTracker/components/Claimmodal/styles.ts @@ -1,4 +1,5 @@ import { m } from 'framer-motion'; + import styled, { css, keyframes } from 'styled-components'; export const Wrapper = styled('div')` @@ -29,10 +30,10 @@ export const Cover = styled(m.div)` export const BoxWrapper = styled(m.div)` width: 100%; - max-width: 560px; + max-width: 635px; flex-shrink: 0; - min-height: 479px; + // min-height: 650px; border-radius: 35px; background: linear-gradient(180deg, #c9eb00 32.79%, #fff 69.42%); @@ -41,7 +42,7 @@ export const BoxWrapper = styled(m.div)` position: relative; z-index: 1; - padding: 0 15px 22px 15px; + padding: 180px 50px 22px 50px; display: flex; flex-direction: column; @@ -62,7 +63,7 @@ export const TextWrapper = styled('div')` export const Title = styled('div')` color: #000; text-align: center; - font-size: 28px; + font-size: 32px; font-style: normal; font-weight: 800; line-height: 99.7%; @@ -123,54 +124,12 @@ export const ShareWrapper = styled('div')<{ $isClaim?: boolean }>` ${({ $isClaim }) => $isClaim && css` + min-height: 70px; font-weight: bold; - background: white; + background: rgba(255, 255, 255, 0.1); `}; `; -export const ShareText = styled('div')` - color: #c9eb00; - font-size: 18px; - font-style: normal; - font-weight: 600; - line-height: 99.7%; -`; - -export const CopyButton = styled('button')` - width: 113px; - height: 51px; - flex-shrink: 0; - - border-radius: 100px; - background: #fff; - - color: #000; - font-size: 18px; - font-style: normal; - font-weight: 600; - line-height: 99.7%; - - display: flex; - justify-content: center; - align-items: center; - - cursor: pointer; - position: relative; - - .copytext { - display: inline-block; - transition: transform 0.2s; - } - - &:hover { - .copytext { - transform: scale(1.1); - } - } -`; - -export const CopyText = styled(m.span)``; - const float = keyframes` 0% { transform: translateY(0) rotate(0deg); @@ -221,3 +180,52 @@ export const Gem3 = styled('img')` rotate: 45deg; animation: ${float} 3.8s ease-in-out infinite; `; + +export const StyledInput = styled('input')` + width: 100%; +`; + +export const ClaimButton = styled('button')` + transition: transform 0.2s ease; + text-transform: uppercase; + color: #c9eb00; + font-size: 21px; + text-align: center; + font-family: Druk, sans-serif; + width: 100%; + border-radius: 49px; + background: #000; + box-shadow: 28px 28px 77px 0px rgba(0, 0, 0, 0.1); + min-height: 70px; + + position: relative; + font-weight: bold; + &:hover { + transform: scale(1.05); + } +`; + +export const ActionWrapper = styled('div')` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; +`; + +export const TextButton = styled('button')` + text-transform: uppercase; + color: black; + font-size: 16px; + text-align: center; + font-family: Poppins, sans-serif; + + position: relative; + font-weight: bold; + text-align: center; + margin-bottom: 15px; + width: fit-content; + &:hover { + text-decoration: underline; + } +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/components/Gems/Gems.tsx b/src/containers/Airdrop/AirdropGiftTracker/components/Gems/Gems.tsx new file mode 100644 index 000000000..8433e6d93 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/components/Gems/Gems.tsx @@ -0,0 +1,18 @@ +import { Wrapper, Number, Label, GemImage } from './styles'; +import gemImage from '../../images/gem.png'; + +interface Props { + number: number; + label: string; +} + +export default function Gems({ number, label }: Props) { + return ( + + + {number.toLocaleString()} + + + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/components/Gems/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/components/Gems/styles.ts new file mode 100644 index 000000000..6a6451fb3 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/components/Gems/styles.ts @@ -0,0 +1,26 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +`; + +export const Number = styled('div')` + display: flex; + align-items: center; + gap: 2px; + + color: #000; + font-size: 18px; + font-weight: 600; +`; + +export const Label = styled('div')` + color: #797979; + font-size: 12px; + font-weight: 500; +`; + +export const GemImage = styled('img')``; diff --git a/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/InfoTooltip.tsx b/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/InfoTooltip.tsx new file mode 100644 index 000000000..84ce22608 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/InfoTooltip.tsx @@ -0,0 +1,9 @@ +import { Wrapper } from './styles'; + +interface Props { + children: React.ReactNode; +} + +export default function InfoTooltip({ children }: Props) { + return {children}; +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/styles.ts new file mode 100644 index 000000000..6a6451fb3 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/components/InfoTooltip/styles.ts @@ -0,0 +1,26 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; +`; + +export const Number = styled('div')` + display: flex; + align-items: center; + gap: 2px; + + color: #000; + font-size: 18px; + font-weight: 600; +`; + +export const Label = styled('div')` + color: #797979; + font-size: 12px; + font-weight: 500; +`; + +export const GemImage = styled('img')``; diff --git a/src/containers/Airdrop/AirdropGiftTracker/images/gem.png b/src/containers/Airdrop/AirdropGiftTracker/images/gem.png new file mode 100644 index 000000000..663e4b2ba Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/images/gem.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/images/gift.png b/src/containers/Airdrop/AirdropGiftTracker/images/gift.png new file mode 100644 index 000000000..16722720f Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/images/gift.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/images/gold_box.png b/src/containers/Airdrop/AirdropGiftTracker/images/gold_box.png new file mode 100644 index 000000000..2d96da3be Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/images/gold_box.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/images/trophy.png b/src/containers/Airdrop/AirdropGiftTracker/images/trophy.png new file mode 100644 index 000000000..8fee85aa5 Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/images/trophy.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/LoggedIn.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/LoggedIn.tsx new file mode 100644 index 000000000..6d9064754 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/LoggedIn.tsx @@ -0,0 +1,73 @@ +import { useEffect, useMemo, useState } from 'react'; +import Gems from '../../components/Gems/Gems'; +import UserInfo from './segments/UserInfo/UserInfo'; +import { UserRow, Wrapper } from './styles'; +import { useAirdropStore, REFERRAL_GEMS, GIFT_GEMS } from '@app/store/useAirdropStore'; +import Invite from './segments/Invite/Invite'; +import Flare from './segments/Flare/Flare'; +import { AnimatePresence } from 'framer-motion'; + +export default function LoggedIn() { + const [gems, setGems] = useState(0); + + const { userDetails, userPoints, flareAnimationType, bonusTiers, setFlareAnimationType, referralQuestPoints } = + useAirdropStore(); + + useEffect(() => { + setGems(userPoints?.base.gems || userDetails?.user?.rank?.gems || 0); + }, [userPoints?.base.gems, userDetails?.user?.rank?.gems]); + + // const handleShowFlare = () => { + // if (flareAnimationType) { + // setFlareAnimationType(); + // return; + // } + // + // //setShowFlare('GoalComplete'); + // setFlareAnimationType('FriendAccepted'); + // //setShowFlare('BonusGems'); + // }; + // + const bonusTier = useMemo( + () => + bonusTiers + ?.sort((a, b) => a.target - b.target) + .find((t) => t.target == (userPoints?.base.gems || userDetails?.user?.rank?.gems || 0)), + [bonusTiers, userDetails?.user?.rank?.gems, userPoints?.base.gems] + ); + + const flareGems = useMemo(() => { + switch (flareAnimationType) { + case 'GoalComplete': + return bonusTier?.bonusGems || 0; + case 'FriendAccepted': + return referralQuestPoints?.pointsForClaimingReferral || REFERRAL_GEMS; + case 'BonusGems': + return referralQuestPoints?.pointsForClaimingReferral || GIFT_GEMS; + default: + return 0; + } + }, [flareAnimationType, bonusTier?.bonusGems, referralQuestPoints?.pointsForClaimingReferral]); + + return ( + + + + + + + + + + {flareAnimationType && ( + setFlareAnimationType()} + onClick={() => setFlareAnimationType()} + /> + )} + + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/BonusGems.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/BonusGems.tsx new file mode 100644 index 000000000..549da4f8e --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/BonusGems.tsx @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import GemsAnimation from '../GemsAnimation/GemsAnimation'; +import { Background, Wrapper } from './styles'; +import { Number, Text, TextBottom, TextBottomPosition } from '../styles'; + +interface Props { + gems: number; + onAnimationComplete: () => void; +} + +export default function BonusGems({ gems, onAnimationComplete }: Props) { + useEffect(() => { + const timer = setTimeout(() => { + onAnimationComplete(); + }, 10000); + + return () => clearTimeout(timer); + }, [onAnimationComplete]); + + return ( + + + {gems.toLocaleString()} + + + + Bonus gems earned + + + + + Keep mining to earn more rewards! + + + + + + + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/styles.ts new file mode 100644 index 000000000..67d6fddcf --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/BonusGems/styles.ts @@ -0,0 +1,27 @@ +import { m } from 'framer-motion'; +import styled from 'styled-components'; +import backgroundImage from '../images/bonus_gems_bg.png'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + height: 100%; + color: #000; +`; + +export const Background = styled(m.div)` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + + background-image: url(${backgroundImage}); + background-size: cover; + background-position: center; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/Flare.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/Flare.tsx new file mode 100644 index 000000000..e493ef36c --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/Flare.tsx @@ -0,0 +1,25 @@ +import BonusGems from './BonusGems/BonusGems'; +import FriendAccepted from './FriendAccepted/FriendAccepted'; +import GoalComplete from './GoalComplete/GoalComplete'; +import { Wrapper } from './styles'; + +interface Props { + gems: number; + animationType: 'GoalComplete' | 'FriendAccepted' | 'BonusGems'; + onAnimationComplete: () => void; + onClick: () => void; +} + +export default function Flare({ gems, animationType, onAnimationComplete, onClick }: Props) { + return ( + + {animationType === 'GoalComplete' && } + + {animationType === 'FriendAccepted' && ( + + )} + + {animationType === 'BonusGems' && } + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/FriendAccepted.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/FriendAccepted.tsx new file mode 100644 index 000000000..348004cbc --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/FriendAccepted.tsx @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import GemsAnimation from '../GemsAnimation/GemsAnimation'; +import { Background, Wrapper } from './styles'; +import { Number, Text, TextBottom, TextBottomPosition } from '../styles'; + +interface Props { + gems: number; + onAnimationComplete: () => void; +} + +export default function FriendAccepted({ gems, onAnimationComplete }: Props) { + useEffect(() => { + const timer = setTimeout(() => { + onAnimationComplete(); + }, 10000); + + return () => clearTimeout(timer); + }, [onAnimationComplete]); + + return ( + + + {gems.toLocaleString()} + + + + Bonus gems earned + + + + + One of your friends accepted your gift! + + + + + + + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/styles.ts new file mode 100644 index 000000000..e4126e418 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/FriendAccepted/styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; +import backgroundImage from '../images/friend_accepted_bg.png'; +import { m } from 'framer-motion'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + width: 100%; + height: 100%; + + color: #fff; +`; + +export const Background = styled(m.div)` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + + background-color: #2a3342; + background-image: url(${backgroundImage}); + background-size: cover; + background-position: center; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/GemsAnimation.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/GemsAnimation.tsx new file mode 100644 index 000000000..277642a13 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/GemsAnimation.tsx @@ -0,0 +1,142 @@ +import React, { useEffect, useRef } from 'react'; +import gemImage from '../images/gem.png'; +import { Canvas, Wrapper } from './styles'; + +interface Gem { + x: number; + y: number; + speed: number; + size: number; + rotation: number; + rotationSpeed: number; + reset: (containerWidth: number, containerHeight: number) => void; + fall: (containerHeight: number) => void; + draw: (ctx: CanvasRenderingContext2D, image: HTMLImageElement) => void; +} + +class GemImpl implements Gem { + x: number; + y: number; + speed: number; + size: number; + rotation: number; + rotationSpeed: number; + + constructor(containerWidth: number) { + this.x = 0; + this.y = 0; + this.speed = 0; + this.size = 0; + this.rotation = 0; + this.rotationSpeed = 0; + this.reset(containerWidth); + } + + reset(containerWidth: number): void { + this.x = Math.random() * containerWidth; + this.y = -30; // Start slightly above the container + + const sizeDistribution = Math.random(); + if (sizeDistribution < 0.7) { + this.size = 10 + Math.random() * 10; // 10 to 20 pixels + } else if (sizeDistribution < 0.9) { + this.size = 20 + Math.random() * 10; // 20 to 30 pixels + } else { + this.size = 30 + Math.random() * 10; // 30 to 40 pixels + } + + this.speed = 1.5 - this.size / 40 + Math.random() * 1; + this.rotation = Math.random() * Math.PI * 2; + this.rotationSpeed = (Math.random() - 0.5) * 0.05; + } + + fall(containerHeight: number): void { + this.y += this.speed; + if (this.y > containerHeight + this.size) { + this.y = -this.size; + } + this.rotation += this.rotationSpeed; + } + + draw(ctx: CanvasRenderingContext2D, image: HTMLImageElement): void { + ctx.save(); + ctx.translate(this.x + this.size / 2, this.y + this.size / 2); + ctx.rotate(this.rotation); + ctx.drawImage(image, -this.size / 2, -this.size / 2, this.size, this.size); + ctx.restore(); + } +} + +interface GemsAnimationProps { + delay?: number; // Delay in seconds +} + +const GemsAnimation: React.FC = ({ delay = 0 }) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let animationFrameId: number; + let gems: Gem[] = []; + const totalGems = 15; + const image = new Image(); + image.src = gemImage; + + const resizeCanvas = () => { + const parent = canvas.parentElement; + if (!parent) return; + + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + + // Reset gems when canvas size changes + gems = []; + for (let i = 0; i < totalGems; i++) { + gems.push(new GemImpl(canvas.width)); + } + }; + + const resizeObserver = new ResizeObserver(resizeCanvas); + resizeObserver.observe(canvas.parentElement as Element); + + const animate = (): void => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Sort gems by size, smallest to largest + gems.sort((a, b) => a.size - b.size); + + gems.forEach((gem) => { + gem.fall(canvas.height); + gem.draw(ctx, image); + }); + + animationFrameId = requestAnimationFrame(animate); + }; + + resizeCanvas(); // Initial setup + + // Start the animation after the specified delay + const timeoutId = setTimeout(() => { + animate(); + }, delay * 1000); + + return () => { + cancelAnimationFrame(animationFrameId); + resizeObserver.disconnect(); + clearTimeout(timeoutId); + }; + }, [delay]); + + return ( + + + + ); +}; + +export default GemsAnimation; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/styles.ts new file mode 100644 index 000000000..b22c9dff9 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GemsAnimation/styles.ts @@ -0,0 +1,20 @@ +import { m } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled(m.div)` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 0; +`; + +export const Canvas = styled('canvas')` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 0; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/GoalComplete.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/GoalComplete.tsx new file mode 100644 index 000000000..8a864128b --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/GoalComplete.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react'; +import GemsAnimation from '../GemsAnimation/GemsAnimation'; +import { Background, GiftBox, GiftBoxLid, GiftBoxShine, GiftBoxWrapper, IntroBox, Wrapper } from './styles'; +import { Number, Text } from '../styles'; + +import giftBoxImage from '../images/gold_gift_box.png'; +import giftBoxLidImage from '../images/gold_gift_box_lid.png'; +import giftBoxShineImage from '../images/gift_box_shine.png'; + +interface Props { + gems: number; + onAnimationComplete: () => void; +} + +export default function FriendAccepted({ gems, onAnimationComplete }: Props) { + const [showIntro, setShowIntro] = useState(true); + const [showGiftBox, setShowGiftBox] = useState(true); + + const introDuration = 2000; + const mainDuration = 11500; + + useEffect(() => { + const introTimer = setTimeout(() => { + setShowIntro(false); + setShowGiftBox(false); + }, introDuration); + + const mainTimer = setTimeout(() => { + onAnimationComplete(); + }, mainDuration); + + return () => { + clearTimeout(introTimer); + clearTimeout(mainTimer); + }; + }, [onAnimationComplete]); + + return ( + + + + + + + + + + {!showIntro && ( + <> + + {gems.toLocaleString()} + + + + Bonus gems earned +
You reached your gifting goal! +
+ + + + )} + + +
+ ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/styles.ts new file mode 100644 index 000000000..22e30243b --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/GoalComplete/styles.ts @@ -0,0 +1,68 @@ +import styled from 'styled-components'; +import backgroundImage from '../images/goal_complete_bg.png'; +import { m } from 'framer-motion'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + width: 100%; + height: 100%; + color: #fff; +`; + +export const Background = styled(m.div)` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: -1; + + background-image: url(${backgroundImage}); + background-size: cover; + background-position: center; +`; + +export const IntroBox = styled('div')` + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +`; + +export const GiftBoxWrapper = styled(m.div)` + width: 211px; + height: 149px; + position: relative; + z-index: 2; +`; + +export const GiftBox = styled(m.img)` + position: absolute; + top: 0; + left: 0; + z-index: 0; +`; + +export const GiftBoxShine = styled(m.img)` + position: absolute; + top: 0; + left: 0; + z-index: 1; +`; + +export const GiftBoxLid = styled(m.img)` + position: absolute; + top: 0; + left: 0; + z-index: 2; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/bonus_gems_bg.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/bonus_gems_bg.png new file mode 100644 index 000000000..62f0814bc Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/bonus_gems_bg.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/friend_accepted_bg.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/friend_accepted_bg.png new file mode 100644 index 000000000..d581cf543 Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/friend_accepted_bg.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gem.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gem.png new file mode 100644 index 000000000..1523683de Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gem.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gift_box_shine.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gift_box_shine.png new file mode 100644 index 000000000..2cbcda230 Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gift_box_shine.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/goal_complete_bg.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/goal_complete_bg.png new file mode 100644 index 000000000..42f8586e8 Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/goal_complete_bg.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box.png new file mode 100644 index 000000000..d11f388ea Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box_lid.png b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box_lid.png new file mode 100644 index 000000000..3dfc0fc75 Binary files /dev/null and b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/images/gold_gift_box_lid.png differ diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/styles.ts new file mode 100644 index 000000000..d7e22ea24 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Flare/styles.ts @@ -0,0 +1,61 @@ +import { m } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled(m.div)` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + background-color: #000; + z-index: 1; + border-radius: 10px; + overflow: hidden; + + cursor: pointer; +`; + +export const Number = styled(m.div)` + text-align: center; + font-family: Druk, sans-serif; + font-size: 38px; + font-weight: 700; + + position: relative; + z-index: 2; +`; + +export const Text = styled(m.div)` + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: normal; + letter-spacing: -0.36px; + + text-align: center; + + position: relative; + z-index: 2; +`; + +export const TextBottomPosition = styled('div')` + position: absolute; + z-index: 2; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + width: 100%; +`; + +export const TextBottom = styled(m.div)` + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: normal; + letter-spacing: -0.36px; + + text-align: center; + z-index: 2; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/Invite.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/Invite.tsx new file mode 100644 index 000000000..3d3700f2f --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/Invite.tsx @@ -0,0 +1,105 @@ +import { + Wrapper, + InviteButton, + Image, + TextWrapper, + Title, + Text, + GemPill, + BonusWrapper, + BonusText, + Copied, +} from './styles'; +import giftImage from '../../../../images/gift.png'; +import gemImage from '../../../../images/gem.png'; +import boxImage from '../../../../images/gold_box.png'; +import { useAirdropStore } from '@app/store/useAirdropStore'; +import { useEffect, useMemo, useState } from 'react'; +import { AnimatePresence, m } from 'framer-motion'; +import { Trans, useTranslation } from 'react-i18next'; + +export default function Invite() { + const airdropUrl = useAirdropStore((state) => state.backendInMemoryConfig?.airdropUrl || ''); + const { t } = useTranslation(['airdrop'], { useSuspense: false }); + const { userDetails, referralCount, bonusTiers } = useAirdropStore(); + + const referralCode = userDetails?.user?.referral_code || ''; + + const [copied, setCopied] = useState(false); + + const url = `${airdropUrl}/download/${referralCode}`; + + const nextBonusTier = useMemo( + () => bonusTiers?.sort((a, b) => a.target - b.target).find((t) => t.target > (referralCount?.count || 0)), + [bonusTiers, referralCount?.count] + ); + const friendsRemaining = nextBonusTier?.target && (nextBonusTier.target - (referralCount?.count || 0) || 0); + + const handleCopy = () => { + setCopied(true); + navigator.clipboard.writeText(url); + }; + + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false); + }, 2000); + return () => { + clearTimeout(timeout); + }; + } + }, [copied]); + + return ( + + + + {copied && ( + + + {t('linkCopied')} + + + )} + + + + + + {t('inviteFirends')} + + }} + values={{ count: referralCount?.count || 0 }} + /> + + + + + {referralCount?.gems.toLocaleString() || 0} + + + + + {nextBonusTier && ( + + + }} + values={{ + count: ` ${friendsRemaining} `, + bonusGems: nextBonusTier?.bonusGems.toLocaleString(), + }} + /> + + + + )} + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/styles.ts new file mode 100644 index 000000000..35359b7b2 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/Invite/styles.ts @@ -0,0 +1,128 @@ +import { m } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; +`; + +export const InviteButton = styled('button')` + position: relative; + display: flex; + align-items: center; + gap: 7px; + + height: 50px; + padding: 12px 14px 12px 13px; + + border-radius: 60px; + background: #000; + + transition: transform 0.2s ease; + overflow: hidden; + + &:hover { + transform: scale(1.05); + } + + &:disabled { + pointer-events: none; + } +`; + +export const TextWrapper = styled('div')` + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +`; + +export const Image = styled('img')``; + +export const Title = styled('div')` + color: #fff; + text-align: center; + font-size: 12px; + font-weight: 600; +`; + +export const Text = styled('div')` + color: rgba(255, 255, 255, 0.5); + text-align: center; + font-size: 11px; + font-weight: 600; + + span { + color: #fff; + } +`; + +export const GemPill = styled('div')` + display: flex; + height: 27px; + padding: 8px 10px 8px 16px; + justify-content: center; + align-items: center; + gap: 2px; + + border-radius: 100px; + background: linear-gradient(0deg, #c9eb00 0%, #c9eb00 100%), linear-gradient(180deg, #755cff 0%, #2946d9 100%), + linear-gradient(180deg, #ff84a4 0%, #d92958 100%); + + color: #000; + text-align: center; + font-size: 12px; + font-weight: 600; +`; + +export const BonusWrapper = styled('div')` + position: relative; + + .giftImage { + position: absolute; + top: -5px; + right: -15px; + } +`; + +export const BonusText = styled('div')` + display: flex; + align-items: center; + + height: 30px; + padding-left: 12px; + + border-radius: 100px; + background: #f0f0f0; + + color: rgba(0, 0, 0, 0.5); + font-size: 11px; + font-weight: 500; + + strong { + color: #000; + font-weight: 600; + display: inline; + } +`; + +export const Copied = styled(m.div)` + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + + background: #c9eb00; + + color: #000; + text-align: center; + font-size: 12px; + font-weight: 600; + + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/UserInfo.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/UserInfo.tsx new file mode 100644 index 000000000..bfe87a0ef --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/UserInfo.tsx @@ -0,0 +1,23 @@ +import { useAirdropStore } from '@app/store/useAirdropStore'; +import { Avatar, Info, Name, Rank, TrophyImage, Wrapper } from './styles'; +import trophyImage from '../../../../images/trophy.png'; + +export default function UserInfo() { + const { userDetails, userPoints } = useAirdropStore(); + + const profileimageurl = userDetails?.user?.profileimageurl; + const name = userDetails?.user?.name; + const rank = userPoints?.base.rank || userDetails?.user?.rank?.rank || '0'; + + return ( + + + + @{name} + + {parseInt(rank).toLocaleString()} + + + + ); +} diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/styles.ts new file mode 100644 index 000000000..022c12d14 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/segments/UserInfo/styles.ts @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + align-items: center; + gap: 5px; +`; + +export const Avatar = styled('div')<{ $image?: string }>` + background-image: url(${({ $image }) => $image}); + background-size: cover; + background-position: center; + background-color: rgba(0, 0, 0, 0.1); + width: 36px; + height: 36px; + border-radius: 50%; +`; + +export const Info = styled('div')` + display: flex; + flex-direction: column; + gap: 4px; +`; + +export const Name = styled('div')` + color: #090719; + font-size: 14px; + font-weight: 700; + line-height: 100%; + letter-spacing: -0.28px; +`; + +export const Rank = styled('div')` + color: rgba(79, 79, 79, 0.75); + font-size: 12px; + font-weight: 500; + line-height: 100%; + letter-spacing: -0.24px; + + display: flex; + align-items: center; + gap: 4px; +`; + +export const TrophyImage = styled('img')``; diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/styles.ts new file mode 100644 index 000000000..ea047ff6f --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedIn/styles.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +`; + +export const UserRow = styled('div')` + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + width: 100%; +`; diff --git a/src/containers/Airdrop/AirdropLogin/ConnectButton/ConnectButton.tsx b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx similarity index 50% rename from src/containers/Airdrop/AirdropLogin/ConnectButton/ConnectButton.tsx rename to src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx index 424b4a07f..6be9dfc8f 100644 --- a/src/containers/Airdrop/AirdropLogin/ConnectButton/ConnectButton.tsx +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/LoggedOut.tsx @@ -1,25 +1,30 @@ -import { useAirdropStore, INSTALL_BONUS_GEMS } from '@app/store/useAirdropStore.ts'; -import { NumberPill, StyledButton, XIcon, IconCircle, Text, Gem1, Gem2, Gem3 } from './styles.ts'; -import { useCallback, useEffect } from 'react'; +import { GIFT_GEMS, useAirdropStore } from '@app/store/useAirdropStore'; +import Gems from '../../components/Gems/Gems'; +import { ClaimButton, Wrapper } from './styles'; +import { useCallback, useEffect, useState } from 'react'; import { open } from '@tauri-apps/api/shell'; import { v4 as uuidv4 } from 'uuid'; -import gem1Image from './images/gem-1.png'; -import gem2Image from './images/gem-2.png'; -import gem3Image from './images/gem-3.png'; +import ClaimModal from '../../components/Claimmodal/ClaimModal'; import { useTranslation } from 'react-i18next'; -export default function ConnectButton() { - const { authUuid, setAuthUuid, setAirdropTokens, setUserPoints, backendInMemoryConfig, wipUI } = useAirdropStore(); - +export default function LoggedOut() { + const [modalIsOpen, setModalIsOpen] = useState(false); const { t } = useTranslation(['airdrop'], { useSuspense: false }); + const { referralQuestPoints, authUuid, setAuthUuid, setAirdropTokens, setUserPoints, backendInMemoryConfig } = + useAirdropStore(); - const handleAuth = useCallback(() => { - const token = uuidv4(); - if (backendInMemoryConfig?.airdropUrl) { - setAuthUuid(token); - open(`${backendInMemoryConfig?.airdropUrl}/auth?tauri=${token}`); - } - }, [backendInMemoryConfig?.airdropUrl, setAuthUuid]); + const handleAuth = useCallback( + (code?: string) => { + const token = uuidv4(); + if (backendInMemoryConfig?.airdropTwitterAuthUrl) { + setAuthUuid(token); + open( + `${backendInMemoryConfig?.airdropTwitterAuthUrl}/auth?tauri=${token}${code ? `&universeReferral=${code}` : ''}` + ); + } + }, + [backendInMemoryConfig?.airdropTwitterAuthUrl, setAuthUuid] + ); useEffect(() => { if (authUuid && backendInMemoryConfig?.airdropApiUrl) { @@ -55,21 +60,16 @@ export default function ConnectButton() { } }, [authUuid, backendInMemoryConfig?.airdropApiUrl, setAirdropTokens, setAuthUuid, setUserPoints]); - if (!wipUI) return null; - return ( - - - - - - +{INSTALL_BONUS_GEMS} - - {t('loginButton')} + <> + + setModalIsOpen(true)}> + {t('claimGems')} + - - - - + + + {modalIsOpen && setModalIsOpen(false)} />} + ); } diff --git a/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/styles.ts new file mode 100644 index 000000000..e4beb3789 --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/sections/LoggedOut/styles.ts @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + width: 100%; +`; + +export const ClaimButton = styled('button')` + height: 40px; + padding: 12px 35px; + + border-radius: 60px; + background: #000; + + color: #fff; + text-align: center; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: normal; + + cursor: pointer; + white-space: nowrap; + + span { + display: block; + transition: transform 0.2s ease; + } + + &:hover { + span { + transform: scale(1.075); + } + } +`; diff --git a/src/containers/Airdrop/AirdropGiftTracker/styles.ts b/src/containers/Airdrop/AirdropGiftTracker/styles.ts new file mode 100644 index 000000000..88c1cf31b --- /dev/null +++ b/src/containers/Airdrop/AirdropGiftTracker/styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: 10px; + + width: 100%; + padding: 15px 20px 20px 20px; + + border-radius: 10px; + background: #fff; + box-shadow: 0px 4px 45px 0px rgba(0, 0, 0, 0.08); + + position: relative; +`; + +export const TitleWrapper = styled('div')` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +`; + +export const Title = styled('div')` + color: #797979; + font-size: 12px; + font-weight: 500; +`; diff --git a/src/containers/Airdrop/AirdropLogin/AirdropLogin.tsx b/src/containers/Airdrop/AirdropLogin/AirdropLogin.tsx deleted file mode 100644 index 10b413dbf..000000000 --- a/src/containers/Airdrop/AirdropLogin/AirdropLogin.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useAirdropStore } from '@app/store/useAirdropStore'; -import ConnectButton from './ConnectButton/ConnectButton'; -import UserInfo from './UserInfo/UserInfo'; -import { AirdropLoginPosition } from './styles'; -import { useAirdropSyncState } from '@app/hooks/airdrop/useAirdropSyncState'; - -export default function AirdropLogin() { - useAirdropSyncState(); - const { airdropTokens, wipUI } = useAirdropStore(); - - if (!wipUI) return null; - - const isLoggedIn = !!airdropTokens; - - return {!isLoggedIn ? : }; -} diff --git a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-1.png b/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-1.png deleted file mode 100644 index 897d425b1..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-1.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-2.png b/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-2.png deleted file mode 100644 index b97c49163..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-2.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-3.png b/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-3.png deleted file mode 100644 index e4d4da0c0..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/ConnectButton/images/gem-3.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/ConnectButton/styles.ts b/src/containers/Airdrop/AirdropLogin/ConnectButton/styles.ts deleted file mode 100644 index fbe261ed1..000000000 --- a/src/containers/Airdrop/AirdropLogin/ConnectButton/styles.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { FaXTwitter } from 'react-icons/fa6'; -import styled, { keyframes } from 'styled-components'; - -const rotateGem1 = keyframes` - 0%, 100% { - transform: rotate(0deg) scale(1); - } - 50% { - transform: rotate(20deg) scale(1.1); - } -`; - -const rotateGem2 = keyframes` - 0%, 100% { - transform: rotate(0deg) scale(1); - } - 50% { - transform: rotate(-20deg) scale(1.1); - } -`; - -const rotateGem3 = keyframes` - 0%, 100% { - transform: rotate(0deg) scale(1); - } - 50% { - transform: rotate(-20deg) scale(1.1); - } -`; - -export const StyledButton = styled('button')` - padding: 0px 4px 0 8px; - - display: flex; - align-items: center; - gap: 10px; - - text-transform: none; - pointer-events: all; - - border-radius: 70px; - height: 36px; - - border: 1px solid #000; - background: #000; - transition: transform 0.2s ease; - text-transform: unset; - position: relative; - - &:hover { - border: 1px solid #000; - background: #000; - transform: scale(1.05); - - .ConnectButton-Gem1 { - animation: ${rotateGem1} 0.4s ease-in-out; - } - - .ConnectButton-Gem2 { - animation: ${rotateGem2} 0.4s ease-in-out; - } - - .ConnectButton-Gem3 { - animation: ${rotateGem3} 0.4s ease-in-out; - } - } -`; - -export const NumberPill = styled('div')` - color: #fff; - background: #000; - font-size: 13px; - font-weight: 600; - - border-radius: 100px; - background: linear-gradient(180deg, #ff84a4 0%, #d92958 100%); - - display: flex; - align-items: center; - justify-content: center; - gap: 1px; - - min-width: 63px; - height: 23px; - - padding: 0 8px; - - .StatsIcon-gems { - transform: translateX(-2px); - } -`; - -export const Text = styled('div')` - color: #fff; - font-size: 13px; - font-weight: 600; - text-transform: none; - pointer-events: none; -`; - -export const IconCircle = styled('div')` - background: #fff; - border-radius: 100%; - - display: flex; - align-items: center; - justify-content: center; - - height: 28px; - width: 28px; -`; - -export const XIcon = styled(FaXTwitter)` - fill: #000; - width: 15px; - height: 17px; - flex-shrink: 0; -`; - -export const Gem1 = styled('img')` - position: absolute; - top: 1px; - left: -18px; - transition: transform 0.2s ease; -`; - -export const Gem2 = styled('img')` - position: absolute; - top: -9px; - left: 7px; - transition: transform 0.2s ease; -`; - -export const Gem3 = styled('img')` - position: absolute; - top: -15px; - left: 58px; - transition: transform 0.2s ease; -`; diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/DownloadReferralModal.tsx b/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/DownloadReferralModal.tsx deleted file mode 100644 index 60c887a61..000000000 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/DownloadReferralModal/DownloadReferralModal.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { - BoxWrapper, - CopyButton, - CopyText, - Cover, - Gem1, - Gem2, - Gem3, - GemsWrapper, - GemTextImage, - ShareText, - ShareWrapper, - Text, - TextWrapper, - Title, - Wrapper, -} from './styles'; -import gemImage from '../images/gems.png'; -import gemLargeImage from './images/gem-large.png'; -import { useEffect, useState } from 'react'; -import { useAirdropStore, GIFT_GEMS, REFERRAL_GEMS } from '@app/store/useAirdropStore'; -import { Button } from '@app/components/elements/Button'; -import { Input } from '@app/components/elements/inputs/Input'; -import { useAridropRequest } from '@app/hooks/airdrop/stateHelpers/useGetAirdropUserDetails'; -import { Trans, useTranslation } from 'react-i18next'; - -interface DownloadReferralModalProps { - referralCode?: string; - onClose: () => void; -} - -interface ClaimResponse { - success: boolean; - message: string; - inviter?: string; -} - -export default function DownloadReferralModal({ referralCode, onClose }: DownloadReferralModalProps) { - const airdropUrl = useAirdropStore((state) => state.backendInMemoryConfig?.airdropUrl || ''); - const { t } = useTranslation(['airdrop'], { useSuspense: false }); - const [copied, setCopied] = useState(false); - const [error, setError] = useState(''); - - const handleRequest = useAridropRequest(); - - const [claimCode, setClaimCode] = useState(''); - - const handleReferral = async () => { - await handleRequest({ - path: `/miner/download/referral/${claimCode}`, - method: 'POST', - body: {}, - }) - .then((r) => { - if (r?.success) { - setError(''); - setClaimCode(''); - onClose(); - } - setError(r?.message || 'Something went wrong'); - }) - .catch((e) => { - console.error(e); - setError('Something went wrong'); - }); - }; - - const url = `${airdropUrl}/download/${referralCode}`; - const handleCopy = () => { - setCopied(true); - navigator.clipboard.writeText(url); - }; - - useEffect(() => { - if (copied) { - const timeout = setTimeout(() => { - setCopied(false); - }, 2000); - return () => { - clearTimeout(timeout); - }; - } - }, [copied]); - - const claimMarkup = ( - <> - - - <Trans - ns="airdrop" - i18nKey="claimReferralCode" - components={{ span: <span />, image: <GemTextImage src={gemImage} alt="" /> }} - values={{ gems: GIFT_GEMS }} - /> - - {t('claimReferralGifts', { gems: GIFT_GEMS })} - -
- - setClaimCode(e.target.value)} - value={claimCode} - /> - - - {error && {error}} -
- - ); - - const inviteMarkup = ( - <> - - - <Trans - ns="airdrop" - i18nKey="claimReferralCode" - components={{ span: <span />, image: <GemTextImage src={gemImage} alt="" /> }} - values={{ gems: REFERRAL_GEMS }} - /> - - {t('giftReferralCode', { gems: REFERRAL_GEMS })} - - - {url.replace('https://', '')} - - {copied ? ( - - Copied! - - ) : ( - - Copy - - )} - - - - ); - - return ( - - - - - - - - - {referralCode ? inviteMarkup : claimMarkup} - - - - - ); -} diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/GemsPill.tsx b/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/GemsPill.tsx deleted file mode 100644 index d80547b1c..000000000 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/GemsPill.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { StatsPill, StatsNumber, StatsIcon, GemsAnimation, GemImage } from '../styles'; -import gemImage from '../images/gems.png'; -import { AnimatePresence, useSpring } from 'framer-motion'; -import { useEffect, useState } from 'react'; - -interface Props { - value: number; -} - -export default function GemsPill({ value }: Props) { - const [displayValue, setDisplayValue] = useState(0); - const [animate, setAnimate] = useState(false); - - const getRandomX = (() => { - let lastValue = 20; - return () => { - lastValue = lastValue === 20 ? -20 : 20; - return lastValue; - }; - })(); - - const getRandomRotation = (() => { - let lastValue = 40; - return () => { - lastValue = lastValue === 40 ? -40 : 40; - return lastValue; - }; - })(); - - const spring = useSpring(0, { mass: 0.8, stiffness: 50, damping: 20 }); - - spring.on('change', (latest: number) => { - setDisplayValue(Math.round(latest)); - }); - - useEffect(() => { - spring.set(value); - }, [spring, value]); - - useEffect(() => { - setAnimate(true); - const timer = setTimeout(() => setAnimate(false), 1000); - return () => clearTimeout(timer); - }, [displayValue]); - - return ( - - {displayValue.toLocaleString()} - - - - - {animate && ( - - {[...Array(8)].map((_, i) => ( - - ))} - - )} - - - ); -} diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/styles.ts b/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/styles.ts deleted file mode 100644 index 3c8e36843..000000000 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/GemsPill/styles.ts +++ /dev/null @@ -1,3 +0,0 @@ -import styled from 'styled-components'; - -export const Wrapper = styled('div')``; diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/UserInfo.tsx b/src/containers/Airdrop/AirdropLogin/UserInfo/UserInfo.tsx deleted file mode 100644 index da50fc950..000000000 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/UserInfo.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { - Wrapper, - StatsPill, - StatsNumber, - StatsIcon, - Divider, - StatsGroup, - NotificationsButton, - Dot, - StyledAvatar, - Menu, - MenuItem, - MenuWrapper, -} from './styles'; - -import gemImage from './images/gems.png'; -import { FaBell } from 'react-icons/fa6'; -import { useCallback, useEffect, useState } from 'react'; -import { GIFT_GEMS, REFERRAL_GEMS, useAirdropStore } from '@app/store/useAirdropStore'; -import { useTranslation } from 'react-i18next'; -import { AnimatePresence } from 'framer-motion'; -import DownloadReferralModal from './DownloadReferralModal/DownloadReferralModal'; -import { NumberPill } from '../ConnectButton/styles'; -import GemsPill from './GemsPill/GemsPill'; - -export default function UserInfo() { - const { logout, userDetails, airdropTokens, userPoints, wipUI, referralCount } = useAirdropStore(); - const [open, setOpen] = useState(false); - const [modalOpen, setModalOpen] = useState<'claim' | 'invite' | undefined>(undefined); - - const { t } = useTranslation(['airdrop'], { useSuspense: false }); - - const profileimageurl = userDetails?.user?.profileimageurl; - const rank = userPoints?.base.rank || userDetails?.user?.rank?.rank; - - const handleClick = () => { - setOpen(!open); - }; - const handleClose = () => { - setOpen(false); - }; - - const handleLogout = () => { - logout(); - }; - - const handleReferalClose = () => { - setModalOpen(undefined); - }; - - const handleClickOutside = useCallback( - (event: MouseEvent) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (open && (event.target as any)?.id !== 'avatar-wrapper') { - handleClose(); - } - }, - [open] - ); - - useEffect(() => { - document.addEventListener('click', handleClickOutside); - return () => document.removeEventListener('click', handleClickOutside); - }, [handleClickOutside]); - - const [gems, setGems] = useState(0); - - useEffect(() => { - setGems(userPoints?.base.gems || userDetails?.user?.rank?.gems || 0); - }, [userPoints?.base.gems, userDetails?.user?.rank?.gems]); - - if (!wipUI) return null; - if (!airdropTokens?.token) return null; - - const showNotificationButton = false; - - return ( - <> - - - - - {referralCount?.count ? ( - - {referralCount.count} 🎁 - - ) : null} - - {rank && ( - - Rank #{parseInt(rank).toLocaleString()} - - )} - - - - - {showNotificationButton && ( - - - - - )} - - - - - {open && ( - - setModalOpen('invite')}> - {t('referral')}{' '} - - + - {REFERRAL_GEMS} - - - setModalOpen('claim')}> - {t('claimGems')}{' '} - - +{GIFT_GEMS} - - - {t('logout')} - - )} - - - - - {modalOpen && ( - - )} - - - ); -} diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/images/avatar.png b/src/containers/Airdrop/AirdropLogin/UserInfo/images/avatar.png deleted file mode 100644 index 756c7946a..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/UserInfo/images/avatar.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/images/hammers.png b/src/containers/Airdrop/AirdropLogin/UserInfo/images/hammers.png deleted file mode 100644 index d05772741..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/UserInfo/images/hammers.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/images/shells.png b/src/containers/Airdrop/AirdropLogin/UserInfo/images/shells.png deleted file mode 100644 index 71dfae951..000000000 Binary files a/src/containers/Airdrop/AirdropLogin/UserInfo/images/shells.png and /dev/null differ diff --git a/src/containers/Airdrop/AirdropLogin/UserInfo/styles.ts b/src/containers/Airdrop/AirdropLogin/UserInfo/styles.ts deleted file mode 100644 index da740b026..000000000 --- a/src/containers/Airdrop/AirdropLogin/UserInfo/styles.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { m } from 'framer-motion'; -import styled, { css, keyframes } from 'styled-components'; - -const ring = keyframes` - 0%, 100% { - transform: rotate(0deg); - } - 25% { - transform: rotate(15deg); - } - 50% { - transform: rotate(-15deg); - } - 75% { - transform: rotate(15deg); - } -`; - -export const Wrapper = styled('div')` - display: flex; - align-items: center; - gap: 13px; -`; - -export const StatsGroup = styled('div')` - display: flex; - align-items: center; - gap: 8px; -`; - -export const StatsPill = styled('div')` - display: flex; - align-items: center; - gap: 5px; - - border-radius: 20px; - background-color: #fff; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); - - height: 36px; - padding: 0 15px; - pointer-events: all; - position: relative; -`; - -export const StatsNumber = styled('div')` - color: #000; - font-size: 15px; - font-weight: 600; -`; - -export const StatsIcon = styled('img')` - width: 18px; -`; - -export const Divider = styled('div')` - width: 1px; - height: 28px; - background: rgba(0, 0, 0, 0.1); - flex-shrink: 0; -`; - -export const NotificationsButton = styled('button')` - background: none; - border: none; - cursor: pointer; - pointer-events: all; - - position: relative; - border-radius: 100%; - border: 1px solid rgba(0, 0, 0, 0.1); - - width: 36px; - height: 36px; - - display: flex; - align-items: center; - justify-content: center; - - .NotificationsButtonIcon { - width: 16px; - height: 16px; - } - - transition: border-color 0.2s ease; - - &:hover { - border-color: rgba(0, 0, 0, 0.3); - - .NotificationsButtonIcon { - animation: ${ring} 0.5s ease-in-out; - transform-origin: top center; - } - } -`; - -export const Dot = styled('div')<{ $color: 'green' | 'red' }>` - width: 12px; - height: 12px; - border-radius: 100%; - - position: absolute; - top: -2px; - right: -2px; - - border: 2px solid #d0d0d0; - - background: ${({ $color }) => ($color === 'green' ? '#47D85E' : '#FF0000')}; -`; - -export const StyledAvatar = styled('div')<{ $img?: string }>` - position: relative; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - font-size: 1.25rem; - line-height: 1; - border-radius: 50%; - overflow: hidden; - user-select: none; - pointer-events: all; - cursor: pointer; - background-color: rgba(0, 0, 0, 0.1); - width: 36px; - height: 36px; - background-size: cover; - background-position: center; - - ${({ $img }) => - $img && - css` - background-image: url(${$img}); - `} - - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); -`; - -export const Menu = styled(m.div)` - position: absolute; - top: 100%; - right: 0; - z-index: 2; - - border-radius: 10px; - background: #fff; - box-shadow: 0px 14px 25px 0px rgba(0, 0, 0, 0.15); - - display: flex; - flex-direction: column; - align-items: flex-start; - - min-width: 205px; - padding: 10px; - pointer-events: all; - - margin-top: 12px; -`; - -export const MenuItem = styled('div')` - color: #000; - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: normal; - border-radius: 5px; - - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - - width: 100%; - padding: 10px 15px; - cursor: pointer; - transition: background-color 0.2s ease; - - white-space: nowrap; - - &:hover { - background-color: rgba(0, 0, 0, 0.05); - } - - .StatsIcon-gems { - width: 12px; - position: relative; - z-index: 2; - } -`; - -export const MenuWrapper = styled('div')` - position: relative; -`; - -export const GemsAnimation = styled(m.div)` - position: absolute; - top: 10px; - right: 33px; - z-index: 0; -`; - -export const GemImage = styled(m.img)` - position: absolute; - top: 0; - left: 0; -`; diff --git a/src/containers/Airdrop/AirdropLogin/styles.ts b/src/containers/Airdrop/AirdropLogin/styles.ts deleted file mode 100644 index 82fcccc59..000000000 --- a/src/containers/Airdrop/AirdropLogin/styles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import styled from 'styled-components'; - -export const AirdropLoginPosition = styled('div')` - position: fixed; - top: 20px; - right: 20px; - z-index: 2; - pointer-events: none; -`; diff --git a/src/containers/Airdrop/AirdropPermission/AirdropPermission.tsx b/src/containers/Airdrop/AirdropPermission/AirdropPermission.tsx index 45ffb59c0..2f34a6981 100644 --- a/src/containers/Airdrop/AirdropPermission/AirdropPermission.tsx +++ b/src/containers/Airdrop/AirdropPermission/AirdropPermission.tsx @@ -2,12 +2,11 @@ import { useCallback } from 'react'; import gemImage from './images/gem.png'; import { useTranslation } from 'react-i18next'; import { ToggleSwitch } from '@app/components/elements/ToggleSwitch'; -import { useAirdropStore } from '@app/store/useAirdropStore'; import { useAppConfigStore } from '@app/store/useAppConfigStore'; import { BoxWrapper, Gem1, Gem2, Gem3, Gem4, Position, Text, TextWrapper, Title } from './styles'; export default function AirdropPermission() { - const wipUI = useAirdropStore((state) => state.wipUI); + const airdropUIEnabled = useAppConfigStore((s) => s.airdrop_ui_enabled); const allowTelemetry = useAppConfigStore((s) => s.allow_telemetry); const setAllowTelemetry = useAppConfigStore((s) => s.setAllowTelemetry); const { t } = useTranslation(['airdrop'], { useSuspense: false }); @@ -19,7 +18,7 @@ export default function AirdropPermission() { return ( - {wipUI && ( + {airdropUIEnabled && ( <> @@ -29,8 +28,8 @@ export default function AirdropPermission() { )} - {t(wipUI ? 'permission.title' : 'permissionNoGems.title')} - {t(wipUI ? 'permission.text' : 'permissionNoGems.text')} + {t(airdropUIEnabled ? 'permission.title' : 'permissionNoGems.title')} + {t(airdropUIEnabled ? 'permission.text' : 'permissionNoGems.text')} diff --git a/src/containers/Airdrop/Settings/Logout.tsx b/src/containers/Airdrop/Settings/Logout.tsx new file mode 100644 index 000000000..06adf8175 --- /dev/null +++ b/src/containers/Airdrop/Settings/Logout.tsx @@ -0,0 +1,18 @@ +import { Button } from '@app/components/elements/Button'; +import { useAirdropStore } from '@app/store/useAirdropStore'; +import { useAppConfigStore } from '@app/store/useAppConfigStore'; + +export default function AirdropLogout() { + const airdropUIEnabled = useAppConfigStore((s) => s.airdrop_ui_enabled); + const logout = useAirdropStore((state) => state.logout); + const { userDetails } = useAirdropStore(); + + if (!airdropUIEnabled || !userDetails) return null; + return ( +
+ +
+ ); +} diff --git a/src/containers/Airdrop/Settings/ToggleAirdropUi.tsx b/src/containers/Airdrop/Settings/ToggleAirdropUi.tsx deleted file mode 100644 index bf2c6749a..000000000 --- a/src/containers/Airdrop/Settings/ToggleAirdropUi.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// import { ToggleSwitch } from '@app/components/elements/ToggleSwitch'; -import { Wrapper } from './styles'; -// import { useAirdropStore } from '@app/store/useAirdropStore'; -// import { useCallback, useRef, useState } from 'react'; - -export const ToggleAirdropUi = () => { - // const { wipUI, setWipUI } = useAirdropStore(); - // const [disabled, setDisabled] = useState(!wipUI); - // const clickCountRef = useRef(0); - - // const toggleWipUI = useCallback(() => { - // if (disabled) { - // if (clickCountRef.current > 9) { - // setDisabled(false); - // } else { - // clickCountRef.current = clickCountRef.current + 1; - // return; - // } - // } - // setWipUI(!wipUI); - // }, [disabled, setWipUI, wipUI]); - - return {/* */}; -}; diff --git a/src/containers/Settings/ExperimentalSettings.tsx b/src/containers/Settings/ExperimentalSettings.tsx index 1e326b7fc..20f2ef214 100644 --- a/src/containers/Settings/ExperimentalSettings.tsx +++ b/src/containers/Settings/ExperimentalSettings.tsx @@ -7,7 +7,6 @@ import DebugSettings from '@app/containers/Settings/sections/experimental/DebugS import AppVersions from '@app/containers/Settings/sections/experimental/AppVersions.tsx'; import VisualMode from '@app/containers/Dashboard/components/VisualMode.tsx'; import { SettingsGroup, SettingsGroupWrapper } from '@app/containers/Settings/components/SettingsGroup.styles.ts'; -import { ToggleAirdropUi } from '@app/containers/Airdrop/Settings/ToggleAirdropUi.tsx'; export const ExperimentalSettings = () => { const showExperimental = useUIStore((s) => s.showExperimental); @@ -26,7 +25,6 @@ export const ExperimentalSettings = () => { - diff --git a/src/containers/Settings/GeneralSettings.tsx b/src/containers/Settings/GeneralSettings.tsx index 2c2505c48..7e502cebe 100644 --- a/src/containers/Settings/GeneralSettings.tsx +++ b/src/containers/Settings/GeneralSettings.tsx @@ -2,6 +2,7 @@ import AirdropPermissionSettings from './sections/general/AirdropPermissionSetti import LogsSettings from './sections/general/LogsSettings.tsx'; import LanguageSettings from './sections/general/LanguageSettings.tsx'; import { ResetSettingsButton } from './sections/general/ResetSettingsButton.tsx'; +import AirdropLogout from '../Airdrop/Settings/Logout.tsx'; export const GeneralSettings = () => { return ( @@ -10,6 +11,7 @@ export const GeneralSettings = () => { + ); }; diff --git a/src/containers/Settings/sections/general/AirdropPermissionSettings.tsx b/src/containers/Settings/sections/general/AirdropPermissionSettings.tsx index 5e767c8de..f92c37350 100644 --- a/src/containers/Settings/sections/general/AirdropPermissionSettings.tsx +++ b/src/containers/Settings/sections/general/AirdropPermissionSettings.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'react-i18next'; import { ToggleSwitch } from '@app/components/elements/ToggleSwitch'; -import { useAirdropStore } from '@app/store/useAirdropStore'; import { useAppConfigStore } from '@app/store/useAppConfigStore'; import { useCallback } from 'react'; import { @@ -13,7 +12,7 @@ import { import { Typography } from '@app/components/elements/Typography.tsx'; export default function AirdropPermissionSettings() { - const wipUI = useAirdropStore((state) => state.wipUI); + const airdropUIEnabled = useAppConfigStore((s) => s.airdrop_ui_enabled); const allowTelemetry = useAppConfigStore((s) => s.allow_telemetry); const setAllowTelemetry = useAppConfigStore((s) => s.setAllowTelemetry); const { t } = useTranslation(['airdrop'], { useSuspense: false }); @@ -27,9 +26,11 @@ export default function AirdropPermissionSettings() { - {t(wipUI ? 'permission.title' : 'permissionNoGems.title')} + + {t(airdropUIEnabled ? 'permission.title' : 'permissionNoGems.title')} + - {t(wipUI ? 'permission.text' : 'permissionNoGems.text')} + {t(airdropUIEnabled ? 'permission.text' : 'permissionNoGems.text')} diff --git a/src/containers/SideBar/SideBar.tsx b/src/containers/SideBar/SideBar.tsx index a9074e5bb..e280c0a3c 100644 --- a/src/containers/SideBar/SideBar.tsx +++ b/src/containers/SideBar/SideBar.tsx @@ -4,6 +4,7 @@ import Heading from './components/Heading'; import { SideBarContainer, SideBarInner, BottomContainer, TopContainer } from './styles'; import MiningButton from '@app/containers/Dashboard/MiningView/components/MiningButton.tsx'; +import AirdropGiftTracker from '@app/containers/Airdrop/AirdropGiftTracker/AirdropGiftTracker'; import { Divider } from '@app/components/elements/Divider.tsx'; import LostConnectionAlert from './components/LostConnectionAlert'; @@ -20,6 +21,7 @@ function SideBar() { + diff --git a/src/containers/SideBar/styles.ts b/src/containers/SideBar/styles.ts index 38afc1428..93c1c280d 100644 --- a/src/containers/SideBar/styles.ts +++ b/src/containers/SideBar/styles.ts @@ -42,6 +42,9 @@ export const BottomContainer = styled(m.div)` justify-self: flex-end; width: 100%; padding: 0 16px; + + flex-direction: column; + gap: 13px; `; // Wallet diff --git a/src/hooks/airdrop/stateHelpers/useAirdropUserPointsListener.ts b/src/hooks/airdrop/stateHelpers/useAirdropUserPointsListener.ts index 0e053e80d..f75548a90 100644 --- a/src/hooks/airdrop/stateHelpers/useAirdropUserPointsListener.ts +++ b/src/hooks/airdrop/stateHelpers/useAirdropUserPointsListener.ts @@ -4,7 +4,11 @@ import { useEffect } from 'react'; export const useAirdropUserPointsListener = () => { const setUserPoints = useAirdropStore((state) => state.setUserPoints); + const referralCount = useAirdropStore((state) => state.referralCount); + const bonusTiers = useAirdropStore((state) => state.bonusTiers); const setUserPointsReferralCount = useAirdropStore((state) => state.setReferralCount); + const setFlareAnimationType = useAirdropStore((state) => state.setFlareAnimationType); + useEffect(() => { let unListen: () => void = () => { //do nothing @@ -15,7 +19,20 @@ export const useAirdropUserPointsListener = () => { const payload = event.payload as UserPoints; setUserPoints(payload); if (payload.referralCount) { - setUserPointsReferralCount(payload.referralCount); + if (referralCount?.count !== payload.referralCount.count) { + if (referralCount?.count) { + setFlareAnimationType('FriendAccepted'); + if ( + payload.referralCount.count && + bonusTiers?.find((t) => t.target === payload?.referralCount?.count) + ) { + setTimeout(() => { + setFlareAnimationType('GoalComplete'); + }, 2000); + } + } + setUserPointsReferralCount(payload.referralCount); + } } } }) @@ -27,5 +44,6 @@ export const useAirdropUserPointsListener = () => { return () => { unListen(); }; - }, [setUserPoints, setUserPointsReferralCount]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bonusTiers, referralCount?.count]); }; diff --git a/src/hooks/airdrop/stateHelpers/useGetAirdropUserDetails.ts b/src/hooks/airdrop/stateHelpers/useGetAirdropUserDetails.ts index 87cf133c7..815272bab 100644 --- a/src/hooks/airdrop/stateHelpers/useGetAirdropUserDetails.ts +++ b/src/hooks/airdrop/stateHelpers/useGetAirdropUserDetails.ts @@ -1,32 +1,9 @@ -import { useAirdropStore, UserEntryPoints, UserDetails, ReferralCount } from '@app/store/useAirdropStore'; +import { useAirdropStore, UserEntryPoints, UserDetails, ReferralCount, BonusTier } from '@app/store/useAirdropStore'; import { useCallback, useEffect } from 'react'; - -interface RequestProps { - path: string; - method: 'GET' | 'POST'; - body?: Record; -} - -export const useAridropRequest = () => { - const airdropToken = useAirdropStore((state) => state.airdropTokens?.token); - const baseUrl = useAirdropStore((state) => state.backendInMemoryConfig?.airdropApiUrl); - - return async ({ body, method, path }: RequestProps) => { - if (!baseUrl || !airdropToken) return; - - const response = await fetch(`${baseUrl}${path}`, { - method: method, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${airdropToken}`, - }, - body: JSON.stringify(body), - }); - return response.json() as Promise; - }; -}; +import { useAridropRequest } from '../utils/useHandleRequest'; export const useGetAirdropUserDetails = () => { + const baseUrl = useAirdropStore((state) => state.backendInMemoryConfig?.airdropApiUrl); const airdropToken = useAirdropStore((state) => state.airdropTokens?.token); const userDetails = useAirdropStore((state) => state.userDetails); const setUserDetails = useAirdropStore((state) => state.setUserDetails); @@ -34,17 +11,24 @@ export const useGetAirdropUserDetails = () => { const setReferralCount = useAirdropStore((state) => state.setReferralCount); const setAcceptedReferral = useAirdropStore((state) => state.setAcceptedReferral); const handleRequest = useAridropRequest(); + const setBonusTiers = useAirdropStore((state) => state.setBonusTiers); + const logout = useAirdropStore((state) => state.logout); + // GET USER DETAILS const fetchUserDetails = useCallback(async () => { - const data = await handleRequest({ + return handleRequest({ path: '/user/details', method: 'GET', + onError: logout, + }).then((data) => { + if (data?.user.id) { + setUserDetails(data); + return data.user; + } }); - if (!data?.user.id) return; - setUserDetails(data); - return data.user; - }, [handleRequest, setUserDetails]); + }, [handleRequest, logout, setUserDetails]); + // GET USER POINTS const fetchUserPoints = useCallback(async () => { const data = await handleRequest({ path: '/user/score', @@ -60,6 +44,7 @@ export const useGetAirdropUserDetails = () => { }); }, [handleRequest, setUserPoints]); + // GET USER REFERRAL POINTS const fetchUserReferralPoints = useCallback(async () => { const data = await handleRequest<{ count: ReferralCount }>({ path: '/miner/download/referral-count', @@ -74,29 +59,43 @@ export const useGetAirdropUserDetails = () => { const fetchAcceptedReferral = useCallback(async () => { const data = await handleRequest<{ claimed: boolean }>({ - path: '/miner/claimed-referral', + path: '/miner/download/claimed-referral', method: 'GET', }); setAcceptedReferral(!!data?.claimed); }, [handleRequest, setAcceptedReferral]); + // FETCH BONUS TIERS + const fetchBonusTiers = useCallback(async () => { + const data = await handleRequest<{ tiers: BonusTier[] }>({ + path: '/miner/download/bonus-tiers', + method: 'GET', + }); + if (!data?.tiers) return; + setBonusTiers(data?.tiers); + }, [handleRequest, setBonusTiers]); + + // FETCH ALL USER DATA useEffect(() => { const fetchData = async () => { const details = await fetchUserDetails(); - const requests: (() => Promise)[] = []; + if (!details) return; + + const requests: Promise[] = []; if (!details?.rank.gems) { - requests.push(fetchUserPoints); + requests.push(fetchUserPoints()); } - requests.push(fetchUserReferralPoints); - requests.push(fetchAcceptedReferral); + requests.push(fetchUserReferralPoints()); + requests.push(fetchAcceptedReferral()); + requests.push(fetchBonusTiers()); await Promise.all(requests); }; - if (!userDetails?.user?.id) { + if (!userDetails?.user?.id && airdropToken) { fetchData(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [airdropToken, userDetails?.user?.id]); + }, [airdropToken, userDetails, baseUrl]); }; diff --git a/src/hooks/airdrop/stateHelpers/useGetReferralQuestPoints.ts b/src/hooks/airdrop/stateHelpers/useGetReferralQuestPoints.ts new file mode 100644 index 000000000..a75928fc6 --- /dev/null +++ b/src/hooks/airdrop/stateHelpers/useGetReferralQuestPoints.ts @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useAridropRequest } from '../utils/useHandleRequest'; +import { useAirdropStore } from '@app/store/useAirdropStore'; + +export enum QuestNames { + MinerReceivedGift = 'miner-received-gift', + MinerQuestReferral = 'quest-download-referral', +} + +interface QuestData { + displayName: string; + isNew: boolean; + name: QuestNames; + pointTypeName: string; + points: number; + questFulfilled: boolean; + isHidden: boolean; +} + +interface QuestDataResponse { + quests: QuestData[]; +} + +export const useGetReferralQuestPoints = () => { + const handleRequest = useAridropRequest(); + const { setReferralQuestPoints } = useAirdropStore(); + + useEffect(() => { + const handleFetch = async () => { + const response = await handleRequest({ + path: `/quest/list-with-fulfillment`, + method: 'GET', + }); + + if (!response?.quests.length) return; + const reducedQuest = response.quests.reduce( + (acc, quest) => { + if (quest.name === QuestNames.MinerReceivedGift) { + acc.pointsForClaimingReferral = quest.points; + } + if (quest.name === QuestNames.MinerQuestReferral) { + acc.pointsPerReferral = quest.points; + } + return acc; + }, + { + pointsPerReferral: 0, + pointsForClaimingReferral: 0, + } + ); + setReferralQuestPoints(reducedQuest); + }; + + handleFetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +}; diff --git a/src/hooks/airdrop/useAirdropSyncState.ts b/src/hooks/airdrop/useAirdropSyncState.ts index 0160c163b..ea64831c9 100644 --- a/src/hooks/airdrop/useAirdropSyncState.ts +++ b/src/hooks/airdrop/useAirdropSyncState.ts @@ -1,6 +1,7 @@ import { useAirdropTokensRefresh } from './stateHelpers/useAirdropTokensRefresh'; import { useAirdropUserPointsListener } from './stateHelpers/useAirdropUserPointsListener'; import { useGetAirdropUserDetails } from './stateHelpers/useGetAirdropUserDetails'; +import { useGetReferralQuestPoints } from './stateHelpers/useGetReferralQuestPoints'; import { useGetRustInMemoryConfig } from './stateHelpers/useGetRustInMemoryConfig'; export const useAirdropSyncState = () => { @@ -8,4 +9,5 @@ export const useAirdropSyncState = () => { useAirdropTokensRefresh(); useGetAirdropUserDetails(); useAirdropUserPointsListener(); + useGetReferralQuestPoints(); }; diff --git a/src/hooks/airdrop/utils/useHandleRequest.ts b/src/hooks/airdrop/utils/useHandleRequest.ts new file mode 100644 index 000000000..ca17e50e9 --- /dev/null +++ b/src/hooks/airdrop/utils/useHandleRequest.ts @@ -0,0 +1,43 @@ +import { useAirdropStore } from '@app/store/useAirdropStore'; + +interface RequestProps { + path: string; + method: 'GET' | 'POST'; + body?: Record; + onError?: (e: unknown) => void; +} + +export const useAridropRequest = () => { + const airdropToken = useAirdropStore((state) => state.airdropTokens?.token); + const baseUrl = useAirdropStore((state) => state.backendInMemoryConfig?.airdropApiUrl); + + return async ({ body, method, path, onError }: RequestProps) => { + if (!baseUrl || !airdropToken) return; + + const response = await fetch(`${baseUrl}${path}`, { + method: method, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${airdropToken}`, + }, + body: JSON.stringify(body), + }); + + try { + if (!response.ok) { + console.error('Error fetching airdrop data', response); + if (onError) { + onError(response); + } + return; + } + return response.json() as Promise; + } catch (e) { + console.error('Error fetching airdrop data', e); + if (onError) { + onError(e); + } + return; + } + }; +}; diff --git a/src/store/useAirdropStore.ts b/src/store/useAirdropStore.ts index a171afee4..61ac451ab 100644 --- a/src/store/useAirdropStore.ts +++ b/src/store/useAirdropStore.ts @@ -23,6 +23,13 @@ function parseJwt(token: string): TokenResponse { } ////////////////////////////////////////// +// + +export interface BonusTier { + id: string; + target: number; + bonusGems: number; +} interface TokenResponse { exp: number; @@ -101,28 +108,40 @@ export interface BackendInMemoryConfig { airdropTwitterAuthUrl: string; } +type AnimationType = 'GoalComplete' | 'FriendAccepted' | 'BonusGems'; + +export interface ReferralQuestPoints { + pointsPerReferral: number; + pointsForClaimingReferral: number; +} + ////////////////////////////////////////// interface AirdropState { authUuid: string; - wipUI?: boolean; acceptedReferral?: boolean; airdropTokens?: AirdropTokens; userDetails?: UserDetails; userPoints?: UserPoints; referralCount?: ReferralCount; backendInMemoryConfig?: BackendInMemoryConfig; + flareAnimationType?: AnimationType; + bonusTiers?: BonusTier[]; + referralQuestPoints?: ReferralQuestPoints; } interface AirdropStore extends AirdropState { + setReferralQuestPoints: (referralQuestPoints: ReferralQuestPoints) => void; + setAuthUuid: (authUuid: string) => void; setAirdropTokens: (airdropToken: AirdropTokens) => void; setUserDetails: (userDetails?: UserDetails) => void; setUserPoints: (userPoints: UserPoints) => void; - setWipUI: (wipUI: boolean) => void; setBackendInMemoryConfig: (config?: BackendInMemoryConfig) => void; setReferralCount: (referralCount: ReferralCount) => void; setAcceptedReferral: (acceptedReferral: boolean) => void; + setFlareAnimationType: (flareAnimationType?: AnimationType) => void; + setBonusTiers: (bonusTiers: BonusTier[]) => void; logout: () => void; } @@ -134,11 +153,14 @@ const clearState: AirdropState = { userPoints: undefined, }; +const NOT_PERSISTED_KEYS = ['userPoints', 'backendInMemoryConfig', 'userDetails', 'authUuid', 'referralCount']; export const useAirdropStore = create()( persist( (set) => ({ authUuid: '', - setWipUI: (wipUI) => set({ wipUI }), + setReferralQuestPoints: (referralQuestPoints) => set({ referralQuestPoints }), + setFlareAnimationType: (flareAnimationType) => set({ flareAnimationType }), + setBonusTiers: (bonusTiers) => set({ bonusTiers }), setAcceptedReferral: (acceptedReferral) => set({ acceptedReferral }), logout: () => set(clearState), setUserDetails: (userDetails) => set({ userDetails }), @@ -157,9 +179,7 @@ export const useAirdropStore = create()( { name: 'airdrop-store', partialize: (state) => - Object.fromEntries( - Object.entries(state).filter(([key]) => !['userPoints', 'backendInMemoryConfig'].includes(key)) - ), + Object.fromEntries(Object.entries(state).filter(([key]) => !NOT_PERSISTED_KEYS.includes(key))), } ) ); diff --git a/src/store/useAppConfigStore.ts b/src/store/useAppConfigStore.ts index 744c0b712..30fe1dab5 100644 --- a/src/store/useAppConfigStore.ts +++ b/src/store/useAppConfigStore.ts @@ -34,6 +34,7 @@ const initialState: State = { monero_address: '', gpu_mining_enabled: true, cpu_mining_enabled: true, + airdrop_ui_enabled: false, }; export const useAppConfigStore = create()((set) => ({ diff --git a/src/types/app-status.ts b/src/types/app-status.ts index 473b8af14..3f40028f4 100644 --- a/src/types/app-status.ts +++ b/src/types/app-status.ts @@ -16,6 +16,7 @@ export interface AppConfig { monero_address: string; gpu_mining_enabled: boolean; cpu_mining_enabled: boolean; + airdrop_ui_enabled: boolean; } export interface CpuMinerMetrics {